From a916ae512e38bff9d0f34f7e2b79fbf79f1b4916 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 23 May 2026 19:59:41 +0000 Subject: [PATCH] fix(coding-agent): preserved startup extension notifications Keep chat notifications emitted during session_start visible after the initial transcript render rebuilds the chat container. Add regression coverage for preserving startup notifications during initial render. Fixes #1316 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/main.ts | 2 +- .../src/modes/interactive-mode.ts | 4 +- packages/coding-agent/src/modes/types.ts | 2 +- .../src/modes/utils/ui-helpers.ts | 12 ++++- .../test/interactive-mode-status.test.ts | 53 +++++++++++++++++-- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2b6d919a1..af8746bec 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - 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)). ## [15.2.4] - 2026-05-22 ### Breaking Changes 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 4805f7444..e8596a8a4 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(),