Add Claude backend support with interactive prompt UI#21
Open
lysrain21 wants to merge 155 commits into
Open
Conversation
* 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.
Add Pi ask_user browser prompts
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ask_userbrowser prompt support on top of the Claude prompt UI work, including cursor message API deliveryChanges
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 inferenceutil.py: Claude session discovery, meta payload, /proc scan, CWD-scan fallbackrollout_log.py: Claude event extraction + AskUserQuestion/ExitPlanMode interactive prompt eventsrollout_log.py: Piask_usertool-call extraction into sharedinteractive: ask_user_questionevents, including context, options, backend marker, allow flags, and cursor tail/history deliverybroker.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 filenameserver.py: launch defaults, reasoning effort (low/medium/high/max), spawn with --dangerously-skip-permissions/--effort, /keys API endpoint for raw keystrokessessiond.py: Claude headless session supportFrontend (JS/CSS):
ask_userprompts render context and option buttons in the same interactive prompt UI/keyswithout the extra Claude submit EnterTest plan
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -vpython3 -m unittest tests.test_rollout_log -vask_userevents without live duplicatesnode --check codoxear/static/app.jspython3 -m py_compile codoxear/rollout_log.py\u001b[B\r🤖 Generated with Claude Code