diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a75ddeb2c..ac506f5b0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -169,6 +169,8 @@ - Fixed the DuckDuckGo web_search provider returning empty results for non-encyclopedic queries by switching from the Instant Answer API to parsing the HTML frontend, and added clear error handling for bot-challenge throttling. - Fixed Windows --extension paths with spaces or \\?\ prefixes being truncated or incorrectly passed to Bun import/spawn APIs. - Fixed /mcp reauth compatibility with Cloudflare by aligning OAuth prompt behavior with the reference MCP SDK and updating the client label to oh-my-pi. +- Fixed the `edit` tool persisting unbounded full-file `oldText` / `newText` snapshots in tool-result `details`, inflating per-turn session JSONL lines (hundreds of KB per edit on large files). `details.oldText`/`details.newText` are now pruned when their combined length exceeds 32 KB; the visible diff, path, line, and diagnostic metadata are preserved, and ACP `diff` content still flows for smaller edits. ([#3786](https://github.com/can1357/oh-my-pi/issues/3786)) +- Fixed hidden-thinking live status rows rendering as glyph-only lines by appending a persistent `Thinking` label next to the pulse. ## [16.2.5] - 2026-06-28 diff --git a/packages/coding-agent/src/modes/components/assistant-message.ts b/packages/coding-agent/src/modes/components/assistant-message.ts index 5db8282f6..1db10fc2d 100644 --- a/packages/coding-agent/src/modes/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/components/assistant-message.ts @@ -329,17 +329,18 @@ export class AssistantMessageComponent extends Container { #thinkingDotsLabel(): string { const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…"; const coloredGlyph = theme.fg("thinkingText", glyph); + const thinkingLabel = theme.fg("muted", " Thinking"); const rate = Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed()); // The numeric badge (" · toks/s") only renders while this block // is genuinely streaming provider tokens. A block that has observed no token // delta (e.g. a provider that reports usage only at turn end) or whose rate - // has decayed to zero (a streaming lull) drops it entirely — the bare pulse - // keeps signalling that the model is thinking. The liveness flag also stops - // the session-wide gauge from leaking a previous turn's rate onto a fresh - // token-less block. - if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph; + // has decayed to zero (a streaming lull) drops it entirely — the persistent + // text label keeps the pulse descriptive for terminals and screen readers. + // The liveness flag also stops the session-wide gauge from leaking a previous + // turn's rate onto a fresh token-less block. + if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph + thinkingLabel; // Total provider tokens, dimmed, sit next to the pulse. - const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` ${formatNumber(this.#thinkingTokens)}`) : ""; + const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` · ${formatNumber(this.#thinkingTokens)}`) : ""; // Speed badge color: dim gray at rest, brightening toward the theme accent as // streaming speed climbs (gray → bright accent). Ease (sqrt) so typical // mid-stream rates already read as clearly accent-tinted instead of staying @@ -348,7 +349,7 @@ export class AssistantMessageComponent extends Container { const hex = lerpHex(theme.getColorHex("dim"), theme.getAccentColorHex(), ratio); const rateText = ` · ${rate.toFixed(1)} toks/s`; const rateSpan = theme.getColorMode() === "truecolor" ? chalk.hex(hex)(rateText) : theme.fg("muted", rateText); - return coloredGlyph + totalSpan + rateSpan; + return coloredGlyph + thinkingLabel + totalSpan + rateSpan; } #startThinkingAnimation(): void { diff --git a/packages/coding-agent/test/modes/components/assistant-message-error.test.ts b/packages/coding-agent/test/modes/components/assistant-message-error.test.ts index 722f7470d..32820e59a 100644 --- a/packages/coding-agent/test/modes/components/assistant-message-error.test.ts +++ b/packages/coding-agent/test/modes/components/assistant-message-error.test.ts @@ -178,10 +178,13 @@ describe("AssistantMessageComponent streaming thinking pulse", () => { // First frame of the expanding/shrinking ✻ pulse; deterministic right after updateContent. const PULSE = "✻"; + const THINKING_LABEL = "Thinking"; + const THINKING_GLYPH_ONLY_LINE = /^[✻✼❉❊✺✹✸✶]\s*$/; - it("shows the pulse in place of hidden reasoning while thinking streams", () => { + it("shows a described pulse in place of hidden reasoning while thinking streams", () => { const lines = liveLines(streaming([{ type: "thinking", thinking: "private reasoning" }])); - expect(lines.some(line => line.includes(PULSE))).toBe(true); + expect(lines.some(line => line.includes(PULSE) && line.includes(THINKING_LABEL))).toBe(true); + expect(lines.map(line => line.trim()).some(line => THINKING_GLYPH_ONLY_LINE.test(line))).toBe(false); expect(lines.some(line => line.includes("private reasoning"))).toBe(false); }); @@ -261,7 +264,7 @@ describe("AssistantMessageComponent streaming thinking pulse", () => { component.updateContent(streaming([{ type: "thinking", thinking: "ab" }], 57), { transient: true }); const plain = Bun.stripANSI(component.render(RENDER_WIDTH).join("\n")); - // Layout: " · toks/s" — 57 provider tokens, 47.0 tok/s. + // Layout: " Thinking · · toks/s" — 57 provider tokens, 47.0 tok/s. expect(plain).toContain("57 · 47.0 toks/s"); nowSpy.mockRestore(); @@ -301,12 +304,14 @@ describe("AssistantMessageComponent streaming thinking pulse", () => { // Long pause: rate observations age out of the window. A same-token update // refreshes the live label, which now drops the numeric badge entirely - // rather than lingering on "0.0 toks/s" — only the bare pulse remains. + // rather than lingering on "0.0 toks/s" while retaining descriptive text. mockTime = 30_000; component.updateContent(streaming([{ type: "thinking", thinking: "ab" }], 57), { transient: true }); const plain = Bun.stripANSI(component.render(RENDER_WIDTH).join("\n")); expect(plain).not.toContain("toks/s"); + expect(plain).not.toContain("57"); expect(plain.includes(PULSE)).toBe(true); + expect(plain).toContain(THINKING_LABEL); nowSpy.mockRestore(); component.dispose(); @@ -335,6 +340,7 @@ describe("AssistantMessageComponent streaming thinking pulse", () => { expect(plain).not.toContain("toks/s"); expect(plain).not.toContain("99"); expect(plain.includes(PULSE)).toBe(true); + expect(plain).toContain(THINKING_LABEL); nowSpy.mockRestore(); b.dispose();