Refactor selectors: replace show/hide pairs with single showSelector helper

This commit is contained in:
Mario Zechner 2025-12-09 01:04:55 +01:00
parent fd7f20f968
commit dbd5f5eb0b

View file

@ -10,6 +10,7 @@ import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
import type { SlashCommand } from "@mariozechner/pi-tui"; import type { SlashCommand } from "@mariozechner/pi-tui";
import { import {
CombinedAutocompleteProvider, CombinedAutocompleteProvider,
type Component,
Container, Container,
Input, Input,
Loader, Loader,
@ -72,15 +73,6 @@ export class InteractiveMode {
// Tool execution tracking: toolCallId -> component // Tool execution tracking: toolCallId -> component
private pendingTools = new Map<string, ToolExecutionComponent>(); private pendingTools = new Map<string, ToolExecutionComponent>();
// Selector components
private thinkingSelector: ThinkingSelectorComponent | null = null;
private queueModeSelector: QueueModeSelectorComponent | null = null;
private themeSelector: ThemeSelectorComponent | null = null;
private modelSelector: ModelSelectorComponent | null = null;
private userMessageSelector: UserMessageSelectorComponent | null = null;
private sessionSelector: SessionSelectorComponent | null = null;
private oauthSelector: OAuthSelectorComponent | null = null;
// Track if this is the first user message (to skip spacer) // Track if this is the first user message (to skip spacer)
private isFirstUserMessage = true; private isFirstUserMessage = true;
@ -891,143 +883,128 @@ export class InteractiveMode {
// Selectors // Selectors
// ========================================================================= // =========================================================================
private showThinkingSelector(): void { /**
this.thinkingSelector = new ThinkingSelectorComponent( * Shows a selector component in place of the editor.
this.session.thinkingLevel, * @param create Factory that receives a `done` callback and returns the component and focus target
(level) => { */
this.session.setThinkingLevel(level); private showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
this.updateEditorBorderColor(); const done = () => {
this.chatContainer.addChild(new Spacer(1)); this.editorContainer.clear();
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0)); this.editorContainer.addChild(this.editor);
this.hideThinkingSelector(); this.ui.setFocus(this.editor);
this.ui.requestRender(); };
}, const { component, focus } = create(done);
() => {
this.hideThinkingSelector();
this.ui.requestRender();
},
);
this.editorContainer.clear(); this.editorContainer.clear();
this.editorContainer.addChild(this.thinkingSelector); this.editorContainer.addChild(component);
this.ui.setFocus(this.thinkingSelector.getSelectList()); this.ui.setFocus(focus);
this.ui.requestRender(); this.ui.requestRender();
} }
private hideThinkingSelector(): void { private showThinkingSelector(): void {
this.editorContainer.clear(); this.showSelector((done) => {
this.editorContainer.addChild(this.editor); const selector = new ThinkingSelectorComponent(
this.thinkingSelector = null; this.session.thinkingLevel,
this.ui.setFocus(this.editor); (level) => {
this.session.setThinkingLevel(level);
this.updateEditorBorderColor();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0));
done();
this.ui.requestRender();
},
() => {
done();
this.ui.requestRender();
},
);
return { component: selector, focus: selector.getSelectList() };
});
} }
private showQueueModeSelector(): void { private showQueueModeSelector(): void {
this.queueModeSelector = new QueueModeSelectorComponent( this.showSelector((done) => {
this.session.queueMode, const selector = new QueueModeSelectorComponent(
(mode) => { this.session.queueMode,
this.session.setQueueMode(mode); (mode) => {
this.chatContainer.addChild(new Spacer(1)); this.session.setQueueMode(mode);
this.chatContainer.addChild(new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0)); this.chatContainer.addChild(new Spacer(1));
this.hideQueueModeSelector(); this.chatContainer.addChild(new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0));
this.ui.requestRender(); done();
}, this.ui.requestRender();
() => { },
this.hideQueueModeSelector(); () => {
this.ui.requestRender(); done();
}, this.ui.requestRender();
); },
this.editorContainer.clear(); );
this.editorContainer.addChild(this.queueModeSelector); return { component: selector, focus: selector.getSelectList() };
this.ui.setFocus(this.queueModeSelector.getSelectList()); });
this.ui.requestRender();
}
private hideQueueModeSelector(): void {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.queueModeSelector = null;
this.ui.setFocus(this.editor);
} }
private showThemeSelector(): void { private showThemeSelector(): void {
const currentTheme = this.settingsManager.getTheme() || "dark"; const currentTheme = this.settingsManager.getTheme() || "dark";
this.themeSelector = new ThemeSelectorComponent( this.showSelector((done) => {
currentTheme, const selector = new ThemeSelectorComponent(
(themeName) => { currentTheme,
const result = setTheme(themeName); (themeName) => {
this.settingsManager.setTheme(themeName); const result = setTheme(themeName);
this.ui.invalidate(); this.settingsManager.setTheme(themeName);
this.chatContainer.addChild(new Spacer(1));
if (result.success) {
this.chatContainer.addChild(new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0));
} else {
this.chatContainer.addChild(
new Text(
theme.fg(
"error",
`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`,
),
1,
0,
),
);
}
this.hideThemeSelector();
this.ui.requestRender();
},
() => {
this.hideThemeSelector();
this.ui.requestRender();
},
(themeName) => {
const result = setTheme(themeName);
if (result.success) {
this.ui.invalidate(); this.ui.invalidate();
this.chatContainer.addChild(new Spacer(1));
if (result.success) {
this.chatContainer.addChild(new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0));
} else {
this.chatContainer.addChild(
new Text(
theme.fg(
"error",
`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`,
),
1,
0,
),
);
}
done();
this.ui.requestRender(); this.ui.requestRender();
} },
}, () => {
); done();
this.editorContainer.clear(); this.ui.requestRender();
this.editorContainer.addChild(this.themeSelector); },
this.ui.setFocus(this.themeSelector.getSelectList()); (themeName) => {
this.ui.requestRender(); const result = setTheme(themeName);
} if (result.success) {
this.ui.invalidate();
private hideThemeSelector(): void { this.ui.requestRender();
this.editorContainer.clear(); }
this.editorContainer.addChild(this.editor); },
this.themeSelector = null; );
this.ui.setFocus(this.editor); return { component: selector, focus: selector.getSelectList() };
});
} }
private showModelSelector(): void { private showModelSelector(): void {
this.modelSelector = new ModelSelectorComponent( this.showSelector((done) => {
this.ui, const selector = new ModelSelectorComponent(
this.session.model, this.ui,
this.settingsManager, this.session.model,
(model) => { this.settingsManager,
this.agent.setModel(model); (model) => {
this.sessionManager.saveModelChange(model.provider, model.id); this.agent.setModel(model);
this.chatContainer.addChild(new Spacer(1)); this.sessionManager.saveModelChange(model.provider, model.id);
this.chatContainer.addChild(new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0)); this.chatContainer.addChild(new Spacer(1));
this.hideModelSelector(); this.chatContainer.addChild(new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0));
this.ui.requestRender(); done();
}, this.ui.requestRender();
() => { },
this.hideModelSelector(); () => {
this.ui.requestRender(); done();
}, this.ui.requestRender();
); },
this.editorContainer.clear(); );
this.editorContainer.addChild(this.modelSelector); return { component: selector, focus: selector };
this.ui.setFocus(this.modelSelector); });
this.ui.requestRender();
}
private hideModelSelector(): void {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.modelSelector = null;
this.ui.setFocus(this.editor);
} }
private showUserMessageSelector(): void { private showUserMessageSelector(): void {
@ -1040,53 +1017,44 @@ export class InteractiveMode {
return; return;
} }
this.userMessageSelector = new UserMessageSelectorComponent( this.showSelector((done) => {
userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), const selector = new UserMessageSelectorComponent(
(entryIndex) => { userMessages.map((m) => ({ index: m.entryIndex, text: m.text })),
const selectedText = this.session.branch(entryIndex); (entryIndex) => {
this.chatContainer.clear(); const selectedText = this.session.branch(entryIndex);
this.isFirstUserMessage = true; this.chatContainer.clear();
this.renderInitialMessages(this.session.state); this.isFirstUserMessage = true;
this.chatContainer.addChild(new Spacer(1)); this.renderInitialMessages(this.session.state);
this.chatContainer.addChild(new Text(theme.fg("dim", "Branched to new session"), 1, 0)); this.chatContainer.addChild(new Spacer(1));
this.editor.setText(selectedText); this.chatContainer.addChild(new Text(theme.fg("dim", "Branched to new session"), 1, 0));
this.hideUserMessageSelector(); this.editor.setText(selectedText);
this.ui.requestRender(); done();
}, this.ui.requestRender();
() => { },
this.hideUserMessageSelector(); () => {
this.ui.requestRender(); done();
}, this.ui.requestRender();
); },
this.editorContainer.clear(); );
this.editorContainer.addChild(this.userMessageSelector); return { component: selector, focus: selector.getMessageList() };
this.ui.setFocus(this.userMessageSelector.getMessageList()); });
this.ui.requestRender();
}
private hideUserMessageSelector(): void {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.userMessageSelector = null;
this.ui.setFocus(this.editor);
} }
private showSessionSelector(): void { private showSessionSelector(): void {
this.sessionSelector = new SessionSelectorComponent( this.showSelector((done) => {
this.sessionManager, const selector = new SessionSelectorComponent(
async (sessionPath) => { this.sessionManager,
this.hideSessionSelector(); async (sessionPath) => {
await this.handleResumeSession(sessionPath); done();
}, await this.handleResumeSession(sessionPath);
() => { },
this.hideSessionSelector(); () => {
this.ui.requestRender(); done();
}, this.ui.requestRender();
); },
this.editorContainer.clear(); );
this.editorContainer.addChild(this.sessionSelector); return { component: selector, focus: selector.getSessionList() };
this.ui.setFocus(this.sessionSelector.getSessionList()); });
this.ui.requestRender();
} }
private async handleResumeSession(sessionPath: string): Promise<void> { private async handleResumeSession(sessionPath: string): Promise<void> {
@ -1115,13 +1083,6 @@ export class InteractiveMode {
this.ui.requestRender(); this.ui.requestRender();
} }
private hideSessionSelector(): void {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.sessionSelector = null;
this.ui.setFocus(this.editor);
}
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> { private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
if (mode === "logout") { if (mode === "logout") {
const loggedInProviders = listOAuthProviders(); const loggedInProviders = listOAuthProviders();
@ -1135,95 +1096,90 @@ export class InteractiveMode {
} }
} }
this.oauthSelector = new OAuthSelectorComponent( this.showSelector((done) => {
mode, const selector = new OAuthSelectorComponent(
async (providerId: string) => { mode,
this.hideOAuthSelector(); async (providerId: string) => {
done();
if (mode === "login") { if (mode === "login") {
this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0)); this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0));
this.ui.requestRender(); this.ui.requestRender();
try { try {
await login( await login(
providerId as SupportedOAuthProvider, providerId as SupportedOAuthProvider,
(url: string) => { (url: string) => {
this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0)); this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0)); this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild( this.chatContainer.addChild(
new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0), new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0),
); );
this.ui.requestRender();
const openCmd =
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
exec(`${openCmd} "${url}"`);
},
async () => {
return new Promise<string>((resolve) => {
const codeInput = new Input();
codeInput.onSubmit = () => {
const code = codeInput.getValue();
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.ui.setFocus(this.editor);
resolve(code);
};
this.editorContainer.clear();
this.editorContainer.addChild(codeInput);
this.ui.setFocus(codeInput);
this.ui.requestRender(); this.ui.requestRender();
});
},
);
invalidateOAuthCache(); const openCmd =
this.chatContainer.addChild(new Spacer(1)); process.platform === "darwin"
this.chatContainer.addChild( ? "open"
new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0), : process.platform === "win32"
); ? "start"
this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0)); : "xdg-open";
this.ui.requestRender(); exec(`${openCmd} "${url}"`);
} catch (error: unknown) { },
this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`); async () => {
return new Promise<string>((resolve) => {
const codeInput = new Input();
codeInput.onSubmit = () => {
const code = codeInput.getValue();
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.ui.setFocus(this.editor);
resolve(code);
};
this.editorContainer.clear();
this.editorContainer.addChild(codeInput);
this.ui.setFocus(codeInput);
this.ui.requestRender();
});
},
);
invalidateOAuthCache();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0),
);
this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
this.ui.requestRender();
} catch (error: unknown) {
this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
try {
await logout(providerId as SupportedOAuthProvider);
invalidateOAuthCache();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),
);
this.chatContainer.addChild(
new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0),
);
this.ui.requestRender();
} catch (error: unknown) {
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
}
} }
} else { },
try { () => {
await logout(providerId as SupportedOAuthProvider); done();
invalidateOAuthCache(); this.ui.requestRender();
this.chatContainer.addChild(new Spacer(1)); },
this.chatContainer.addChild( );
new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0), return { component: selector, focus: selector };
); });
this.chatContainer.addChild(
new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0),
);
this.ui.requestRender();
} catch (error: unknown) {
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
},
() => {
this.hideOAuthSelector();
this.ui.requestRender();
},
);
this.editorContainer.clear();
this.editorContainer.addChild(this.oauthSelector);
this.ui.setFocus(this.oauthSelector);
this.ui.requestRender();
}
private hideOAuthSelector(): void {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.oauthSelector = null;
this.ui.setFocus(this.editor);
} }
// ========================================================================= // =========================================================================