Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-resume-missing-session-footer-leak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix footer leaking onto the terminal when resuming a non-existent session.
15 changes: 12 additions & 3 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ export class KimiTUI {
private async initMainTui(): Promise<boolean> {
const shouldReplayHistory = await this.init();

// Mount only after init() succeeds; see mountFooter().
this.mountFooter();
this.renderWelcome();
this.setupAutocomplete();
void this.loadPersistedInputHistory();
Expand Down Expand Up @@ -601,11 +603,18 @@ export class KimiTUI {
ui.addChild(this.state.todoPanelContainer);
ui.addChild(this.state.queueContainer);
ui.addChild(this.state.editorContainer);
// FooterComponent isn't a Container; wrap it so it picks up the same
// outer gutter as the transcript/panels above.
// Footer is mounted later (mountFooter), not here.
}

// Footer is the only chrome with content before a session is ready, so
// mounting it at construction lets a stray pre-start render leak it to the
// terminal — e.g. above the error when resuming a missing session. Mount it
// only once init() succeeds. FooterComponent isn't a Container, so wrap it to
// pick up the same outer gutter as the panels above.
private mountFooter(): void {
const footerWrap = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER);
footerWrap.addChild(this.state.footer);
ui.addChild(footerWrap);
this.state.ui.addChild(footerWrap);
}

// =========================================================================
Expand Down
47 changes: 47 additions & 0 deletions apps/kimi-code/test/tui/kimi-tui-startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,4 +776,51 @@ describe("KimiTUI startup", () => {

await expect(driver.init()).rejects.toThrow("provider config is invalid");
});

it("does not mount the footer when resuming a missing session fails", async () => {
// Regression: a stray pre-startEventLoop render used to paint the footer
// (cwd/git + "context:" statusline) to the terminal before the fatal
// error, leaving it stranded above the error message. The footer must not
// be in the layout tree when initMainTui() throws.
const harness = makeHarness(makeSession(), {
listSessions: vi.fn(async () => []),
});
const driver = makeDriver(
harness,
makeStartupInput({ session: "missing-session" }),
) as unknown as MigrateExitDriver;

await expect(driver.initMainTui()).rejects.toThrow(
'Session "missing-session" not found.',
);
expect(uiContainsFooter(driver)).toBe(false);
});

it("mounts the footer once startup reaches the main TUI", async () => {
const session = makeSession({ id: "ses-target" });
const harness = makeHarness(session, {
listSessions: vi.fn(async () => [{ id: "ses-target", workDir: "/tmp/proj-a" }]),
});
const driver = makeDriver(
harness,
makeStartupInput({ session: "ses-target" }),
) as unknown as MigrateExitDriver;

// Not mounted until init() succeeds.
expect(uiContainsFooter(driver)).toBe(false);

await driver.initMainTui();

expect(uiContainsFooter(driver)).toBe(true);
});
});

function uiContainsFooter(driver: StartupDriver): boolean {
const target: unknown = driver.state.footer;
const visit = (node: unknown): boolean => {
if (node === target) return true;
const children = (node as { children?: unknown[] }).children;
return Array.isArray(children) && children.some(visit);
};
return visit(driver.state.ui);
}
Loading