Skip to content

Add Claude backend support with interactive prompt UI#21

Open
lysrain21 wants to merge 155 commits into
yiwenlu66:mainfrom
lysrain21:main
Open

Add Claude backend support with interactive prompt UI#21
lysrain21 wants to merge 155 commits into
yiwenlu66:mainfrom
lysrain21:main

Conversation

@lysrain21

@lysrain21 lysrain21 commented Apr 23, 2026

Copy link
Copy Markdown

Summary

  • Add Claude Code as a third agent backend alongside Codex and Pi
  • Interactive prompt rendering (AskUserQuestion / ExitPlanMode) as clickable buttons in web UI
  • Add latest Pi ask_user browser prompt support on top of the Claude prompt UI work, including cursor message API delivery
  • Light monochrome UI redesign

Changes

Backend (Python):

  • claude_log.py: JSONL log parser (session ID, CWD, user/assistant text, token usage, turn end detection, thinking/tool_use counts)
  • agent_backend.py: CLAUDE_BACKEND registry + log path inference
  • util.py: Claude session discovery, meta payload, /proc scan, CWD-scan fallback
  • rollout_log.py: Claude event extraction + AskUserQuestion/ExitPlanMode interactive prompt events
  • rollout_log.py: Pi ask_user tool-call extraction into shared interactive: ask_user_question events, including context, options, backend marker, allow flags, and cursor tail/history delivery
  • broker.py: Claude state machine (top-level type=user/assistant/system), pre-fork log snapshot, CWD-scan discovery, no bracketed paste for Ink UI, UUID from filename
  • server.py: launch defaults, reasoning effort (low/medium/high/max), spawn with --dangerously-skip-permissions/--effort, /keys API endpoint for raw keystrokes
  • sessiond.py: Claude headless session support

