Files
boocode/boocode_roadmap.md
indifferentketchup 943ae7df03 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) <noreply@anthropic.com>
2026-05-16 05:55:50 +00:00

25 KiB
Raw Permalink Blame History

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 110 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<Listener>).

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 (chats 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.
  2. Vitest harness: 23 tests covering routes + resolveProjectPath. Pinned to v3 (Vite 5 / vitest 4 incompatibility).
  3. Error-log surfacing: dead-code removal from earlier H1/H2 audit items, structured error logs to client.
  4. Mount scoping: /opt:/opt:ro + BOOTSTRAP_ROOT writable subdir. Container loses write to /opt proper.
  5. Persistent context-window tracker: floating popover above chat input right edge, source = latest message_complete frames ctx_used / ctx_max, color-coded (neutral <60%, amber 6085%, 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).
  2. feat(mobile): viewport hook + sidebar drawer + hamburger headers (M1 + M2 + M6-header).
  3. feat(mobile): single-pane stack + long-press tab menu + swipe-to-close (M3 + M4 + A2).
  4. feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety (M5 + M6-bottom + M7 + M8).
  5. 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=<paneId>. 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).
  2. 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.
  3. Fix agent SSH key path so future Claude Code dispatches dont fall back to in-repo keys.
  4. Stale code audit: pruning unused exports, dead WS frames (e.g. session_renamed server publisher TODO from Batch 1), backup .bak files, unused imports.
  5. Overengineering audit: places where hand-rolled patterns are more complex than necessary, places where singleton hooks should consolidate (useSessionStream refcount).
  6. PATCH /api/panes/:id session-ownership check tightening.
  7. /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-<session_id>), per-pane tmux window (term-<pane_id>).
  • 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<Listener>.
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.
  2. Dispatch prompt via Paseo (Claude Code runs at /opt/boocode).
  3. Claude Code recon → blocking questions → implement → hand back.
  4. Review hand-back in separate Claude chat (spec compliance, code quality, drift, stale code).
  5. Deploy: docker compose up --build -d.
  6. Smoke test per the hand-backs plan.
  7. Sam commits manually, pushes to Gitea, merges to main.
  8. Next version.

Sam reviews all diffs. Sam commits. Never git pull/push/commit on his behalf.