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.