Frontend (JS/CSS):

  • Claude tab in new session modal with logo, model/reasoning selectors
  • Interactive prompt stepper: AskUserQuestion renders as sequential clickable option buttons, ExitPlanMode as approve/reject
  • Pi ask_user prompts render context and option buttons in the same interactive prompt UI
  • Pi option clicks send raw arrow/enter key sequences through /keys without the extra Claude submit Enter
  • Browser notes clearly mark unsupported Pi prompt modes such as multi-select, freeform-only answers, and optional comments
  • Light monochrome UI redesign (--bg:#f5f5f5, --accent:#333, no blue accents)

Test plan

  • 281 unit tests pass: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v
  • Focused Pi prompt parser tests: python3 -m unittest tests.test_rollout_log -v
  • Cursor API regression tests: tail/history/live include Pi ask_user events without live duplicates
  • JavaScript syntax check: node --check codoxear/static/app.js
  • Python compile check: python3 -m py_compile codoxear/rollout_log.py
  • Claude log parsing smoke test against real session logs
  • End-to-end browser test: create Claude session, send message, receive response
  • Browser-tools Pi fixture validation: cursor-delivered prompt rendered, option click entered answered state, backend received \u001b[B\r
  • New session modal: 3 backend tabs (Codex/Pi/Claude) with correct defaults
  • Pi/Codex sessions unaffected

🤖 Generated with Claude Code

san-tian and others added 30 commits March 5, 2026 13:22
* Backend: file history, broker busy fix, resume helper

* tests: init SessionManager aliases/files in helper

SessionManager now expects _aliases and _files; the __new__ test helper must set them.

---------

Co-authored-by: Yiwen Lu <mcfloundinho@gmail.com>
Rebased onto latest main to resolve conflicts.

Co-authored-by: Yiwen Lu <mcfloundinho@gmail.com>
When a server user_message matches the optimistic local echo bubble, we consume it and update the pending DOM node. Ensure the acknowledged user event is still recorded in the local cache and duplicate filter so reloads do not drop recent user prompts.
Invalidate v2 caches that could omit recent user messages due to pending-ack consumption in earlier builds.
Prevent duplicate chat events in init snapshots by deduplicating on (role, ts_ms, text) when seeding and incrementally appending.
lysrain21 and others added 17 commits May 29, 2026 17:58
- agent-error: terminal vs auto-retried (retryInMs) distinction so the UI no
  longer flaps busy on Claude 502 retries; fix frontend role filters that
  dropped agent_error cards (role=system) before render
- file resolution: route /file/read + /file/blob through the typed resolver;
  surface not_found/dead_symlink/permission_denied/outside_allowed_root reasons;
  block absolute paths escaping the session cwd (was serving e.g. /etc/hostname)
- notifications: NotificationChannel protocol + WebPushChannel + BarkChannel with
  bark_base_url deep-link; HTTP-only deployments can receive mobile push
- ask_user: normalize Claude multiSelect->allowMultiple + header; implement
  multi-select (toggle+confirm) and reliable single-select cursor mapping; send
  move and action keys separately to avoid a TUI race
- ui: full dark-mode variable set; panels/active-session/buttons use vars
Follow-up to a8c92e1 applying three rounds of adversarial review on the
fix-reported-ux-issues change.

File-path containment (server.py):
- _containment_violation enforces cwd containment on every resolver branch
  (relative .., absolute, in-cwd symlink-to-outside, tracked/bare-filename
  fallbacks). Resolves the request's PARENT the same way allowed_root is
  resolved, so a symlinked cwd component no longer false-rejects every file,
  while a dead symlink inside the cwd still reports dead_symlink.
- /file/download and /file/write (overwrite) route through the typed resolver.
  The overwrite branch now rejects EVERY non-ok status and drops the weaker
  _resolve_session_path fallback, closing an arbitrary-overwrite escape via an
  in-cwd symlinked dir + missing intermediate segment (sl/nope/../secret.txt).
- Both the absolute and relative branches fail closed when a named session
  cannot be resolved, instead of resolving against the server process cwd.
- Removed the unused _resolve_client_file_path wrapper.

Busy-state retry logic (claude_log.py):
- claude_agent_error_is_terminal treats an error as non-terminal only on
  positive evidence of a pending retry (finite retryInMs>0 AND integer
  retryAttempt<maxRetries). Malformed/missing/non-int/bool/NaN/inf counters are
  terminal, avoiding a stuck-busy spinner. Pi/Codex errors remain turn-terminal.

UI (app.js):
- agent_error events get a dedup key (ts+source+type+message) so the same error
  arriving via tail and live append renders once; declines to dedup when ts is
  absent rather than collapsing distinct errors.
- Multi-select commits cursorIdx right after the move send so a failed toggle
  cannot desync the cursor position.

Tests: 335 passed, 1 skipped. New regression coverage for every escape above
plus the symlinked-cwd false-positive and malformed-retry cases. All fixes
re-verified live via the running server (read/download/write containment, error
card rendering in a real browser).
UI/UX accessibility polish on the Codoxear SPA:
- Keyboard :focus-visible rings (2px --accent) on buttons, .icon-btn, links,
  and text/password/datetime inputs + selects. Inputs previously set
  outline:none with no replacement; only option buttons had a ring.
- agent_error cards get role="alert" so screen readers announce upstream agent
  errors rather than relying on the red border alone.
- prefers-reduced-motion: reduce collapses transitions/animations to ~0s.

Verified live via headless Chrome: 2px rgb(91,157,255) ring on Tab focus,
19 error cards with role=alert, transition-duration 1e-05s under reduced motion.
Full suite still 335 passed, 1 skipped.
role="alert" is an assertive ARIA live region. Setting it unconditionally in
makeRow meant loading a session with many past errors announced all of them at
once. Pass a `live` flag so only the live-poller append path (appendEvent) marks
the card assertive; historical cards (renderTranscript, prependOlderEvents) get
no live-region role and read in normal document order.

Verified live: session load shows 19 error cards, 0 with role=alert; visual
styling and dedup unchanged. 335 tests pass, 1 skipped.
Adversarial review of the a11y pass found two gaps:
- Session cards were clickable divs with no keyboard path. They are now
  role="button" + tabindex=0 with an aria-label and Enter/Space activation, so
  keyboard-only users can switch sessions (the app's primary navigation). This
  also makes the [role=button]/[tabindex] focus-ring selectors live.
- The borderless .queueText editor suppressed its own outline, so it had no
  focus affordance and the generic input:focus-visible ring was overridden by
  the more-specific .queueText:focus { outline:none }. Surface focus on the
  .queueEditorShell container via :focus-within instead.

Verified live: Tab reaches a session card (2px accent ring), Enter selects it
and loads the transcript. 335 tests pass, 1 skipped.
The previous commit put role=button + tabindex on the .session container, which
also wraps the rename/dup/delete <button>s. That is a nested-interactive ARIA
violation and made child-button key events bubble into the open handler: Space
on a row button was hijacked (button dead, card selected) and Enter on a row
button fired both the button action and session-select.

Put the open-session affordance on the inner title/meta region instead
(.sessionMain desktop / .sessionInner mobile) — a sibling of the action buttons,
never their ancestor — via a makeOpenTarget() helper that also guards
e.target !== node in its keydown.

Verified live: card container is a plain div (no role), the open-target is a
role=button with 0 nested buttons and its own focus ring, Tab visits it as a
separate stop from the action buttons, Enter selects + loads the transcript, and
Enter on a child rename button no longer switches sessions. 335 tests pass.
A self-contained contrast audit (luminance + alpha compositing, run live in both
color schemes) surfaced real AA failures, now fixed:

- --muted text was too light: rgba(0,0,0,0.45) ~3.1:1 in light mode and
  rgba(255,255,255,0.52) in dark. Raised to 0.58 / 0.62 (~4.6:1), lifting
  metaText, day separators, Load-older, prompt tags, and error timestamps to AA.
- .ts timestamps and the composer .ph placeholder used a hardcoded dark navy
  with no dark-mode override (near-invisible dark-on-dark); both now use
  var(--muted) and track the theme.
- Two opaque-light surfaces rendered as white-in-dark-mode: the composer input
  pill (.composer form) and the copy button (.msg-copy-btn). Both now use
  var(--panel)/var(--muted); verified their dark-mode bg is now rgb(23,26,33)
  and a re-scan finds zero opaque light surfaces.
- Light-mode --danger darkened #c53030 -> #b91c1c so the error-card chips reach
  ~4.5:1 on the tinted card.

Audit also confirmed: no missing alt text, input labels, or button names across
250 nodes. 335 tests pass, 1 skipped.
The prior commit set .msg-copy-btn border to var(--border) (~1.2:1 vs the page
bg), below WCAG 1.4.11's 3:1 for UI-component boundaries — the circular button
outline was near-invisible and leaned entirely on the box-shadow. Use
var(--muted) for the border so the affordance is a clear mid-tone in both
themes. 335 tests pass.
The UI followed the OS/browser color scheme via @media (prefers-color-scheme:
dark), so the same build rendered white on a light-mode Mac but black on a
dark-mode phone. Per request, make the app light-only:
- Remove the dark-mode @media block (its sole non-theme rule, the error-card
  background, already has a light definition at .event-card.event-error).
- Lock color-scheme: light on :root so native controls (form fields,
  scrollbars) stay light regardless of the OS setting.

Verified live at 390px width under an emulated dark OS setting: body bg
rgb(245,245,245), panel #ffffff, composer #ffffff, color-scheme light — i.e.
identical to light-mode. 335 tests pass.
The codoxear AskUserQuestion browser handler shipped under
fix-reported-ux-issues task 12 had two bugs in real-world states the
original task did not cover:

1. Single-select n>=2 final question: the handler sent only one \r after
   the option selection, but Claude's TUI opens a "Review your answers"
   confirmation screen after the final option Enter and waits for a
   second Enter on the pre-positioned "Submit answers" row. Without that
   second \r the prompt stayed on screen indefinitely, the agent never
   resumed, and the user had to press Submit in the terminal manually.

2. Multi-select non-final Confirm: the handler sent Tab+Enter, but Tab
   already advances cleanly to the next question; the trailing Enter
   auto-toggled q[i+1] opt 0 ON as a side effect, and the user's first
   Space click on q[i+1] then toggled it back OFF, silently dropping
   one selection per non-first question.

Five live scenarios verified against broker-169813 (Claude Opus 4.7,
2026-06-01) by reproducing the patched app.js byte sequences via the
same /api/sessions/<id>/keys endpoint the browser calls:

- single-select n=3: 3/3 answers landed, no skipped, prompt closed <1s.
- single-select n=1: \r alone submits (no review screen); the new
  isSingleQuestion guard correctly skips the second \r.
- multi-select n=3: 6/6 toggles preserved across all 3 questions
  (after Tab-only non-final Confirm fix).
- multi-select n=1: both toggles preserved.
- cursor-drift regression: documented limitation. The new shared cursor
  model defends against frontend-caused drift (Claude auto-advance,
  re-render) but not external Tab; recorded in browser-validation.md
  as a follow-up, not blocking this fix.

Includes 9 new automated tests (337 ran, 1 skipped, 0 failures) and a
new OpenSpec change with proposal/design/tasks/spec recording the
confirmed TUI protocol against Claude Code v2.1.156 for future
regression diagnostics.
Fix Claude AskUserQuestion final-question submit and cursor drift
Allow file saves when session log is missing
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.

4 participants