Skip to content

feat: add clickable overlay buttons for CLI prompts in Shell terminal#480

Merged
blackmammoth merged 4 commits into
siteboon:mainfrom
PaloSP:feat/cli-prompt-overlay-buttons
Mar 5, 2026
Merged

feat: add clickable overlay buttons for CLI prompts in Shell terminal#480
blackmammoth merged 4 commits into
siteboon:mainfrom
PaloSP:feat/cli-prompt-overlay-buttons

Conversation

@PaloSP
Copy link
Copy Markdown
Contributor

@PaloSP PaloSP commented Mar 4, 2026

Summary

  • Detect CLI selection prompts (permission requests, plan approvals, user questions) in the xterm.js buffer after terminal output settles (~500ms debounce)
  • Display an overlay bar with clickable buttons at the bottom of the Shell terminal, so users can respond by tapping instead of typing a number
  • Include an Esc button to cancel the prompt
  • Overlay auto-clears when the prompt is gone or on disconnect

Closes #427

How it works

  • After terminal output settles, the last 20 lines of the xterm.js buffer are scanned for numbered option patterns (❯ 1. Option) and a footer anchor (Esc to cancel / Enter to select)
  • Options are extracted and displayed as buttons; clicking a button sends the corresponding keystroke to the PTY
  • Detection runs entirely on the frontend by reading the xterm.js buffer directly, avoiding unreliable server-side PTY parsing

Test plan

  • Start a Claude CLI session in the Shell terminal
  • Trigger a permission prompt (e.g. file write) — verify overlay buttons appear
  • Click an option button — verify the correct selection is sent
  • Click Esc button — verify the prompt is cancelled
  • Disconnect/reconnect — verify overlay clears on disconnect
  • Verify overlay does not appear for regular numbered lists in conversation output
image image

Summary by CodeRabbit

  • New Features
    • Automatic CLI prompt detection now shows multiple-choice prompts as an interactive option bar at the bottom of the shell.
    • Tap numbered options to send input or press Escape to dismiss the overlay.
    • New Shell props: initialCommand, minimal, autoConnect, isActive, and onProcessComplete for finer control of shell behavior.
    • Exposes an output callback ref so external code can react to each shell output.

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
@PaloSP
Copy link
Copy Markdown
Contributor Author

PaloSP commented Mar 4, 2026

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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Configuration Constants
src/components/shell/constants/constants.ts
Added five exported constants for prompt detection debounce, scan window, and option limits: PROMPT_DEBOUNCE_MS, PROMPT_BUFFER_SCAN_LINES, PROMPT_OPTION_SCAN_LINES, PROMPT_MAX_OPTIONS, PROMPT_MIN_OPTIONS.
Type Definitions
src/components/shell/types/types.ts
Added optional `onOutputRef?: MutableRefObject<(() => void)
Hook Integration
src/components/shell/hooks/useShellConnection.ts, src/components/shell/hooks/useShellRuntime.ts
Added onOutputRef to options/signatures; useShellConnection invokes onOutputRef.current() after writing output and useShellRuntime forwards the ref.
Component Implementation
src/components/shell/view/Shell.tsx
Introduced debounced scanning of the xterm buffer for CLI prompt footers and numbered options, cliPromptOptions state, prompt check timer/ref, UI overlay with numbered buttons + Esc, sendInput helper to emit socket input messages, and cleanup on disconnect/unmount.
Manifest
package.json
Minor lines changed (no dependency changes indicated).

Sequence Diagram

sequenceDiagram
    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
Loading

Possibly related PRs

Suggested reviewers

  • blackmammoth

Poem

🐰 I hop the buffer, sniff each line,
A prompt appears — the buttons shine.
Tap a number, Esc if fraught,
I send the key that users sought.
Hooray — less typing, more delight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding clickable overlay buttons for CLI prompts in the Shell terminal, matching the core changes across all modified files.
Linked Issues check ✅ Passed All coding requirements from issue #427 are met: CLI prompt detection via xterm buffer scanning, debounce mechanism, numbered option extraction, clickable overlay UI, keystroke transmission, and frontend-only parsing logic.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the CLI prompt overlay feature; no unrelated modifications detected across constants, hooks, types, and Shell component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f4615df and 40c6425.

📒 Files selected for processing (5)
  • src/components/shell/constants/constants.ts
  • src/components/shell/hooks/useShellConnection.ts
  • src/components/shell/hooks/useShellRuntime.ts
  • src/components/shell/types/types.ts
  • src/components/shell/view/Shell.tsx

Comment thread src/components/shell/view/Shell.tsx Outdated
Comment thread src/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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

checkBufferForPrompt now 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

📥 Commits

Reviewing files that changed from the base of the PR and between 40c6425 and 7aec3b1.

📒 Files selected for processing (1)
  • src/components/shell/view/Shell.tsx

Comment on lines +107 to +123
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;
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

PaloSP added 2 commits March 4, 2026 13:36
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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Preserve legacy isActive behavior when autoConnect is omitted.

Line 42 defaults autoConnect to false while Line 52 ignores isActive. Existing callers that still pass only isActive (e.g., src/components/TaskList.jsx Lines 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7aec3b1 and def5f02.

📒 Files selected for processing (1)
  • src/components/shell/view/Shell.tsx

@blackmammoth
Copy link
Copy Markdown
Collaborator

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.

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.

Copy link
Copy Markdown
Collaborator

@blackmammoth blackmammoth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested and works.

@blackmammoth blackmammoth merged commit 2444209 into siteboon:main Mar 5, 2026
1 check passed
SuperOuxx pushed a commit to SuperOuxx/coding-agent-ui that referenced this pull request Mar 5, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Clickable overlay buttons for CLI prompts in Shell terminal

2 participants