Files
ai-agent/sidecar
Gabriel Vidal 6c793d2dca feat(forms): structured ask-form questions answered in the PWA
Rich forms the agent asks the user to fill, replacing Claude Code's
interactive AskUserQuestion for spawned sessions:

- backend/forms.py + /api/forms REST (create / list / get with waitSecs
  long-poll / submit / cancel), JSON sidecar store at /data/forms.json,
  'form' SSE events; field types: text textarea number select multiselect
  radio checkbox slider file date, answers validated server-side
- conversation viewer renders the ask-form Bash call as a live inline form
  card (submit with confirm recap, cancel, file uploads via /api/upload);
  answers stay in the thread read-only after submit; wait/cancel subcommands
  render as compact status strips; new 'Form cards' visibility switch
- ?form=<id> query param (the notification deep link) opens the form
  focused in a full-screen modal
- sidecar disallows AskUserQuestion on spawned claude runs
  (SIDECAR_DISALLOWED_TOOLS to override)
- mock backend: /api/forms routes + seeded pending/submitted/cancelled
  forms and kitchen-sink thread cards (SEED_VERSION 7)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:18:34 +02:00
..

claude-sidecar

A tiny host-side FastAPI wrapper that launches claude -p sessions on behalf of the ai-agent viewer.

Why a host process (not a container)

The ai-agent viewer runs in Docker and can't reach the host's claude CLI — its auth (~/.claude/.credentials.json), RTK/vault hooks, and skills all live in the host user's home. This sidecar runs natively on the host as the repo owner, so a session it spawns is exactly like one started from a terminal.

Flow

Frontend (sticky input)
   │  POST /api/spawn {prompt}
   ▼
ai-agent backend (container)
   │  generates a session UUID, POST /spawn to the sidecar
   ▼  http://host.docker.internal:8790/spawn   (Bearer SIDECAR_TOKEN)
claude-sidecar (host)
   └─ claude -p <prompt> --session-id <uuid> --output-format json \
              --permission-mode bypassPermissions --model opus \
              --remote-control   (detached)

--remote-control is added by default so every spawned run registers with Claude Code Remote Control and shows up in the Claude app — you can watch and drive the conversation from your phone. Set SIDECAR_REMOTE_CONTROL=0 to opt out.

Claude writes its transcript to ~/.claude/projects/-home-gabrielvidal-homelab/<uuid>.jsonl, which the ai-agent container already watches read-only. The new conversation appears in the viewer within ~2s and the frontend redirects to it once it has synced.

The conversation view has a matching sticky composer that continues an existing thread: it POSTs /api/spawn's sibling /api/resume, which the backend proxies to POST /resume here — claude -p <prompt> --resume <sessionId> in the session's original cwd. Because resume reuses the session id, the new turns append to the same transcript and stream straight into the open conversation.

Install (host)

sidecar/install.sh

Creates a venv, writes a systemd user unit (~/.config/systemd/user/claude-sidecar.service), and starts it. Reboot survival needs sudo loginctl enable-linger $USER.

Config comes from the repo .env: SIDECAR_TOKEN, SIDECAR_PORT (default 8790). The ai-agent container reads SIDECAR_URL + SIDECAR_TOKEN (see its docker-compose.yml, which also adds host.docker.internal:host-gateway).

Endpoints

  • GET /health{ok, cwd, claude}
  • POST /spawn (Bearer auth) {prompt, sessionId?, model?, cwd?}{sessionId, pid, log} — spawns detached, returns immediately.
  • POST /resume (Bearer auth) {sessionId, prompt, model?, cwd?}{sessionId, pid, log} — continues an existing conversation by running claude -p <prompt> --resume <sessionId>. Reuses the original session id, so the new turns append to the same transcript and stream into the open conversation. --resume is directory-scoped, so pass the session's original cwd.
  • POST /interrupt (Bearer auth) {sessionId}{sessionId, pid, signal, ok} — sends the run's process group a SIGINT (like Ctrl+C), so Claude aborts the turn, writes a [Request interrupted by user] marker and exits. 404 if no live process is tracked for that session.

Per-session stdout/stderr is captured under logs/<sessionId>.log; the pid is recorded in logs/<sessionId>.pid so /interrupt can find the run.

Manage

systemctl --user status  claude-sidecar
systemctl --user restart claude-sidecar
journalctl --user -u claude-sidecar -f