diff --git a/packages/coding-agent/src/eval/__tests__/llm-bridge.test.ts b/packages/coding-agent/src/eval/__tests__/llm-bridge.test.ts index 7ae317358..bcd3fc444 100644 --- a/packages/coding-agent/src/eval/__tests__/llm-bridge.test.ts +++ b/packages/coding-agent/src/eval/__tests__/llm-bridge.test.ts @@ -57,6 +57,7 @@ function makeSession(opts: SessionOptions = {}): ToolSession { const modelRegistry = { getAvailable: () => opts.available ?? [SMOL, DEFAULT, SLOW], getApiKey: async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey), + resolver: vi.fn(() => async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey)), } as unknown as ModelRegistry; return { settings, diff --git a/packages/coding-agent/test/input-controller-escape.test.ts b/packages/coding-agent/test/input-controller-escape.test.ts index 001ff60ff..b04750571 100644 --- a/packages/coding-agent/test/input-controller-escape.test.ts +++ b/packages/coding-agent/test/input-controller-escape.test.ts @@ -113,7 +113,7 @@ function createContext(): { ctx = { editor: editor as unknown as InteractiveModeContext["editor"], - ui: { requestRender } as unknown as InteractiveModeContext["ui"], + ui: { requestRender, addInputListener: vi.fn(), addStartListener: vi.fn() } as unknown as InteractiveModeContext["ui"], loadingAnimation: undefined, autoCompactionLoader: undefined, retryLoader: undefined, diff --git a/packages/coding-agent/test/issue-846-repro.test.ts b/packages/coding-agent/test/issue-846-repro.test.ts index 13b3e1362..a1762a8d5 100644 --- a/packages/coding-agent/test/issue-846-repro.test.ts +++ b/packages/coding-agent/test/issue-846-repro.test.ts @@ -37,6 +37,7 @@ interface ModelRegistryLike { find: (...args: unknown[]) => Model; getAll: () => Model[]; getApiKey: (...args: unknown[]) => Promise; + resolver: (...args: unknown[]) => () => Promise; } const createdDirs = new Set(); @@ -62,6 +63,7 @@ function createModelRegistry(model: Model): ModelRegistryLike { find: vi.fn(() => model), getAll: vi.fn(() => [model]), getApiKey: vi.fn(async () => "test-api-key"), + resolver: vi.fn(() => async () => "test-api-key"), }; } diff --git a/packages/coding-agent/test/memories-runtime.test.ts b/packages/coding-agent/test/memories-runtime.test.ts index 394daebbb..ba2429d73 100644 --- a/packages/coding-agent/test/memories-runtime.test.ts +++ b/packages/coding-agent/test/memories-runtime.test.ts @@ -46,6 +46,7 @@ function createModelRegistry(model: Model): any { find: vi.fn(() => model), getAll: vi.fn(() => [model]), getApiKey: vi.fn(async () => "test-api-key"), + resolver: vi.fn(() => async () => "test-api-key"), }; } diff --git a/packages/coding-agent/test/role-thinking-helper-propagation.test.ts b/packages/coding-agent/test/role-thinking-helper-propagation.test.ts index 37e4b7b54..f094340bc 100644 --- a/packages/coding-agent/test/role-thinking-helper-propagation.test.ts +++ b/packages/coding-agent/test/role-thinking-helper-propagation.test.ts @@ -43,6 +43,7 @@ describe("role thinking helper propagation", () => { const registry = { getAvailable: () => [model], getApiKey: async () => "test-key", + resolver: vi.fn(() => async () => "test-key"), }; const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "end_turn", @@ -66,6 +67,7 @@ describe("role thinking helper propagation", () => { const registry = { getAvailable: () => [model], getApiKey: async () => "test-key", + resolver: vi.fn(() => async () => "test-key"), }; const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "end_turn", diff --git a/packages/coding-agent/test/tiny-title-generator.test.ts b/packages/coding-agent/test/tiny-title-generator.test.ts index c744ca940..c6ce8c636 100644 --- a/packages/coding-agent/test/tiny-title-generator.test.ts +++ b/packages/coding-agent/test/tiny-title-generator.test.ts @@ -48,6 +48,7 @@ function createRegistry(model: Model) { return { getAvailable: () => [model], getApiKey: async () => "test-key", + resolver: vi.fn(() => async () => "test-key"), } as never; } diff --git a/packages/coding-agent/test/web/search/abort-and-timeout.test.ts b/packages/coding-agent/test/web/search/abort-and-timeout.test.ts index 9a347d196..2e82e4d27 100644 --- a/packages/coding-agent/test/web/search/abort-and-timeout.test.ts +++ b/packages/coding-agent/test/web/search/abort-and-timeout.test.ts @@ -123,6 +123,7 @@ describe("Anthropic provider hard-timeout wiring", () => { systemPrompt: "", authStorage: { getApiKey: async () => "sk-fallback", + resolver: vi.fn(() => async () => "sk-fallback"), } as unknown as AuthStorage, }); diff --git a/packages/coding-agent/test/web/search/tavily.test.ts b/packages/coding-agent/test/web/search/tavily.test.ts index 689a0a495..8eb7d2e7a 100644 --- a/packages/coding-agent/test/web/search/tavily.test.ts +++ b/packages/coding-agent/test/web/search/tavily.test.ts @@ -57,6 +57,7 @@ describe("Tavily searchTavily request shape (integration)", () => { async getApiKey() { return process.env.TAVILY_API_KEY ?? undefined; }, + resolver: vi.fn(() => async () => process.env.TAVILY_API_KEY ?? undefined), hasAuth() { return Boolean(process.env.TAVILY_API_KEY); }, diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index a2184580b..a2c9834c5 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -421,7 +421,7 @@ export class Container implements Component { */ type RenderIntent = | { kind: "noop" } - | { kind: "initial" } + | { kind: "initial"; clearScrollback: boolean } | { kind: "sessionReplace" } | { kind: "historyRebuild" } | { kind: "overlayRebuild" } @@ -1750,6 +1750,7 @@ export class TUI extends Container { if ( this.#eagerNativeScrollbackRebuild && eagerEraseScrollbackRisk && + !intent.clearScrollback && !allowUnknownViewportMutation && liveRegionStart !== undefined && liveRegionStart < lines.length && @@ -1765,14 +1766,12 @@ export class TUI extends Container { this.#nativeScrollbackCommitSafeEnd, ); } else { - // Start from a clean terminal: clear native scrollback too (off a - // multiplexer, where ED3 is a no-op and would only duplicate the pane - // history). Prior shell content is intentionally not preserved. this.#emitFullPaint(lines, width, height, cursorPos, { clearViewport: true, - clearScrollback: !isMultiplexerSession(), + clearScrollback: intent.clearScrollback && !isMultiplexerSession(), }); } + this.#clearScrollbackOnNextRender = false; this.#hasEverRendered = true; return; } @@ -1878,17 +1877,13 @@ export class TUI extends Container { liveRegionStart: number | undefined, commitSafeEnd: number | undefined, ): RenderIntent { - // A forced scrollback wipe can be queued before start()'s initial paint runs - // (cold `omp --resume` does this while replacing the welcome frame with the - // restored transcript). Honor it before the normal initial-preserve path so - // the first committed frame is the clean session replay, not a deferred wipe - // that waits for the user's first keystroke. - if (this.#clearScrollbackOnNextRender) return { kind: "sessionReplace" }; + // Initial paint after start(): preserve prior shell scrollback by default, + // but honor callers that are replacing terminal history before any frame is + // committed. This keeps the first visible commit clean instead of appending + // a tall transcript once and wiping on the next render. + if (!this.#hasEverRendered) return { kind: "initial", clearScrollback: this.#clearScrollbackOnNextRender }; - // Initial paint after start(): clear the viewport AND prior shell scrollback - // so the session starts from a clean terminal. (We intentionally do not - // preserve pre-omp shell history — see the `initial` emit below.) - if (!this.#hasEverRendered) return { kind: "initial" }; + if (this.#clearScrollbackOnNextRender) return { kind: "sessionReplace" }; const forceViewportRepaint = this.#forceViewportRepaintOnNextRender; const eagerEraseScrollbackRisk = this.#hasEagerEraseScrollbackRisk();