feat: add clickable overlay buttons for CLI prompts in Shell terminal#480
Conversation
Detect numbered selection prompts in the xterm.js terminal buffer and display clickable overlay buttons, allowing users to respond by tapping instead of typing numbers. Useful on mobile/tablet devices. Closes siteboon#427
|
Note: This has been primarily tested with Claude Code CLI prompts. It is unclear whether Codex CLI or Gemini CLI use similar numbered selection prompts. @blackmammoth — should we restrict the overlay to Claude sessions only, or keep it generic and let it work for any CLI that matches the pattern? If other CLIs don't use this format, we could gate it behind a provider check to avoid false positives. |
📝 WalkthroughWalkthroughAdds frontend detection of interactive CLI prompts in the Shell terminal, exposes an optional output-ref callback propagated through runtime/connection hooks, and renders a debounced clickable overlay with numbered options + Esc that sends keystrokes to the pty. Changes
Sequence DiagramsequenceDiagram
participant User
participant Shell as "Shell Component"
participant onOutput as "onOutputRef"
participant Timer as "Debounce Timer"
participant Buffer as "xterm Buffer"
participant Overlay as "Overlay UI"
participant Socket
Shell->>onOutput: call onOutputRef.current() (if set)
onOutput->>Timer: schedule prompt check (PROMPT_DEBOUNCE_MS)
Timer->>Buffer: read last PROMPT_BUFFER_SCAN_LINES
Buffer-->>Timer: return buffer slice
Timer->>Shell: update cliPromptOptions (if prompt found)
Shell->>Overlay: render option buttons + Esc
User->>Overlay: click option button
Overlay->>Socket: sendInput(selectedOption)
Socket-->>Shell: terminal output updates (may remove prompt)
Shell->>Overlay: clear options on prompt disappearance or disconnect
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/shell/view/Shell.tsx`:
- Around line 143-147: A scheduled prompt check via schedulePromptCheck (which
sets promptCheckTimer.current to a timeout calling checkBufferForPrompt) can
fire after disconnect and repopulate cliPromptOptions; cancel this by clearing
the pending timeout when the connection is torn down. Update the
disconnect/cleanup logic that clears cliPromptOptions to also call clearTimeout
on promptCheckTimer.current (and set promptCheckTimer.current = null) so any
pending scheduled check is cancelled; also ensure any reconnection path does not
reuse an old timer reference.
- Around line 101-114: The code currently falls back to scanning the entire
output for option anchors when footerIdx is -1, causing false positives; change
the logic so options are only searched/parsed if a footer anchor exists.
Concretely, in Shell.tsx replace the fallback block that iterates over lines
(the loop using anchorIdx, lines, and the regexes /^\s*[❯›>]\s*\d+\.\s+/ and
/^\s*\d+\.\s+/) with a guard that skips searching when footerIdx === -1 (i.e.,
only attempt to locate/compute anchorIdx when footerIdx !== -1), ensuring
anchorIdx remains -1 and options parsing is skipped when the footer is absent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4a0ed7fd-a0ed-475a-ac6d-ab377d924cf7
📒 Files selected for processing (5)
src/components/shell/constants/constants.tssrc/components/shell/hooks/useShellConnection.tssrc/components/shell/hooks/useShellRuntime.tssrc/components/shell/types/types.tssrc/components/shell/view/Shell.tsx
- Remove fallback option scanning without footer anchor to prevent false positives on regular numbered lists in conversation output - Cancel pending prompt check timer on disconnect to prevent stale options from reappearing after reconnection
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/components/shell/view/Shell.tsx (1)
79-126: Extract prompt parsing into a pure helper and unit test it.
checkBufferForPromptnow carries parsing + buffer traversal + state update in one block. A small pure parser (lines in → options out) would make edge-case testing easier (contiguity, numbering gaps, footer variants) and reduce regression risk.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/shell/view/Shell.tsx` around lines 79 - 126, The prompt-detection logic inside checkBufferForPrompt should be extracted into a pure, unit-testable function (e.g., parseCliPromptFromLines(lines: string[]): CliPromptOption[]); move the footer detection (/esc to cancel|enter to select/), option-line regex (/^\s*[❯›>]?\s*(\d+)\.\s+(.+)/), optMap building, contiguous numbering check (PROMPT_MIN_OPTIONS, PROMPT_MAX_OPTIONS) and final filtering into that helper, keep checkBufferForPrompt responsible only for reading the xterm buffer, creating the trimmed lines array and calling the parser, then applying setCliPromptOptions with the parser result; add unit tests for parseCliPromptFromLines covering contiguity, gaps, duplicate numbers, footer variants, and boundary counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/shell/view/Shell.tsx`:
- Around line 107-123: The option-parsing loop currently collects numbered lines
anywhere in the scan window which can include non-contiguous matches; modify the
loop around footerIdx/optScanStart (the block using PROMPT_OPTION_SCAN_LINES,
optMap and match) to require a contiguous block immediately above the footer by
scanning upward and stopping as soon as a non-matching line is encountered after
you have started collecting options (e.g., track a started flag: when you find
the first match set started=true and if started and a line has no match break),
then build valid (CliPromptOption[]) from optMap as before.
---
Nitpick comments:
In `@src/components/shell/view/Shell.tsx`:
- Around line 79-126: The prompt-detection logic inside checkBufferForPrompt
should be extracted into a pure, unit-testable function (e.g.,
parseCliPromptFromLines(lines: string[]): CliPromptOption[]); move the footer
detection (/esc to cancel|enter to select/), option-line regex
(/^\s*[❯›>]?\s*(\d+)\.\s+(.+)/), optMap building, contiguous numbering check
(PROMPT_MIN_OPTIONS, PROMPT_MAX_OPTIONS) and final filtering into that helper,
keep checkBufferForPrompt responsible only for reading the xterm buffer,
creating the trimmed lines array and calling the parser, then applying
setCliPromptOptions with the parser result; add unit tests for
parseCliPromptFromLines covering contiguity, gaps, duplicate numbers, footer
variants, and boundary counts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cd37bacc-613c-4d0a-9157-f78099b4ca45
📒 Files selected for processing (1)
src/components/shell/view/Shell.tsx
| const optScanStart = Math.max(0, footerIdx - PROMPT_OPTION_SCAN_LINES); | ||
| for (let i = footerIdx - 1; i >= optScanStart; i--) { | ||
| const match = lines[i].match(/^\s*[❯›>]?\s*(\d+)\.\s+(.+)/); | ||
| if (match) { | ||
| const num = match[1]; | ||
| const label = match[2].trim(); | ||
| if (parseInt(num, 10) <= PROMPT_MAX_OPTIONS && label.length > 0 && !optMap.has(num)) { | ||
| optMap.set(num, label); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const valid: CliPromptOption[] = []; | ||
| for (let i = 1; i <= optMap.size; i++) { | ||
| if (optMap.has(String(i))) valid.push({ number: String(i), label: optMap.get(String(i))! }); | ||
| else break; | ||
| } |
There was a problem hiding this comment.
Require a contiguous option block immediately above the footer.
The current loop gathers any numbered matches in the scan window, even if separated by non-option lines. That can still misclassify regular output when a footer-like line appears nearby.
Proposed fix
- const optMap = new Map<string, string>();
- const optScanStart = Math.max(0, footerIdx - PROMPT_OPTION_SCAN_LINES);
- for (let i = footerIdx - 1; i >= optScanStart; i--) {
- const match = lines[i].match(/^\s*[❯›>]?\s*(\d+)\.\s+(.+)/);
- if (match) {
- const num = match[1];
- const label = match[2].trim();
- if (parseInt(num, 10) <= PROMPT_MAX_OPTIONS && label.length > 0 && !optMap.has(num)) {
- optMap.set(num, label);
- }
- }
- }
-
- const valid: CliPromptOption[] = [];
- for (let i = 1; i <= optMap.size; i++) {
- if (optMap.has(String(i))) valid.push({ number: String(i), label: optMap.get(String(i))! });
- else break;
- }
+ const optScanStart = Math.max(0, footerIdx - PROMPT_OPTION_SCAN_LINES);
+ const parsed: CliPromptOption[] = [];
+ for (let i = footerIdx - 1; i >= optScanStart; i--) {
+ const match = lines[i].match(/^\s*[❯›>]?\s*(\d+)\.\s+(.+)/);
+ if (!match) {
+ if (parsed.length > 0) break; // stop once prompt block ends
+ continue; // allow padding lines directly above footer
+ }
+ const number = match[1];
+ const label = match[2].trim();
+ if (parseInt(number, 10) > PROMPT_MAX_OPTIONS || label.length === 0) break;
+ parsed.push({ number, label });
+ }
+ parsed.reverse();
+
+ const valid: CliPromptOption[] = [];
+ for (let i = 0; i < parsed.length; i++) {
+ if (parsed[i].number !== String(i + 1)) break;
+ valid.push(parsed[i]);
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/shell/view/Shell.tsx` around lines 107 - 123, The
option-parsing loop currently collects numbered lines anywhere in the scan
window which can include non-contiguous matches; modify the loop around
footerIdx/optScanStart (the block using PROMPT_OPTION_SCAN_LINES, optMap and
match) to require a contiguous block immediately above the footer by scanning
upward and stopping as soon as a non-matching line is encountered after you have
started collecting options (e.g., track a started flag: when you find the first
match set started=true and if started and a line has no match break), then build
valid (CliPromptOption[]) from optMap as before.
There was a problem hiding this comment.
Intentionally kept non-contiguous. CLI prompts can wrap option labels across multiple terminal rows (long text, narrow viewport), producing non-matching lines between numbered entries. Requiring strict contiguity would break detection in those cases. Added a comment in the code explaining this design decision.
There was a problem hiding this comment.
@PaloSP, you're absolutely right — the second PR description image actually illustrates this perfectly: each option like 1. Dark is immediately followed by a subtitle line (Dark background with light text) before the next numbered entry appears. Strict contiguity would break detection there. The non-contiguous approach is the correct design, and a code comment documenting the rationale is a good addition. Withdrawing my earlier suggestion.
✏️ Learnings added
Learnt from: PaloSP
Repo: siteboon/claudecodeui PR: 480
File: src/components/shell/view/Shell.tsx:110-126
Timestamp: 2026-03-04T12:41:33.432Z
Learning: In `src/components/shell/view/Shell.tsx`, the CLI prompt option scanner intentionally allows non-contiguous lines between numbered options (e.g., `1. Dark` followed by a description line before `2. Light`). Requiring strict contiguity would break detection when prompts render multi-line option labels (number + subtitle row). This is by design and should not be flagged as a bug.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Stop collecting numbered options as soon as a non-matching line is encountered, preventing false matches from non-contiguous numbered text above the prompt. Addresses CodeRabbit review feedback on PR siteboon#480.
CLI prompts may wrap options across multiple terminal rows or include blank separators. Revert contiguous-block requirement and document why non-matching lines are tolerated during upward scan.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/shell/view/Shell.tsx (1)
42-43:⚠️ Potential issue | 🟠 MajorPreserve legacy
isActivebehavior whenautoConnectis omitted.Line 42 defaults
autoConnecttofalsewhile Line 52 ignoresisActive. Existing callers that still pass onlyisActive(e.g.,src/components/TaskList.jsxLines 455-471) will silently stop auto-connecting.Suggested compatibility fix
export default function Shell({ selectedProject = null, selectedSession = null, initialCommand = null, isPlainShell = false, onProcessComplete = null, minimal = false, - autoConnect = false, + autoConnect, isActive, }: ShellProps) { @@ - // Keep the public API stable for existing callers that still pass `isActive`. - void isActive; + // Backward compatibility: if `autoConnect` is not provided, fall back to legacy `isActive`. + const shouldAutoConnect = autoConnect ?? Boolean(isActive); @@ } = useShellRuntime({ @@ - autoConnect, + autoConnect: shouldAutoConnect,Also applies to: 51-53, 67-74
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/shell/view/Shell.tsx` around lines 42 - 43, The prop defaulting breaks legacy callers because autoConnect is defaulted to false and isActive is ignored; update the Shell component prop handling so that when autoConnect is undefined it falls back to isActive (e.g., treat autoConnect = autoConnect !== undefined ? autoConnect : isActive), ensuring autoConnect and isActive are respected (update any occurrences around the autoConnect/isActive handling and related conditionals in Shell.tsx and any helper functions that read autoConnect).
🧹 Nitpick comments (1)
src/components/shell/view/Shell.tsx (1)
142-147: Tighten unmount cleanup by nulling callback/timer refs after clearing.This makes teardown deterministic and avoids retaining stale callback references longer than needed.
Suggested cleanup refinement
useEffect(() => { return () => { - if (promptCheckTimer.current) clearTimeout(promptCheckTimer.current); + if (promptCheckTimer.current) { + clearTimeout(promptCheckTimer.current); + promptCheckTimer.current = null; + } + onOutputRef.current = null; }; }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/shell/view/Shell.tsx` around lines 142 - 147, The useEffect unmount cleanup currently clears promptCheckTimer.current but leaves the ref populated; update the cleanup to not only clearTimeout(promptCheckTimer.current) but also set promptCheckTimer.current = null (or undefined) afterwards so the timer ref and any held callback references are released; modify the cleanup returned by the useEffect that references promptCheckTimer to clear the timeout and then null the ref to ensure deterministic teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/components/shell/view/Shell.tsx`:
- Around line 42-43: The prop defaulting breaks legacy callers because
autoConnect is defaulted to false and isActive is ignored; update the Shell
component prop handling so that when autoConnect is undefined it falls back to
isActive (e.g., treat autoConnect = autoConnect !== undefined ? autoConnect :
isActive), ensuring autoConnect and isActive are respected (update any
occurrences around the autoConnect/isActive handling and related conditionals in
Shell.tsx and any helper functions that read autoConnect).
---
Nitpick comments:
In `@src/components/shell/view/Shell.tsx`:
- Around line 142-147: The useEffect unmount cleanup currently clears
promptCheckTimer.current but leaves the ref populated; update the cleanup to not
only clearTimeout(promptCheckTimer.current) but also set
promptCheckTimer.current = null (or undefined) afterwards so the timer ref and
any held callback references are released; modify the cleanup returned by the
useEffect that references promptCheckTimer to clear the timeout and then null
the ref to ensure deterministic teardown.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 59cbc2ff-3d86-45de-82bc-d464faca0c0a
📒 Files selected for processing (1)
src/components/shell/view/Shell.tsx
I don't think Gemini and Codex use the same format but I think we can keep the code as it is since they may use a similar format in the future. |
…siteboon#480) * feat: add clickable overlay buttons for CLI prompt selection Detect numbered selection prompts in the xterm.js terminal buffer and display clickable overlay buttons, allowing users to respond by tapping instead of typing numbers. Useful on mobile/tablet devices. Closes siteboon#427 * fix: address CodeRabbit review feedback - Remove fallback option scanning without footer anchor to prevent false positives on regular numbered lists in conversation output - Cancel pending prompt check timer on disconnect to prevent stale options from reappearing after reconnection * fix: require contiguous option block above footer anchor Stop collecting numbered options as soon as a non-matching line is encountered, preventing false matches from non-contiguous numbered text above the prompt. Addresses CodeRabbit review feedback on PR siteboon#480. * revert: allow non-contiguous option lines for multi-line labels CLI prompts may wrap options across multiple terminal rows or include blank separators. Revert contiguous-block requirement and document why non-matching lines are tolerated during upward scan.
Summary
Closes #427
How it works
❯ 1. Option) and a footer anchor (Esc to cancel/Enter to select)Test plan
Summary by CodeRabbit