From d5e0cb463088716118220e2a66a4ac8fb12435e7 Mon Sep 17 00:00:00 2001 From: Hew Li Yang Date: Fri, 5 Dec 2025 18:47:57 +0800 Subject: [PATCH 1/3] wip: add /resume slash command --- packages/coding-agent/README.md | 10 +++ packages/coding-agent/src/tui/tui-renderer.ts | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index ec4809fc..630fc48b 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 13e66778..6254ec45 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); @@ -1468,6 +1485,53 @@ export class TuiRenderer { this.ui.setFocus(this.editor); } + private showSessionSelector(): void { + // Create session selector + this.sessionSelector = new SessionSelectorComponent( + this.sessionManager, + (sessionPath) => { + // Set the selected session as active + this.sessionManager.setSessionFile(sessionPath); + + // Reload the session + const loaded = loadSessionFromEntries(this.sessionManager.loadEntries()); + this.agent.replaceMessages(loaded.messages); + + // 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)); + + // Hide selector and show editor again + this.hideSessionSelector(); + this.ui.requestRender(); + }, + () => { + // 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 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[] = []; From 30f63bcaf6d610791c782bec000ec158f8165365 Mon Sep 17 00:00:00 2001 From: Hew Li Yang Date: Fri, 5 Dec 2025 18:56:44 +0800 Subject: [PATCH 2/3] wip: changelog --- packages/coding-agent/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ff0512ee..e5ee9007 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,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)) From 4bd52cf3ed19acbb9c041f344df7b5f9632e5c48 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 5 Dec 2025 22:53:51 +0100 Subject: [PATCH 3/3] feat(coding-agent): add /resume command to switch sessions mid-conversation - Opens interactive session selector - Properly aborts in-flight agent turns before switching - Restores model and thinking level from resumed session - Clears UI state (queued messages, pending tools, etc.) closes #117 --- packages/coding-agent/src/tui/tui-renderer.ts | 80 ++++++++++++++----- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 6254ec45..bd21490d 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -1489,26 +1489,9 @@ export class TuiRenderer { // Create session selector this.sessionSelector = new SessionSelectorComponent( this.sessionManager, - (sessionPath) => { - // Set the selected session as active - this.sessionManager.setSessionFile(sessionPath); - - // Reload the session - const loaded = loadSessionFromEntries(this.sessionManager.loadEntries()); - this.agent.replaceMessages(loaded.messages); - - // 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)); - - // Hide selector and show editor again + async (sessionPath) => { this.hideSessionSelector(); - this.ui.requestRender(); + await this.handleResumeSession(sessionPath); }, () => { // Just hide the selector @@ -1524,6 +1507,65 @@ export class TuiRenderer { 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();