fix(transport): read Claude's own-row "Restart to update" banner as a status line (#2662)

CLAUDE_STATUS_WARNINGS, the exact rows that may sit under Claude's mode footer, gains one alternative: "✔ Update installed · Restart to update", the wording Claude 2.1.319 prints on its own row in a narrow pane. The pattern stays anchored at both ends and still matches "/restart to apply" and "Restart to apply" exactly, so the footer search reaches the mode footer and the draft-aware reader, activity classification and send readiness read the prompt as before; a near miss still reads unknown. The versioned banner and the /rc row were already handled. Tests add the row to the warning-footer readiness matrix and an empty, typed and near-miss composer case.
This commit is contained in:
NIKHIL authored and GitHub committed 2026-11-07 04:40:13 -08:00
1 parent 6fa0ad5c6e
commit c26af88dc7
3 files changed
+22 -4

No files matched your search

@@ -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.
@@ -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: "❯" });
@@ -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 });