From 943ae7df033beabe765c6f79ef67095d24dd49ee Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 16 May 2026 05:55:50 +0000 Subject: [PATCH] docs: add v1.x roadmap snapshot Captures v1.0 through v1.6 history with status, decisions made, schema additions, reusable patterns, tech stack, container topology, and the dependency graph going forward through v1.11 (BooTerm). Authored by Sam; v1.6 details lifted from the v1.6 hand-back. Co-Authored-By: Claude Opus 4.7 (1M context) --- boocode_roadmap.md | 487 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 boocode_roadmap.md diff --git a/boocode_roadmap.md b/boocode_roadmap.md new file mode 100644 index 0000000..c4f15a9 --- /dev/null +++ b/boocode_roadmap.md @@ -0,0 +1,487 @@ +# BooCode v1.x — Roadmap + +Last updated: 2026-05-16 + +## Overview + +BooCode is a standalone code-chat tool at `/opt/boocode/`. Read-only by design in v1.x — pick a project, chat with a local LLM that has file-inspection tools, get streaming responses over WebSocket. Built May 2026 after the in-boolab BooCode mode stalled. + +v1 shipped in a single Claude Code session. v1.1 onwards is a batched build-out. Original Batch 1–10 plan was reordered mid-stream — chats-inside-sessions, archive, and fork/delete work was prioritized over the mobile pass. + +Live at `https://code.indifferentketchup.com` (Caddy → Authelia → Tailscale → `100.114.205.53:9500`). + +----- + +## Version summary + +|Version |Theme |Status |Notes | +|----------------|---------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|-----------------------------------------| +|v1.0 |Initial scaffold, read-only tools, WS streaming |✅ Done |Shipped in one Claude Code session | +|v1.1-batch1 |Markdown, Copy + Regen, tok/s + ctx, AI chat naming |✅ Merged |— | +|v1.1-batch2 |Sidebar restructure: projects → sessions, max 5 + “view all” |✅ Merged |— | +|v1.1-batch3 |Pane system, FileBrowserPane + Shiki, chat→file click, cross-tab |✅ Merged |— | +|v1.1-batch3.5 |Chip infrastructure, `@file` picker, line-select-attach |✅ Merged |— | +|v1.2 |Chats inside sessions refactor, right-rail, `/compact`, archive, force-send |✅ Merged |Replaced original “Batch 4 = mobile” plan| +|v1.2-project-ux |Project archive UX, sidebar context menu, full-bootstrap, Gitea API |✅ Merged |— | +|v1.3 |Tab-close + chat-archive |✅ Merged |— | +|v1.4 |Fork from message + delete message + header polish + housekeeping |✅ Merged |Was original “Batch 5” | +|v1.5 |Refactor splits, vitest harness (23 tests), error-log surfacing, `/opt:ro` + `BOOTSTRAP_ROOT`, persistent context-window tracker |✅ Merged |— | +|v1.5.1 |Bootstrap hotfix: git in container, SSH keypair, known_hosts, SSH URL rewrite, /opt/projects label |✅ Merged |`4a9f207` | +|v1.6-mobile-pass|Mobile pass: drawer, pane stacking, long-press, swipe-to-close, pull-to-refresh, IME safety, safe-area, tap targets + H1 path-guard fix|🔄 Hand-back received, uncommitted|Was original “Batch 4” | +|v1.6.1-cleanup |Stale code audit, overengineering audit, secrets hygiene, RightRail mobile fix |Planned (next) |— | +|v1.7 |Drag-drop + paste-as-attachment (chip infra extension) |Planned |Was Batch 6 | +|v1.8 |Settings drawer (system prompt per project + session, web search toggle) |Planned |Was Batch 7 | +|v1.9 |Web search backend: SearXNG `web_search` + `web_fetch` tools |Planned |Was Batch 8 | +|v1.10 |Agents (Tier 2): `AGENTS.md`, per-agent model/temp/tools, picker |Planned |Was Batch 9 | +|v1.11 |BooTerm: separate container, xterm.js + node-pty + tmux, terminal pane |Planned |Was Batch 10 | + +----- + +## Version details + +### v1.1-batch1 — Message polish ✅ + +Markdown (`react-markdown` + `remark-gfm`), Copy + Regenerate, tok/s + context counter, AI session naming. + +**Key decisions:** + +- `sessions.name` (not `title`). +- `enable_thinking: false` + `max_tokens: 30` for Qwen3 utility calls. +- `messages_deleted` WS frame added for multi-tab regen. +- In-app event bus (`sessionEvents.ts`, module-scope `Set`). + +**Schema:** `messages.tokens_used`, `messages.ctx_used`, `messages.ctx_max`, `messages.started_at`, `messages.finished_at`. + +----- + +### v1.1-batch2 — Sidebar restructure ✅ + +Projects as expandable groups, up to 5 recent sessions per project, “View all (N)”, `GET /api/sidebar`, `useSidebar` singleton hook. + +**Key decisions:** + +- `useSidebar` module-scope singleton. +- `localStorage['boocode.sidebar.expanded']`. +- `session_renamed` payload `{session_id, name}`. + +----- + +### v1.1-batch3 — Pane system ✅ + +`session_panes` table, pane CRUD with transactional position-shift, Workspace + tab strip + drag-to-reorder (native HTML5), ChatPane (extracted), FileBrowserPane (tree + Shiki + filter), chat→file click, PaneTab context menu, `file_ops` + `file_index` shared services, broker user channel + `/ws/user`, `session_updated`, `session_loaded`, idempotent default-Chat-pane backfill. + +**Schema:** `session_panes` (id, session_id, position, kind CHECK, state JSONB). + +----- + +### v1.1-batch3.5 — Chips + @file + line-select ✅ + +`Attachment` type + `flattenToMessage` + LANG_MAP, `AttachmentChip`, `AttachmentPreviewModal`, ChatInput chip-row, hand-rolled `@file` mention popover, line-select-attach in FileBrowserPane via local `FileViewer`, FileBrowserPane filter upgrade (empty=tree, non-empty=flat). + +----- + +### v1.2 — Chats inside sessions ✅ + +Originally planned as the mobile pass. Reshuffled: structural refactor — chats inside sessions, right-rail, `/compact` (chat’s own model summarizes via `kind='compact'` system message), force-send. + +**Schema:** `chats` table, `sessions.status`, `messages.chat_id`, `messages.kind` (regular | compact). + +----- + +### v1.2-project-ux ✅ + +Full new-project bootstrap (mkdir + git init + .gitignore + first commit + Gitea remote + push), sidebar context menu (Rename / Archive / Open in Gitea), project landing page archived-list, Gitea API integration. Option B taken: `BOOTSTRAP_ROOT` env var, `/opt` stays read-only mount, `/opt/projects` writable. + +**Schema:** `projects.status`, `projects.archived_at`. + +**Key decisions:** + +- `execFile` only, no `exec` shell strings. +- DB INSERT last in bootstrap sequence. +- Soft-fail on Gitea steps. +- Project Delete endpoint exists but stays unexposed (re-add INSERTs fresh row → FK cascade nukes history; archive is the safe pattern). + +----- + +### v1.3 — Tab close + chat archive ✅ + +Tab close UX cleanup, chat-level archive (separate from session archive). + +----- + +### v1.4 — Fork + delete + header polish ✅ + +Was originally planned as Batch 5. + +**Shipped:** `POST /api/sessions/:id/fork` (deep copy messages up to target, new session in same project), `DELETE /api/sessions/:id/messages/:id` (cascading via `messages_deleted` frame), header breadcrumb (Projects → Project → Session), inline-editable session name, file path shown when File Browser pane is active, `useActivePane` hook. + +----- + +### v1.5 — Refactor + tests + security scoping + context tracker ✅ + +5-commit sequence: + +1. **Refactor:** FileBrowserPane (865 → split with FileViewer extracted), Workspace, inference split. +1. **Vitest harness:** 23 tests covering routes + resolveProjectPath. Pinned to v3 (Vite 5 / vitest 4 incompatibility). +1. **Error-log surfacing:** dead-code removal from earlier H1/H2 audit items, structured error logs to client. +1. **Mount scoping:** `/opt:/opt:ro` + `BOOTSTRAP_ROOT` writable subdir. Container loses write to `/opt` proper. +1. **Persistent context-window tracker:** floating popover above chat input right edge, source = latest `message_complete` frame’s `ctx_used` / `ctx_max`, color-coded (neutral <60%, amber 60–85%, red 85%+), hides when `ctx_max` null. + +**Carried bug:** `resolveProjectPath` whitelist-root bypass — discovered, asserted as “BEHAVIOR GAP” rather than silently patched. Fix landed in v1.6 (H1). + +----- + +### v1.5.1 — Bootstrap hotfix ✅ (`4a9f207`) + +Dockerfile (git installed in container), docker-compose.yml, project_bootstrap.ts (SSH keypair, known_hosts, SSH URL Tailscale rewrite), CreateProjectModal.tsx, .gitignore. /opt/projects label clarified. + +**Known issue carried forward:** dispatch used the in-repo `secrets/boocode_gitea` SSH key because the agent key was rejected. Key exposure flagged. Audit + rotation tracked in v1.6.1 below. + +----- + +### v1.6-mobile-pass 🔄 + +**Hand-back received, uncommitted on `v1.6-mobile-pass`.** 5-commit sequence proposed: + +1. `chore: fix resolveProjectPath whitelist-root bypass` (H1 — dropped `real !== whitelistReal` short-circuit; 23/23 pass). +1. `feat(mobile): viewport hook + sidebar drawer + hamburger headers` (M1 + M2 + M6-header). +1. `feat(mobile): single-pane stack + long-press tab menu + swipe-to-close` (M3 + M4 + A2). +1. `feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety` (M5 + M6-bottom + M7 + M8). +1. `feat(mobile): pull-to-refresh sidebar list` (A1). + +**Decisions:** + +- H2 (roadmap update) handled in this file rather than by Claude Code. +- M5: mobile = button-only send, Enter inserts newline. Desktop unchanged. `isComposing` guard for CJK IME. +- M6: kept `max-w-[1000px]` (mobile naturally full-width below cap). +- URL state: `?pane=`. Bare URL resets activePaneIdx to 0. +- Long-press dispatches synthetic `contextmenu` on `[data-tab-id]`, opening Radix ContextMenuTrigger at touch coords. iOS callout suppressed. +- `SwipeablePaneTab`: 60px threshold, bails if vertical >30px, opacity 1→0.4. +- A2 bundled with M3 in Commit 3 (structural coupling). +- Home.tsx no hamburger. + +**Deferred from v1.6 → rolled into v1.6.1-cleanup:** + +- RightRail still renders on mobile (~32px column). +- Secrets hygiene audit. +- `ProjectSidebar.tsx` and `ChatTabBar.tsx` share content from two commits each — use `git add -p`. + +----- + +### v1.6.1-cleanup — Stale + overengineering audit + secrets hygiene (next) + +**Depends on:** v1.6 committed. + +**Scope:** + +1. RightRail mobile fix (`max-md:hidden` on outer container). +1. Secrets audit: rotate `secrets/boocode_gitea`, confirm `.gitignore` covers `secrets/`, scan git history (`git log --all -- secrets/`), `git filter-repo` or BFG if exposed in history, force-push if rewriting. +1. Fix agent SSH key path so future Claude Code dispatches don’t fall back to in-repo keys. +1. Stale code audit: pruning unused exports, dead WS frames (e.g. `session_renamed` server publisher TODO from Batch 1), backup `.bak` files, unused imports. +1. Overengineering audit: places where hand-rolled patterns are more complex than necessary, places where singleton hooks should consolidate (`useSessionStream` refcount). +1. PATCH `/api/panes/:id` session-ownership check tightening. +1. `/opt:/opt:ro` mount whitelist tightening (precursor to BooCoder). + +**No new features. No schema changes.** + +----- + +### v1.7 — Drag-drop + paste (planned, was Batch 6) + +**Depends on:** v1.6.1 merged. + +**Scope (trimmed — chip infra exists from v1.1-batch3.5):** + +- Drag-drop files onto ChatInput → chip via `addAttachment({kind: 'file', source: 'drop'})`. +- Paste >8 lines → chip via `addAttachment({kind: 'paste', source: 'paste'})`. ≤8 lines inline. +- Drop overlay (dashed border + “Drop to attach”). +- Client-side 5 MB cap + binary detection (null-byte check in first 8KB). +- Max 10 attachments shared cap. +- Folder drop rejected. Image paste rejected. Binary files rejected with toast. + +**Frontend only.** + +----- + +### v1.8 — Settings drawer (planned, was Batch 7) + +**Depends on:** header gear (already in v1.4). + +**Scope:** + +- Right-side drawer (hand-rolled, no shadcn Sheet). Tabbed: Session + Project. +- Session tab: system prompt, web search toggle, model picker, session name. +- Project tab: default system prompt, default web search, project name, root path (read-only), delete project (consider whether to expose given the cascade concern). +- Resolution: `session.system_prompt OR project.default_system_prompt OR ""`. +- Project defaults applied at session create (copied), not retroactively. +- Web search toggle persistent per session (`sessions.web_search_enabled`). + +**Schema:** `sessions.web_search_enabled`, `projects.default_system_prompt`, `projects.default_web_search`. + +----- + +### v1.9 — Web search backend (planned, was Batch 8) + +**Depends on:** v1.8. + +**Scope:** + +- `web_search` tool → SearXNG at `http://100.114.205.53:8888/search?format=json`, top-N `{title, url, snippet}`. +- `web_fetch` tool, regex HTML strip (no cheerio), 50KB cap. +- Tools conditionally included based on `session.web_search_enabled`. +- `ToolCallCard.tsx` renders results as clickable URL list, web_fetch as text preview. +- Env: `SEARXNG_URL`, `WEB_FETCH_TIMEOUT_MS`, `WEB_FETCH_MAX_BYTES`. + +----- + +### v1.10 — Agents (planned, was Batch 9) + +**Depends on:** v1.8. + +**Scope:** + +- Tier 2 agents: system prompt + model + temperature + tools whitelist per agent. +- `AGENTS.md` (OpenCode-compatible): `## Agent Name` blocks with YAML frontmatter. +- Three builtin defaults (Investigator, Architect, Reviewer) when no `AGENTS.md`. +- If `AGENTS.md` exists, only its agents shown. +- Agent picker in ChatInput toolbar + SettingsDrawer. +- Tools whitelist enforced at inference layer. BooChat agents read-only. +- File parsed on demand with mtime cache. +- Mid-conversation agent switch allowed; old messages retain their tool history. + +**Schema:** `sessions.agent_id TEXT`. + +----- + +### v1.11 — BooTerm (planned, was Batch 10) + +**Depends on:** v1.1-batch3 (pane system), v1.8 (settings drawer pattern). + +**Scope:** + +- New container `booterm` at `100.114.205.53:9501`. Fastify + node-pty + tmux. +- Caddy path-based routing: `/api/term/*` + `/ws/term/*` → booterm. +- Shared `boocode_db`. +- Per-session tmux (`bc-`), per-pane tmux window (`term-`). +- xterm.js terminal pane. New `kind = 'terminal'` in `session_panes` CHECK. +- PTY over binary WebSocket. Resize via `tmux resize-window`. +- Workspace mount: `/opt/repos:/opt/repos:rw`. BooCode chat container keeps `/opt:/opt:ro`. +- Send-to-terminal from chat: select text → right-click → “Send to terminal”. +- tmux persistence across WS reconnects, page refreshes, container restarts. +- No chroot/namespace isolation. Acceptable single-user homelab. + +**New app:** `apps/booterm/`. + +----- + +## Architecture + +### Containers (current + planned) + +|Container |Port |Mount |Purpose |Status | +|------------|---------------------|-----------------------------------|----------------------------|---------| +|`boocode` |`100.114.205.53:9500`|`/opt:/opt:ro` + `/opt/projects:rw`|Chat + read-only tools + SPA|Live | +|`boocode_db`|`127.0.0.1:5500` |`boocode_pgdata` |Postgres 16-alpine (shared) |Live | +|`booterm` |`100.114.205.53:9501`|`/opt/repos:/opt/repos:rw` |Terminal sessions |v1.11 | +|`boocoder` |TBD |`/opt/repos:/opt/repos:rw` |Write tools |Post-v1.x| + +### URL routing (target state after v1.11) + +``` +code.indifferentketchup.com +├── / → boocode (SPA) +├── /api/chat/*, /ws/chat/* → boocode :9500 +├── /api/term/*, /ws/term/* → booterm :9501 +├── /api/coder/*, /ws/coder/* → boocoder (future) +└── /ws/user → boocode :9500 +``` + +### Database + +Single Postgres `boocode_db`. All containers share. Projects shared. Sessions container-specific. + +Current schema (post v1.5.1): + +``` +projects +├── id UUID PK +├── name TEXT +├── root_path TEXT +├── status TEXT (v1.2-project-ux: active | archived) +├── archived_at TIMESTAMPTZ (v1.2-project-ux) +├── default_system_prompt TEXT (v1.8) +├── default_web_search BOOLEAN (v1.8) +└── created_at TIMESTAMPTZ + +sessions +├── id UUID PK +├── project_id UUID FK → projects +├── name TEXT +├── model TEXT +├── system_prompt TEXT +├── status TEXT (v1.2: active | archived) +├── web_search_enabled BOOLEAN (v1.8) +├── agent_id TEXT (v1.10) +├── created_at TIMESTAMPTZ +└── updated_at TIMESTAMPTZ + +chats (v1.2) +├── id UUID PK +├── session_id UUID FK → sessions +├── name TEXT +├── status TEXT +├── created_at TIMESTAMPTZ +└── updated_at TIMESTAMPTZ + +messages +├── id UUID PK +├── session_id UUID FK → sessions +├── chat_id UUID FK → chats (v1.2) +├── kind TEXT (v1.2: regular | compact) +├── role TEXT +├── content TEXT +├── tool_calls JSONB +├── tool_results JSONB +├── status TEXT +├── last_seq INTEGER +├── tokens_used INTEGER (v1.1-batch1) +├── ctx_used INTEGER (v1.1-batch1) +├── ctx_max INTEGER (v1.1-batch1) +├── started_at TIMESTAMPTZ (v1.1-batch1) +├── finished_at TIMESTAMPTZ (v1.1-batch1) +└── created_at TIMESTAMPTZ + +session_panes (v1.1-batch3) +├── id UUID PK +├── session_id UUID FK → sessions (CASCADE) +├── position INTEGER +├── kind TEXT CHECK (chat | file_browser | terminal) +├── state JSONB +└── created_at TIMESTAMPTZ + +settings +├── k TEXT PK +└── v TEXT +``` + +### Reusable patterns + +|Pattern |Where |Used by | +|----------------------------|----------------------------------------|---------------------------------------------------------| +|In-app event bus |`sessionEvents.ts` |All batches. Module-scope `Set`. | +|Singleton hooks |`useSidebar.ts` |Module-scope shared state. | +|User-channel WS broker |`broker.ts` + `useUserEvents.ts` |Cross-tab lifecycle. One WS per tab. | +|`clock_timestamp()` |All INSERT/UPDATE |Never `NOW()` in new code. | +|Additive schema only |`schema.sql` |`ADD COLUMN IF NOT EXISTS`, `CREATE TABLE IF NOT EXISTS`.| +|Idempotent backfills |`schema.sql` |`INSERT ... WHERE NOT EXISTS`. | +|`enable_thinking: false` |`auto_name.ts` |Required for Qwen3 utility calls. | +|`pathGuard` |`tools/*`, `file_ops.ts` |Realpath + project root enforcement. | +|Shared `file_ops.ts` |`tools.ts`, `routes/projects.ts` |Same core for inference tools and UI. | +|File index (`file_index.ts`)|`routes/projects.ts` |`rg --files` + mtime cache. | +|`useViewport` |`hooks/useViewport.ts` (v1.6) |matchMedia, SSR-safe. | +|`useSidebarDrawer` |`hooks/useSidebarDrawer.tsx` (v1.6) |Context + auto-close on route change. | +|Hand-rolled long-press |`hooks/useLongPress.ts` (v1.6) |500ms touchstart timer, dispatches synthetic contextmenu.| +|Hand-rolled pull-to-refresh |`hooks/usePullToRefresh.ts` (v1.6) |80px threshold, 600ms min hold. | +|Hand-rolled swipe |`components/SwipeablePaneTab.tsx` (v1.6)|60px threshold, vertical bail at 30px. | + +----- + +## Tech stack + +|Layer |Tech | +|----------------|--------------------------------------------------------------------------| +|Backend |Node 20 + Fastify + `@fastify/websocket` + `@fastify/static` + zod + `pg` | +|Frontend |React + Vite + Tailwind v4 + shadcn nova preset | +|Inference |llama-swap `http://100.101.41.16:8401` (OpenAI-compatible) | +|Search |SearXNG `http://100.114.205.53:8888` (v1.9) | +|Syntax |Shiki (`github-dark`) | +|Terminal |xterm.js + node-pty + tmux (v1.11) | +|Auth |`Remote-User` from Authelia via Caddy `forward_auth` | +|Containerization|Docker Compose, Node 20-alpine, multi-stage, ripgrep apk, git apk (v1.5.1)| +|DB |Postgres 16-alpine, loopback `127.0.0.1:5500` | +|Networking |Tailscale mesh, Caddy (DO droplet), Authelia SSO | +|Code hosting |Gitea `git.indifferentketchup.com` | +|Tests |vitest v3 (pinned, Vite 5 / vitest 4 incompatible) | + +----- + +## Known open items + +- **`useSessionStream` refcount.** Two ChatPanes = two WS. Apply singleton pattern. Tracked in v1.6.1. +- **PATCH `/api/panes/:id` lacks session-ownership check.** Single-user fine; tighten in v1.6.1. +- **`/opt:/opt:ro` mount exposes all `.env` files.** Whitelist scope before BooCoder. Tracked in v1.6.1. +- **`session_renamed` no server WS publisher.** Carried from Batch 2. Tracked in v1.6.1. +- **`secrets/boocode_gitea` in repo.** v1.5.1 dispatch fallback. Rotation + history scrub in v1.6.1. +- **Dormant in-boolab BooCode mode.** Reference only. +- **BooCoder container.** Post-v1.x. + +----- + +## Dependency graph + +``` +v1.0 (initial) + │ + ▼ +v1.1-batch1 (markdown) + │ + ▼ +v1.1-batch2 (sidebar) + │ + ▼ +v1.1-batch3 (panes) ────────────────────────┐ + │ │ + ▼ │ +v1.1-batch3.5 (chips) ──────┐ │ + │ │ │ + ▼ │ │ +v1.2 (chats-in-sessions) │ │ + │ │ │ + ▼ │ │ +v1.2-project-ux │ │ + │ │ │ + ▼ │ │ +v1.3 (tab-close) │ │ + │ │ │ + ▼ │ │ +v1.4 (fork+delete+header) ◄──┼────────────────┘ + │ │ + ▼ │ +v1.5 (refactor+tests+ctx) │ + │ │ + ▼ │ +v1.5.1 (bootstrap hotfix) │ + │ │ + ▼ │ +v1.6-mobile-pass │ + │ │ + ▼ │ +v1.6.1-cleanup ◄─────────────┘ + │ + ▼ +v1.7 (drag-drop) ◄── v1.1-batch3.5 + │ + ▼ +v1.8 (settings) + │ + ├──▶ v1.9 (web search) + │ + ├──▶ v1.10 (agents) + │ + └──▶ v1.11 (BooTerm) ◄── v1.1-batch3 +``` + +----- + +## Workflow + +1. Verify previous version merged to `main`. +1. Dispatch prompt via Paseo (Claude Code runs at `/opt/boocode`). +1. Claude Code recon → blocking questions → implement → hand back. +1. Review hand-back in separate Claude chat (spec compliance, code quality, drift, stale code). +1. Deploy: `docker compose up --build -d`. +1. Smoke test per the hand-back’s plan. +1. Sam commits manually, pushes to Gitea, merges to `main`. +1. Next version. + +Sam reviews all diffs. Sam commits. Never git pull/push/commit on his behalf.