diff --git a/lib/tui/session.mjs b/lib/tui/session.mjs index 026e8c3..908fff4 100644 --- a/lib/tui/session.mjs +++ b/lib/tui/session.mjs @@ -182,8 +182,13 @@ function tuiCapturePane(tmux, tmuxName) { } // True once claude's input bar is rendered and ready for keystrokes. +// Two known ready-state footers across claude versions: the classic `? for shortcuts` +// hint, and the `shift+tab to cycle` hint (part of "⏵⏵ bypass permissions on (shift+tab +// to cycle)") that newer claude 2.1.x renders in its place. Match EITHER — a matcher that +// only knew the classic string silently reported "never ready" on those builds, timing out +// every boot (tui_pane_not_ready) and, with the warm pool on, failing every pre-boot. function tuiInputReady(pane) { - return /\? for shortcuts/.test(pane); + return /\? for shortcuts|shift\+tab to cycle/.test(pane); } // True once the pasted prompt has POSITIVELY landed in the input box. We only trust @@ -615,8 +620,9 @@ export async function bootTuiPane({ // A pooled pane is SINGLE-USE: it already carries its own fresh --session-id, it // serves this one turn, and it is killed in the finally like any other pane. // 2. On a MISS: pre-trust the scratch cwd, boot an interactive `claude` in a fresh tmux -// session in the scratch cwd, poll capture-pane until the `? for shortcuts` input bar -// appears (bootTuiPane). BOOT_MS is the max wait, not a fixed delay. +// session in the scratch cwd, poll capture-pane until the input bar appears — see +// tuiInputReady for the ready-state footers matched (bootTuiPane). BOOT_MS is the max +// wait, not a fixed delay. // 3. Write prompt to a 0600 temp file (no shell injection from prompt content). // 4. Paste the prompt via tmux load-buffer + paste-buffer -p (bracketed paste) — // reliable for large multi-line prompts where send-keys -l is not (issue #130). diff --git a/test-features.mjs b/test-features.mjs index 065dfeb..51e4474 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -3504,7 +3504,7 @@ if (process.env.OCP_TUI_LIVE === "1") { // Replicates tuiInputReady, tuiPromptLanded verbatim from lib/tui/session.mjs. // Keep in sync with the definitions there. function _tuiInputReady(pane) { - return /\? for shortcuts/.test(pane); + return /\? for shortcuts|shift\+tab to cycle/.test(pane); } function _tuiPromptLanded(pane, prompt) { const flatPane = pane.replace(/\s+/g, " "); @@ -3522,7 +3522,12 @@ const TUI_READY_PANE = `❯ Try "how does work?" const TUI_LANDED_PANE = `❯ Reply with exactly: PONG_TEST ? for shortcuts · ← for agents`; -// Welcome splash shown before input bar is rendered — no `? for shortcuts`. +// Newer claude 2.1.x renders the input bar with a `shift+tab to cycle` footer instead of +// `? for shortcuts` — the matcher must accept it too, or the pane reads as never-ready. +const TUI_READY_PANE_SHIFT_TAB = `❯ Try "how does work?" + ⏵⏵ bypass permissions on (shift+tab to cycle)`; + +// Welcome splash shown before input bar is rendered — neither ready-state footer. const TUI_BOOT_PANE = `╭─ Claude Code v2.1.114 ─ Welcome back Tao! ─╮\n│ Tips for getting started │`; console.log("\nTUI readiness + paste-verify predicates (issue #130):"); @@ -3533,6 +3538,9 @@ test("tuiInputReady(READY_PANE) === true (input bar rendered)", () => { test("tuiInputReady(LANDED_PANE) === true (input bar still present after paste)", () => { assert.equal(_tuiInputReady(TUI_LANDED_PANE), true); }); +test("tuiInputReady(READY_PANE_SHIFT_TAB) === true (newer claude `shift+tab to cycle` footer)", () => { + assert.equal(_tuiInputReady(TUI_READY_PANE_SHIFT_TAB), true); +}); test("tuiInputReady(BOOT_PANE) === false (welcome splash, no input bar yet)", () => { assert.equal(_tuiInputReady(TUI_BOOT_PANE), false); });