Gabriel Vidal 439f2bf3a6 fix(ui): make HoldEditable's drag survive on touch, and hold work with a mouse
Two bugs, reproduced in a real browser before fixing.

**The drag died on the first hand-over (touch).** Reordering the list live
meant React moved the pressed wrapper node — losing the pointer capture — and
re-rendered whichever item now occupied a position-dependent slot; the navbar's
raised centre tab is a structurally different <a>, so the very anchor the finger
went down on got unmounted. A browser cancels the touch whose target left the
document, so the drag was cancelled and the order snapped back. Traced with a
MutationObserver alongside the pointer events: reorder → lostpointercapture →
anchor remount → pointercancel.

The DOM order is now frozen for the whole drag and the rearrangement is
expressed purely as transforms, computed from slot geometry measured once at
pickup; only the drop commits a real reorder, after the pointer is gone. The
held item's invisible in-slot copy also keeps rendering with its pre-pickup
props so its node is never replaced. Nothing in the DOM moves, so nothing can
cancel the gesture.

That also removes the FLIP bookkeeping wholesale: with the geometry fixed, no
re-measuring, no settle timers and no animation lock, and a fast drag can no
longer outrun the shuffle. The layout model reflows from each item's own size
and the original gaps, so a list of unequal items (an expanded shortcut card
among collapsed ones) lands correctly.

**Hold-to-pick-up never fired with a mouse.** Any travel over 10px during the
1.4s cancelled the hold — but a hand resting on a mouse drifts well past that,
so on desktop the pickup essentially never happened. Travel now only cancels
for touch and pen, where it means "I'm scrolling"; a mouse can't scroll with the
button held. The pickup uses the pointer's current position rather than where it
went down, so the item doesn't jump out from under a drifted cursor.

Also: the slot hand-over threshold sits just short of the midpoint, so aiming at
the middle of the item you want reliably takes that slot instead of being a coin
flip that left the item one short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 23:31:01 +02:00

ai-agent

A self-hosted agent layer for your computer — spawn and watch coding-agent sessions from an installable PWA, browse every conversation with real per-turn token costs, and manage the whole Claude context (CLAUDE.md files, skills, hooks, memories, projects) that shapes what the agent does.

Think Lovable, but open source and running on your own hardware: the agent builds things, the app shows you everything it touches — conversations, files, projects, costs, deploys — and nothing leaves your machine beyond the model API calls you configure.

Extracted from my homelab monorepo after ~280 commits; full history preserved. It runs my homelab's AI operations daily at ai-agent.lab.gabvdl.xyz.

What it does

  • Spawn / resume / interrupt agent sessions from the composer — pick a model (fetched live from the Anthropic API), an effort level, attach files — and watch the run stream into a live thread over SSE within seconds.
  • Conversation archive — every Claude Code transcript parsed into a rich thread view: per-turn token usage and dollar cost, tool cards with duration and diff stats, thinking blocks, task panels — each element with its own visibility switch.
  • Claude-context editor — every CLAUDE.md and the .claude/ tree (skills, hooks, agents, settings) as an editable file list with real token counts and per-model costs, recomputed only when content changes.
  • Catalogs — projects gallery (git history, goals, costs per project), services catalog (static compose + Traefik parsing), assistant memories, scaffolding templates, plans with estimated-vs-actual metrics.
  • Cron agents — scheduled sessions defined by a cron expression plus a prompt file; harness/model/effort declared in the prompt's frontmatter.
  • Notification hub — the source of truth for the agent's "done"/"ask" push notifications (Home Assistant integration included), with tappable answers POSTed back to the blocked session.

Architecture

┌─────────────────────────── Docker container ───────────────────────────┐
│  FastAPI backend (:8080)  ──serves──  React PWA (Vite, installable)    │
│  • transcript parser (resumable, O(new bytes) per live tick)           │
│  • SQLite store: token counts, costs, skill analytics                  │
│  • SSE hub: one event bus invalidates the UI                           │
│  • cron scheduler, notification hub, trusted-caller gate               │
└──────────────┬─────────────────────────────────────────────────────────┘
               │ POST /spawn|/resume|/interrupt (Bearer token)
        ┌──────▼───────────────┐
        │ sidecar (runner)     │  launches `claude -p … --session-id …`
        │ host process — or    │  transcripts land in watched dirs and
        │ inside the container │  stream back into the viewer
        └──────────────────────┘

The same sidecar/sidecar.py runs in one of two places and the backend can't tell the difference: on the host (a session then has the host user's own auth, hooks and skills — exactly a terminal session) or inside the container (RUNNER_IN_CONTAINER=1, the standalone default — the image bundles the Claude Code CLI and spawns sessions with no host process at all).

Notable engineering, hard-won on a real archive (~900 transcripts / 458 MB):

  • Resumable transcript parsing — a live session appends every couple of seconds; a ParserState per growing file is fed only the new bytes instead of re-parsing from byte 0 (O(n²) over a session, before).
  • One git walk, not one per item — both catalogs share a HEAD-keyed git log --name-only bucketed per directory. /api/services went from two git log forks per service per request (11 s) to <1 s.
  • Metadata-only list endpoints + per-file content loads + one-row SSE patches: the conversation list went 1.3 MB → 89 KB, /api/bundle 12.5 s → 26 ms, idle CPU ~58% → ~7% while a session streams.
  • Blue-green deploysdeploy.sh health-gates a standby container on /api/health, cuts the reverse proxy over, recreates, restores: zero downtime, and a bad build never touches the live container.
  • Trusted-caller security gate — the API answers only the reverse proxy, the host, or a shared token; read-only API keys allow safe methods only (a desk phone reads notifications aloud but can never spawn a session); uploaded bytes are served nosniff + attachment so user content never executes on the app origin.
  • Schema-drift canary — the Claude Code transcript format is internal and undocumented; scripts/schema-drift.py diffs a structural footprint of fresh transcripts against a committed baseline and cross-checks the real parser against raw record counts.
  • In-browser mock backend?mock=1 swaps fetch + EventSource for a deterministic, combinatorial in-memory seed (every UI state reachable from a cold load), so the PWA demos and tests with no backend at all.

Stack

FastAPI · Python 3.12 · SQLite (WAL) · React 18 · TypeScript · Vite · Tailwind · TanStack Query (IndexedDB-persisted) · Zustand (server-persisted) · SSE · orval (API types generated from the OpenAPI spec) · Docker · Traefik.

Run it

Standalone (no host mounts — catalogs degrade gracefully when a mount is absent):

docker compose -f docker-compose.standalone.yml up -d
# → http://localhost:8080  (set ANTHROPIC_API_KEY, or mount a logged-in
#   Claude home at the runner's $HOME, to spawn sessions)

scripts/standalone-smoke.sh boots the image against a throwaway workspace and asserts every catalog endpoint, the PWA shell and the bundled runner answer correctly.

In my homelab it runs behind Traefik + Authelia with the host-side sidecar; that deployment config (compose mounts, Traefik router, blue-green deploy.sh) lives in the homelab repo's services/ai-agent/ shim. The copies of those files in this repo's history are kept as reference.

Where it's going

See GOAL.md — the north star is the open-source, intuitive, private and secure agent layer of any computer: one container, zipgo-backed hosting, in-container self-update.

Description
No description provided
Readme 12 MiB
Languages
TypeScript 67.4%
Python 28.6%
Shell 1.3%
JavaScript 0.9%
HTML 0.8%
Other 1%