diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 13f8cd1ef..091def9fc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,7 @@ - Added `OMP_NO_WEBP` environment variable to disable WebP encoding in image resize, fixing HTTP 400 errors when attaching browser snapshots to vision models running on local llama.cpp (which uses STB library that lacks WebP support) - Fixed loop mode submitting the next prompt while a background async-job delivery turn (idle flush) was still pending, which could cause the job result to be silently dropped and make the session appear to keep firing while work was ongoing ([#1294](https://github.com/can1357/oh-my-pi/issues/1294)) - Fixed clipboard image paste (Ctrl+V) silently failing on WSL2 by routing image reads through a `powershell.exe` bridge when WSL interop is detected, since `arboard` returns `ContentNotAvailable` under WSLg ([#1280](https://github.com/can1357/oh-my-pi/issues/1280)) +- Fixed extension `ctx.ui.notify()` messages emitted during `session_start` being cleared before the first interactive render ([#1316](https://github.com/can1357/oh-my-pi/issues/1316)). - Fixed append-only context mode not being recomputed after model switches — the mode was frozen at session construction time using the initial model's provider, so `provider.appendOnlyContext=auto` left append-only enabled after switching away from DeepSeek (or disabled after switching to DeepSeek) for the rest of the session ### Fixed diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 43c33cbaa..190cb2a3c 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -281,7 +281,7 @@ async function runInteractiveMode( }) .catch(() => {}); - mode.renderInitialMessages(); + mode.renderInitialMessages(undefined, { preserveExistingChat: true }); for (const notify of notifs) { if (!notify) { diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 264fe8807..ca8ca0d99 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -2312,8 +2312,8 @@ export class InteractiveMode implements InteractiveModeContext { this.#uiHelpers.renderSessionContext(sessionContext, options); } - renderInitialMessages(prebuiltContext?: SessionContext): void { - this.#uiHelpers.renderInitialMessages(prebuiltContext); + renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void { + this.#uiHelpers.renderInitialMessages(prebuiltContext, options); } getUserMessageText(message: Message): string { diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index 9f1cd4c85..1d66a9f7a 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -186,7 +186,7 @@ export interface InteractiveModeContext { sessionContext: SessionContext, options?: { updateFooter?: boolean; populateHistory?: boolean }, ): void; - renderInitialMessages(prebuiltContext?: SessionContext): void; + renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void; getUserMessageText(message: Message): string; findLastAssistantMessage(): AssistantMessage | undefined; extractAssistantText(message: AssistantMessage): string; diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index ea2468f09..d3afe239d 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -29,6 +29,9 @@ import type { SessionContext } from "../../session/session-manager"; import { formatBytes, formatDuration } from "../../tools/render-utils"; type TextBlock = { type: "text"; text: string }; +interface RenderInitialMessagesOptions { + preserveExistingChat?: boolean; +} type QueuedMessages = { steering: string[]; @@ -459,9 +462,10 @@ export class UiHelpers { this.ctx.ui.requestRender(); } - renderInitialMessages(prebuiltContext?: SessionContext): void { + renderInitialMessages(prebuiltContext?: SessionContext, options: RenderInitialMessagesOptions = {}): void { // This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI). // Clear existing rendered chat first to avoid duplicating the full session in the container. + const preservedChatChildren = options.preserveExistingChat ? this.ctx.chatContainer.children : undefined; this.ctx.chatContainer.clear(); this.ctx.pendingMessagesContainer.clear(); this.ctx.pendingBashComponents = []; @@ -486,6 +490,12 @@ export class UiHelpers { const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`; this.ctx.showStatus(`Session compacted ${times}`); } + if (preservedChatChildren && preservedChatChildren.length > 0) { + for (const child of preservedChatChildren) { + this.ctx.chatContainer.addChild(child); + } + this.ctx.ui.requestRender(); + } } clearEditor(): void { diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 1d31c0ccc..92bfc8987 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -1,8 +1,9 @@ import { beforeAll, describe, expect, test, vi } from "bun:test"; +import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers"; -import { buildSessionContext } from "@oh-my-pi/pi-coding-agent/session/session-manager"; +import { buildSessionContext, type SessionContext } from "@oh-my-pi/pi-coding-agent/session/session-manager"; import { Container } from "@oh-my-pi/pi-tui"; function renderLastLine(container: Container, width = 120): string { @@ -11,10 +12,47 @@ function renderLastLine(container: Container, width = 120): string { return last.render(width).join("\n"); } +function renderContainer(container: Container, width = 120): string { + return container.render(width).join("\n"); +} + +function createInitialRenderHarness(): { ctx: InteractiveModeContext; helpers: UiHelpers } { + let helpers: UiHelpers; + const ctx = { + chatContainer: new Container(), + pendingMessagesContainer: new Container(), + pendingBashComponents: [], + pendingPythonComponents: [], + pendingTools: new Map(), + ui: { requestRender: vi.fn() }, + isBackgrounded: false, + sessionManager: { + buildSessionContext: () => buildSessionContext([]), + getEntries: () => [], + }, + statusLine: { invalidate: vi.fn() }, + updateEditorBorderColor: vi.fn(), + renderSessionContext: ( + context: SessionContext, + options?: { updateFooter?: boolean; populateHistory?: boolean }, + ) => helpers.renderSessionContext(context, options), + addMessageToChat: (message: AgentMessage) => helpers.addMessageToChat(message), + settings: { get: () => false }, + session: { + retryAttempt: 0, + getToolByName: () => undefined, + }, + toolOutputExpanded: false, + hideThinkingBlock: false, + } as unknown as InteractiveModeContext; + helpers = new UiHelpers(ctx); + return { ctx, helpers }; +} + describe("InteractiveMode.showStatus", () => { - beforeAll(() => { + beforeAll(async () => { // showStatus uses the global theme instance - initTheme(); + await initTheme(); }); test("coalesces immediately-sequential status messages", () => { @@ -61,6 +99,15 @@ describe("InteractiveMode.showStatus", () => { expect(renderLastLine(ctx.chatContainer)).toContain("STATUS_TWO"); }); + test("preserves startup notifications while rendering the initial transcript", () => { + const { ctx, helpers } = createInitialRenderHarness(); + + helpers.showWarning("startup notification probe"); + helpers.renderInitialMessages(undefined, { preserveExistingChat: true }); + + expect(renderContainer(ctx.chatContainer)).toContain("startup notification probe"); + }); + test("preserves optimistic user signatures when rebuilding transcript state", () => { const ctx = { chatContainer: new Container(),