diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index adecfa0e..e5478b13 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -31,6 +31,7 @@ ### Added +- **`/resume` Command**: Switch to a different session mid-conversation. Opens an interactive selector showing all available sessions. Equivalent to the `--resume` CLI flag but can be used without restarting the agent. ([#117](https://github.com/badlogic/pi-mono/pull/117) by [@hewliyang](https://github.com/hewliyang)) - **`authHeader` option in models.json**: Custom providers can set `"authHeader": true` to automatically add `Authorization: Bearer ` header. Useful for providers that require explicit auth headers. ([#81](https://github.com/badlogic/pi-mono/issues/81)) - **`--append-system-prompt` Flag**: Append additional text or file contents to the system prompt. Supports both inline text and file paths. Complements `--system-prompt` for layering custom instructions without replacing the base system prompt. ([#114](https://github.com/badlogic/pi-mono/pull/114) by [@markusylisiurunen](https://github.com/markusylisiurunen)) - **Thinking Block Toggle**: Added `Ctrl+T` shortcut to toggle visibility of LLM thinking blocks. When toggled off, shows a static "Thinking..." label instead of full content. Useful for reducing visual clutter during long conversations. ([#113](https://github.com/badlogic/pi-mono/pull/113) by [@markusylisiurunen](https://github.com/markusylisiurunen)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 5445a486..302f80f8 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -502,6 +502,16 @@ This allows you to explore alternative conversation paths without losing your cu /branch ``` +### /resume + +Switch to a different session. Opens an interactive selector showing all available sessions. Select a session to load it and continue where you left off. + +This is equivalent to the `--resume` CLI flag but can be used mid-session. + +``` +/resume +``` + ### /login Login with OAuth to use subscription-based models (Claude Pro/Max): diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index f3fc8124..5447654c 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -42,6 +42,7 @@ import { FooterComponent } from "./footer.js"; import { ModelSelectorComponent } from "./model-selector.js"; import { OAuthSelectorComponent } from "./oauth-selector.js"; import { QueueModeSelectorComponent } from "./queue-mode-selector.js"; +import { SessionSelectorComponent } from "./session-selector.js"; import { ThemeSelectorComponent } from "./theme-selector.js"; import { ThinkingSelectorComponent } from "./thinking-selector.js"; import { ToolExecutionComponent } from "./tool-execution.js"; @@ -95,6 +96,9 @@ export class TuiRenderer { // User message selector (for branching) private userMessageSelector: UserMessageSelectorComponent | null = null; + // Session selector (for resume) + private sessionSelector: SessionSelectorComponent | null = null; + // OAuth selector private oauthSelector: any | null = null; @@ -214,6 +218,11 @@ export class TuiRenderer { description: "Toggle automatic context compaction", }; + const resumeCommand: SlashCommand = { + name: "resume", + description: "Resume a different session", + }; + // Load hide thinking block setting this.hideThinkingBlock = settingsManager.getHideThinkingBlock(); @@ -243,6 +252,7 @@ export class TuiRenderer { clearCommand, compactCommand, autocompactCommand, + resumeCommand, ...fileSlashCommands, ], process.cwd(), @@ -488,6 +498,13 @@ export class TuiRenderer { return; } + // Check for /resume command + if (text === "/resume") { + this.showSessionSelector(); + this.editor.setText(""); + return; + } + // Check for file-based slash commands text = expandSlashCommand(text, this.fileCommands); @@ -1490,6 +1507,95 @@ export class TuiRenderer { this.ui.setFocus(this.editor); } + private showSessionSelector(): void { + // Create session selector + this.sessionSelector = new SessionSelectorComponent( + this.sessionManager, + async (sessionPath) => { + this.hideSessionSelector(); + await this.handleResumeSession(sessionPath); + }, + () => { + // Just hide the selector + this.hideSessionSelector(); + this.ui.requestRender(); + }, + ); + + // Replace editor with selector + this.editorContainer.clear(); + this.editorContainer.addChild(this.sessionSelector); + this.ui.setFocus(this.sessionSelector.getSessionList()); + this.ui.requestRender(); + } + + private async handleResumeSession(sessionPath: string): Promise { + // Unsubscribe first to prevent processing events during transition + this.unsubscribe?.(); + + // Abort and wait for completion + this.agent.abort(); + await this.agent.waitForIdle(); + + // Stop loading animation + if (this.loadingAnimation) { + this.loadingAnimation.stop(); + this.loadingAnimation = null; + } + this.statusContainer.clear(); + + // Clear UI state + this.queuedMessages = []; + this.pendingMessagesContainer.clear(); + this.streamingComponent = null; + this.pendingTools.clear(); + + // Set the selected session as active + this.sessionManager.setSessionFile(sessionPath); + + // Reload the session + const loaded = loadSessionFromEntries(this.sessionManager.loadEntries()); + this.agent.replaceMessages(loaded.messages); + + // Restore model if saved in session + const savedModel = this.sessionManager.loadModel(); + if (savedModel) { + const availableModels = (await getAvailableModels()).models; + const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId); + if (match) { + this.agent.setModel(match); + } + } + + // Restore thinking level if saved in session + const savedThinking = this.sessionManager.loadThinkingLevel(); + if (savedThinking) { + this.agent.setThinkingLevel(savedThinking as ThinkingLevel); + } + + // Resubscribe to agent + this.subscribeToAgent(); + + // Clear and re-render the chat + this.chatContainer.clear(); + this.isFirstUserMessage = true; + this.renderInitialMessages(this.agent.state); + + // Show confirmation message + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Text(theme.fg("dim", "Resumed session"), 1, 0)); + + this.ui.requestRender(); + } + + private hideSessionSelector(): void { + // Replace selector with editor in the container + this.editorContainer.clear(); + this.editorContainer.addChild(this.editor); + this.sessionSelector = null; + this.ui.setFocus(this.editor); + } + private async showOAuthSelector(mode: "login" | "logout"): Promise { // For logout mode, filter to only show logged-in providers let providersToShow: string[] = [];