diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c4e820d64..2ab039d80 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixed +- Fixed Ctrl+T thinking-block toggles clearing the pending user message and loader before the assistant stream starts ([#2370](https://github.com/can1357/oh-my-pi/issues/2370)). - Fixed snapcompact archives going partially unreadable on OpenRouter, which hard-caps requests at 8 images and silently drops the excess: compaction now passes a provider-aware frame budget (`providerFrameBudget`) so the archive never exceeds the cap (unknown providers get a safe floor of 5), with overflow kept as a text tail on the summary instead of rendering frames the gateway would drop; inline system-prompt/tool-result imaging shares the same per-provider budget (`providerImageBudget`), so once existing images exhaust it (e.g. 8 archive frames on OpenRouter) tool results ship verbatim as text - Fixed Hindsight retains to send offset-aware local timestamps instead of UTC `Z` strings so extraction prompts keep the user's local time-of-day context ([#2363](https://github.com/can1357/oh-my-pi/issues/2363)). - Fixed tool calls taller than the viewport reading as cut off while streaming (the head reappeared only once the result landed): the 15.11.6 stranded-preview fix marked every collapsed pending tool preview commit-unstable, which also blocked durable top-anchored streams — e.g. a task call's context/assignment markdown — from reaching native scrollback mid-run. Commit stability is now classified per renderer (`ToolRenderer.provisionalPendingPreview`): only the tail-window previews the result render re-anchors (edit/apply_patch streamed-diff tails, bash/ssh command caps, eval cells with interleaved outputs) stay provisional; every other pending preview commits its settled head mid-stream again diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index d55986fa2..732eab1dc 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -4,6 +4,7 @@ import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui"; import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils"; import { getRoleInfo } from "../../config/model-roles"; import { isSettingsInitialized, settings } from "../../config/settings"; +import { AssistantMessageComponent } from "../../modes/components/assistant-message"; import { renderSegmentTrack } from "../../modes/components/segment-track"; import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress"; import { expandEmoticons } from "../../modes/emoji-autocomplete"; @@ -1039,18 +1040,19 @@ export class InputController { toggleThinkingBlockVisibility(): void { this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock; - settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock); + this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock); this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock; - // Rebuild chat from session messages - this.ctx.chatContainer.clear(); - this.ctx.rebuildChatFromMessages(); + for (const child of this.ctx.chatContainer.children) { + if (child instanceof AssistantMessageComponent) { + child.setHideThinkingBlock(this.ctx.hideThinkingBlock); + child.invalidate(); + } + } - // If streaming, re-add the streaming component with updated visibility and re-render if (this.ctx.streamingComponent && this.ctx.streamingMessage) { this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock); this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage); - this.ctx.chatContainer.addChild(this.ctx.streamingComponent); } this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`); diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index ac0ff3a2d..9c6c1822b 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -316,10 +316,9 @@ export class SelectorController { for (const child of this.ctx.chatContainer.children) { if (child instanceof AssistantMessageComponent) { child.setHideThinkingBlock(value as boolean); + child.invalidate(); } } - this.ctx.chatContainer.clear(); - this.ctx.rebuildChatFromMessages(); break; case "theme": { setTheme(value as string, true).then(result => { diff --git a/packages/coding-agent/test/input-controller-thinking-visibility.test.ts b/packages/coding-agent/test/input-controller-thinking-visibility.test.ts new file mode 100644 index 000000000..f5bf6c6a5 --- /dev/null +++ b/packages/coding-agent/test/input-controller-thinking-visibility.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "bun:test"; +import { AssistantMessageComponent } from "@oh-my-pi/pi-coding-agent/modes/components/assistant-message"; +import { InputController } from "@oh-my-pi/pi-coding-agent/modes/controllers/input-controller"; +import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; + +describe("InputController thinking visibility", () => { + it("keeps pre-stream pending transcript content mounted when Ctrl+T toggles thinking blocks", () => { + const pendingUserMessage = { kind: "pending-user" }; + const loadingIndicator = { kind: "loading" }; + const assistant = new AssistantMessageComponent(); + const setHideThinkingBlock = vi.spyOn(assistant, "setHideThinkingBlock"); + const invalidate = vi.spyOn(assistant, "invalidate"); + const clear = vi.fn(); + const addChild = vi.fn(); + const rebuildChatFromMessages = vi.fn(); + const set = vi.fn(); + const showStatus = vi.fn(); + const children = [pendingUserMessage, assistant, loadingIndicator]; + const chatContainer = { children, clear, addChild }; + const ctx = { + hideThinkingBlock: false, + settings: { set }, + session: { agent: { hideThinkingSummary: false } }, + chatContainer, + streamingComponent: undefined, + streamingMessage: undefined, + rebuildChatFromMessages, + showStatus, + } as unknown as InteractiveModeContext; + + new InputController(ctx).toggleThinkingBlockVisibility(); + + expect(ctx.hideThinkingBlock).toBe(true); + expect(set).toHaveBeenCalledWith("hideThinkingBlock", true); + expect(ctx.session.agent.hideThinkingSummary).toBe(true); + expect(chatContainer.children).toEqual([pendingUserMessage, assistant, loadingIndicator]); + expect(clear).not.toHaveBeenCalled(); + expect(addChild).not.toHaveBeenCalled(); + expect(rebuildChatFromMessages).not.toHaveBeenCalled(); + expect(setHideThinkingBlock).toHaveBeenCalledWith(true); + expect(invalidate).toHaveBeenCalledTimes(1); + expect(showStatus).toHaveBeenCalledWith("Thinking blocks: hidden"); + }); +});