From 506e63a969949251cd5a883c32e5b6b90877a226 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Jan 2026 00:25:04 +0100 Subject: [PATCH] Add thinkingText theme token, fix streaming toggle bug - Add configurable thinkingText color for thinking blocks (defaults to muted) - Make 'Thinking...' label italic when collapsed - Fix Ctrl+T during streaming hiding the current message - Track streamingMessage to properly re-render on toggle Based on #366 by @paulbettner --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/docs/theme.md | 2 + .../components/assistant-message.ts | 7 ++-- .../src/modes/interactive/interactive-mode.ts | 39 ++++++++++++------- .../src/modes/interactive/theme/dark.json | 1 + .../src/modes/interactive/theme/light.json | 1 + .../src/modes/interactive/theme/theme.ts | 2 + 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 84caa259..4872c4b7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -190,6 +190,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo ### Added - **Snake game example hook**: Demonstrates `ui.custom()`, `registerCommand()`, and session persistence. See [examples/hooks/snake.ts](examples/hooks/snake.ts). +- **`thinkingText` theme token**: Configurable color for thinking block text. ([#366](https://github.com/badlogic/pi-mono/pull/366) by [@paulbettner](https://github.com/paulbettner)) ### Changed @@ -198,6 +199,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo ### Fixed +- **Toggling thinking blocks during streaming shows nothing**: Pressing Ctrl+T while streaming would hide the current message until streaming completed. - **Resuming session resets thinking level to off**: Initial model and thinking level were not saved to session file, causing `--resume`/`--continue` to default to `off`. ([#342](https://github.com/badlogic/pi-mono/issues/342) by [@aliou](https://github.com/aliou)) - **Hook `tool_result` event ignores errors from custom tools**: The `tool_result` hook event was never emitted when tools threw errors, and always had `isError: false` for successful executions. Now emits the event with correct `isError` value in both success and error cases. ([#374](https://github.com/badlogic/pi-mono/issues/374) by [@nicobailon](https://github.com/nicobailon)) - **Edit tool fails on Windows due to CRLF line endings**: Files with CRLF line endings now match correctly when LLMs send LF-only text. Line endings are normalized before matching and restored to original style on write. ([#355](https://github.com/badlogic/pi-mono/issues/355) by [@Pratham-Dubey](https://github.com/Pratham-Dubey)) diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md index bc6064f1..aba7643b 100644 --- a/packages/coding-agent/docs/theme.md +++ b/packages/coding-agent/docs/theme.md @@ -22,6 +22,7 @@ Every theme must define all color tokens. There are no optional colors. | `muted` | Secondary/dimmed text | Metadata, descriptions, output | | `dim` | Very dimmed text | Less important info, placeholders | | `text` | Default text color | Main content (usually `""`) | +| `thinkingText` | Thinking block text | Assistant reasoning traces | ### Backgrounds & Content Text (11 colors) @@ -119,6 +120,7 @@ Themes are defined in JSON files with the following structure: "colors": { "accent": "blue", "muted": "gray", + "thinkingText": "gray", "text": "", ... } diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index 8757e76c..01c919f3 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -53,16 +53,15 @@ export class AssistantMessageComponent extends Container { if (this.hideThinkingBlock) { // Show static "Thinking..." label when hidden - this.contentContainer.addChild(new Text(theme.fg("muted", "Thinking..."), 1, 0)); + this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0)); if (hasTextAfter) { this.contentContainer.addChild(new Spacer(1)); } } else { - // Thinking traces in muted color, italic - // Use Markdown component with default text style for consistent styling + // Thinking traces in thinkingText color, italic this.contentContainer.addChild( new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), { - color: (text: string) => theme.fg("muted", text), + color: (text: string) => theme.fg("thinkingText", text), italic: true, }), ); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index eb520f46..ccb21042 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -93,6 +93,7 @@ export class InteractiveMode { // Streaming message tracking private streamingComponent: AssistantMessageComponent | undefined = undefined; + private streamingMessage: AssistantMessage | undefined = undefined; // Tool execution tracking: toolCallId -> component private pendingTools = new Map(); @@ -839,18 +840,19 @@ export class InteractiveMode { this.ui.requestRender(); } else if (event.message.role === "assistant") { this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock); + this.streamingMessage = event.message; this.chatContainer.addChild(this.streamingComponent); - this.streamingComponent.updateContent(event.message); + this.streamingComponent.updateContent(this.streamingMessage); this.ui.requestRender(); } break; case "message_update": if (this.streamingComponent && event.message.role === "assistant") { - const assistantMsg = event.message as AssistantMessage; - this.streamingComponent.updateContent(assistantMsg); + this.streamingMessage = event.message; + this.streamingComponent.updateContent(this.streamingMessage); - for (const content of assistantMsg.content) { + for (const content of this.streamingMessage.content) { if (content.type === "toolCall") { if (!this.pendingTools.has(content.id)) { this.chatContainer.addChild(new Text("", 0, 0)); @@ -881,12 +883,14 @@ export class InteractiveMode { case "message_end": if (event.message.role === "user") break; if (this.streamingComponent && event.message.role === "assistant") { - const assistantMsg = event.message as AssistantMessage; - this.streamingComponent.updateContent(assistantMsg); + this.streamingMessage = event.message; + this.streamingComponent.updateContent(this.streamingMessage); - if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") { + if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") { const errorMessage = - assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error"; + this.streamingMessage.stopReason === "aborted" + ? "Operation aborted" + : this.streamingMessage.errorMessage || "Error"; for (const [, component] of this.pendingTools.entries()) { component.updateResult({ content: [{ type: "text", text: errorMessage }], @@ -896,6 +900,7 @@ export class InteractiveMode { this.pendingTools.clear(); } this.streamingComponent = undefined; + this.streamingMessage = undefined; this.footer.invalidate(); } this.ui.requestRender(); @@ -948,6 +953,7 @@ export class InteractiveMode { if (this.streamingComponent) { this.chatContainer.removeChild(this.streamingComponent); this.streamingComponent = undefined; + this.streamingMessage = undefined; } this.pendingTools.clear(); this.ui.requestRender(); @@ -1329,14 +1335,17 @@ export class InteractiveMode { this.hideThinkingBlock = !this.hideThinkingBlock; this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock); - for (const child of this.chatContainer.children) { - if (child instanceof AssistantMessageComponent) { - child.setHideThinkingBlock(this.hideThinkingBlock); - } - } - + // Rebuild chat from session messages this.chatContainer.clear(); this.rebuildChatFromMessages(); + + // If streaming, re-add the streaming component with updated visibility and re-render + if (this.streamingComponent && this.streamingMessage) { + this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock); + this.streamingComponent.updateContent(this.streamingMessage); + this.chatContainer.addChild(this.streamingComponent); + } + this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`); } @@ -1738,6 +1747,7 @@ export class InteractiveMode { // Clear UI state this.pendingMessagesContainer.clear(); this.streamingComponent = undefined; + this.streamingMessage = undefined; this.pendingTools.clear(); // Switch session via AgentSession (emits hook and tool session events) @@ -2004,6 +2014,7 @@ export class InteractiveMode { this.chatContainer.clear(); this.pendingMessagesContainer.clear(); this.streamingComponent = undefined; + this.streamingMessage = undefined; this.pendingTools.clear(); this.chatContainer.addChild(new Spacer(1)); diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index f55be9f7..069e32fd 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -29,6 +29,7 @@ "muted": "gray", "dim": "dimGray", "text": "", + "thinkingText": "gray", "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index a4276853..138af303 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -28,6 +28,7 @@ "muted": "mediumGray", "dim": "dimGray", "text": "", + "thinkingText": "mediumGray", "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index c0d8bf66..0121e199 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -34,6 +34,7 @@ const ThemeJsonSchema = Type.Object({ muted: ColorValueSchema, dim: ColorValueSchema, text: ColorValueSchema, + thinkingText: ColorValueSchema, // Backgrounds & Content Text (11 colors) selectedBg: ColorValueSchema, userMessageBg: ColorValueSchema, @@ -98,6 +99,7 @@ export type ThemeColor = | "muted" | "dim" | "text" + | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel"