Merge pull request #1317 from can1357/farm/bfe680ee/ctx-ui-notify-during-session-start-is-cl

fix(coding-agent): preserve startup extension notifications
This commit is contained in:
Can Bölük
2026-05-25 13:20:12 +03:00
committed by GitHub
6 changed files with 66 additions and 8 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -281,7 +281,7 @@ async function runInteractiveMode(
})
.catch(() => {});
mode.renderInitialMessages();
mode.renderInitialMessages(undefined, { preserveExistingChat: true });
for (const notify of notifs) {
if (!notify) {
@@ -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 {
+1 -1
View File
@@ -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;
@@ -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 {
@@ -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(),