diff --git a/packages/daemon/src/domain/session-transport.ts b/packages/daemon/src/domain/session-transport.ts index a38c9fbeb..984e03f07 100644 --- a/packages/daemon/src/domain/session-transport.ts +++ b/packages/daemon/src/domain/session-transport.ts @@ -256,8 +256,9 @@ const CLAUDE_STATUS_WARNINGS = [ /^Update available! Run: \S.*$/, // The context hint, as Claude prints it; reported as the last line below the mode bar on one seat. /^new task\? \/clear to save \d+(?:\.\d+)?k tokens$/, - // After Claude installs an update: "/restart to apply" (a Claude seat, 2026-10-30) or "Restart to apply". - /^✔ Update installed · (?:\/restart|Restart) to apply$/, + // After Claude installs an update: "/restart to apply" (a Claude seat, 2026-10-30), "Restart to apply", or + // "Restart to update" on its own row (Claude 2.1.319, #2605). + /^✔ Update installed · (?:\/restart to apply|Restart to (?:apply|update))$/, ]; // Claude can draw noninteractive UI rows after its mode footer. They do not // replace the composer and should only be skipped when they match a known row. diff --git a/packages/daemon/test/claude-warning-footer-readiness.test.ts b/packages/daemon/test/claude-warning-footer-readiness.test.ts index 93068ccfd..5abf9560a 100644 --- a/packages/daemon/test/claude-warning-footer-readiness.test.ts +++ b/packages/daemon/test/claude-warning-footer-readiness.test.ts @@ -214,8 +214,10 @@ const clearHint = "new task? /clear to save 324.1k tokens"; // (2026-10-30) and "Restart to apply" in startup-submit-confirmation's screen. const installedSlash = "✔ Update installed · /restart to apply"; const installedWord = "✔ Update installed · Restart to apply"; +// Claude 2.1.319's wording, on its own row under the footer in a 49-column pane (#2605). +const installedUpdate = "✔ Update installed · Restart to update"; const warningSets = [[update], [focus], [weekly], [update, focus], [updateBanner], [clearHint], [update, updateBanner], - [installedSlash], [installedWord], [update, installedSlash]]; + [installedSlash], [installedWord], [installedUpdate], [update, installedSlash]]; // Native, from a Claude seat running a hook mid-turn (2026-10-05): the timer follows a hook label. const hookRow = "✽ Sketching… (running PostToolUse hook · 3m 12s · ↓ 7.9k tokens)"; function modePane(hint: string, trailers: string[], composer = "❯\u00a0", before = "● Ready.\n\n✻ Crunched for 2s") { @@ -286,7 +288,7 @@ describe("#808: Claude permission-mode footers below status warnings", () => { // The installed-update footer last, as a Claude seat showed it (2026-10-30): an empty prompt under it read unknown, // so managed compaction never saw the idle seat. Only the exact line is a status line; a near miss stays unknown. - it.each([installedSlash, installedWord])("reads an empty prompt under the installed-update footer as idle: %s", footer => { + it.each([installedSlash, installedWord, installedUpdate])("reads an empty prompt under the installed-update footer as idle: %s", footer => { const content = modePane("⏵⏵ auto mode on (shift+tab to cycle)", [footer]); for (const options of [{ runtime: "claude-code" }, {}]) { expect(classifyPaneActivity(content, options)).toMatchObject({ state: "agent_idle", reason: "idle_prompt", evidence: "❯" }); diff --git a/packages/daemon/test/composer-input.test.ts b/packages/daemon/test/composer-input.test.ts index 2539da5d7..90c80cc31 100644 --- a/packages/daemon/test/composer-input.test.ts +++ b/packages/daemon/test/composer-input.test.ts @@ -53,6 +53,21 @@ describe("cursor-bound composer input", () => { expect(inspectComposerInput(frame, "codex").state).toBe("unknown"); }); + // #2605: Claude 2.1.319 prints "✔ Update installed · Restart to update" on its own row under the footer in a narrow + // pane. The draft-aware guard read every idle prompt there as unknown and held each wake as draft_input_unknown. + it("reads the input under Claude's own-row Restart to update banner, and nothing like it (#2605)", () => { + const banner = (body: string, row = "✔ Update installed · Restart to update") => { + const frame = claude(body); + frame.screen = frame.screen.replace("? for shortcuts\n", `? for shortcuts\n${row}\n`); + return frame; + }; + expect(inspectComposerInput(banner(""), "claude-code").state).toBe("empty"); + expect(inspectComposerInput(banner("a real draft"), "claude-code").state).toBe("text"); + for (const nearMiss of ["✔ Update installed · Restart to update now", "Update installed · Restart to update", "✔ Update downloaded · Restart to update"]) { + expect(inspectComposerInput(banner("", nearMiss), "claude-code")).toMatchObject({ state: "unknown", unknownReason: "unrecognized_composer" }); + } + }); + it("keeps nested prompt examples inside the current draft, including when its head is clipped", () => { const body = "intro\n────────────────────\n❯ \n────────────────────\n? for shortcuts\n────────────────────\n❯ "; const snapshot = claude(body, { x: 4, y: 8 });