Update version summary: v1.6-mobile-pass and v1.6.1-cleanup are now merged with SHAs; v1.6.2-mobile-ui-fixes added as in-flight with its 4-commit plan. v1.6.1-cleanup details rewritten to reflect what actually shipped (B1) vs what was audited-only (secrets, panes, unused exports, hand-rolled patterns, mount scope, etc.). Closed two open items: session_renamed has a server publisher since v1.4; PATCH /api/panes/:id is moot (endpoint never re-introduced). Dependency graph updated with v1.6.2 node between v1.6.1 and v1.7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
516 lines
28 KiB
Markdown
516 lines
28 KiB
Markdown
# 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|✅ Merged |`57c883b..943ae7d` (6 commits) |
|
||
|v1.6.1-cleanup |Mostly audit-only; one fix shipped: RightRail `max-md:hidden` wrapper. Audit reports for secrets, stale code, panes, mount scope, hand-rolled patterns deferred to follow-ups |✅ Merged |`6a9fe18` |
|
||
|v1.6.2-mobile-ui-fixes|Mobile UI polish from device testing: kill single-pane navigator chrome, header rework, “New chat” in long-press menu, RightRail as mobile drawer (reverts v1.6.1 wrapper) |🔄 Hand-back received, uncommitted|— |
|
||
|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` (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 ✅
|
||
|
||
**Merged via 6 commits `57c883b..943ae7d`** (5 functional + 1 docs):
|
||
|
||
1. `57c883b chore: fix resolveProjectPath whitelist-root bypass` (H1 — dropped `real !== whitelistReal` short-circuit; flipped the v1.5 BEHAVIOR GAP test; 23/23 pass).
|
||
1. `a643b5f feat(mobile): viewport hook + sidebar drawer + hamburger headers` (M1 + M2 + M6-header).
|
||
1. `cd897d6 feat(mobile): single-pane stack + long-press tab menu + swipe-to-close` (M3 + M4 + A2).
|
||
1. `273eeac feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety` (M5 + M6-bottom + M7 + M8).
|
||
1. `4b5b9b2 feat(mobile): pull-to-refresh sidebar list` (A1).
|
||
1. `943ae7d docs: add v1.x roadmap snapshot` (this file).
|
||
|
||
**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 ✅ (`6a9fe18`)
|
||
|
||
**Shipped:** RightRail wrapped in `<div className="max-md:hidden contents">` so it's hidden entirely below the md breakpoint on mobile. (Note: v1.6.2 reverses this and replaces with a proper mobile drawer — see below.)
|
||
|
||
**Audited but not shipped (queued for follow-ups):**
|
||
|
||
- **Secrets hygiene:** `secrets/boocode_gitea` is NOT tracked; never committed to any branch; `.gitignore` already covers `secrets/`. Rotation is a Gitea-side action, no repo change needed.
|
||
- **`.bak` files:** 3 leftover from v1.5.1 (`docker-compose.yml.bak-20260516`, `Dockerfile.bak-20260516`, `apps/web/src/components/CreateProjectModal.tsx.bak-20260516`). Git-invisible via global `~/.gitignore_global` (`*.bak*`). Decide per file.
|
||
- **Unused exports:** neither `knip` nor `ts-prune` installed. Proposal pending.
|
||
- **Dead WS frames:** `session_renamed` HAS a server publisher (`routes/sessions.ts:140`, added in v1.4) — the roadmap's "no server publisher" open item is **STALE**, crossed off. The `InferenceFrame` union still declares `session_renamed` as a type variant but no code publishes it on the per-session channel; trivial 1-line cleanup deferred.
|
||
- **Unused imports:** web `tsc --noUnusedLocals --noUnusedParameters` returns 0 warnings.
|
||
- **`useSessionStream` refcount:** opportunity confirmed (~90 lines diff to apply the `useSidebar`-style module-scope singleton pattern). Risk LOW. Queued for v1.6.2 or later.
|
||
- **PATCH `/api/panes/:id` ownership:** **MOOT** — endpoint does not exist (the pane REST API was never re-introduced after pane state moved to client-side localStorage in v1.2). Crossed off open items.
|
||
- **Hand-rolled patterns vs library:** 5 hand-rolled hooks/components total 336 lines. None duplicates anything in existing deps; library swap (`@use-gesture`, `react-pull-to-refresh`) not worth the dep cost yet.
|
||
- **`/opt:/opt:ro` mount tightening:** Two-option plan documented for v1.6.2 — Option A (per-project bind-mounts) or Option B (deny `.env` pattern in `pathGuard`). Option B is the simpler short-term fix.
|
||
|
||
-----
|
||
|
||
### v1.6.2-mobile-ui-fixes 🔄
|
||
|
||
**Hand-back received, uncommitted on `v1.6.2-mobile-ui-fixes`.** 4-commit sequence proposed:
|
||
|
||
1. `fix(mobile): hide Split button + single-pane navigator chrome` (G1 — wrap the Workspace Split row in `!isMobile`).
|
||
1. `feat(mobile): rework Session and Project headers for narrow viewports` (G2 — breadcrumb `hidden sm:flex`, session name cap `max-w-[140px] sm:max-w-[280px]`, project page heading `text-base sm:text-lg`, “New session” icon-only on mobile).
|
||
1. `feat(mobile): add "New chat" to tab long-press context menu` (G3 — top of menu, separator, then existing items).
|
||
1. `feat(mobile): right-rail as drawer on mobile, header toggle button` (G4 option b — new `useRightRailDrawer` Context hook, `RightRail` renders as fixed `w-[85vw] max-w-sm` drawer on mobile, FolderTree button in Session header, **reverts v1.6.1's `max-md:hidden` wrapper**).
|
||
|
||
**Decisions:**
|
||
|
||
- G4 option b chosen: mobile file browsing IS useful; drawer pattern mirrors `useSidebarDrawer`.
|
||
- G2 single-row session-name+model layout (model picker right-aligned), per spec example.
|
||
- G3 "New chat" at top, separator, then Rename.
|
||
- G2 "New session" button: icon-only on mobile via `<span className="hidden sm:inline">New session</span>`.
|
||
|
||
**Adjacent uncommitted change (not part of v1.6.2):** `MAX_TOOL_LOOP_DEPTH 5 → 15` in `apps/server/src/services/inference.ts`. Sam-authored, sitting in working tree on `v1.6.2-mobile-ui-fixes`. **NOT on main as of this update.** Commit separately.
|
||
|
||
-----
|
||
|
||
### 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. Audited in v1.6.1, queued.
|
||
- **`/opt:/opt:ro` mount exposes all `.env` files.** Whitelist scope before BooCoder. Two-option plan documented in v1.6.1 audit; ship in v1.6.2 or v1.7.
|
||
- **`secrets/boocode_gitea` in repo working tree.** Never committed (git-invisible via global ignore). Rotate the Gitea-side key when convenient; no repo action required.
|
||
- **Dormant in-boolab BooCode mode.** Reference only.
|
||
- **BooCoder container.** Post-v1.x.
|
||
|
||
**Closed since last update:**
|
||
|
||
- ~~`session_renamed` no server WS publisher~~ — server publishes via `broker.publishUser` from `routes/sessions.ts:140` (added in v1.4). Confirmed in v1.6.1 audit.
|
||
- ~~PATCH `/api/panes/:id` lacks session-ownership check~~ — endpoint does not exist; the pane REST API was never re-introduced after v1.2 moved pane state to localStorage.
|
||
|
||
-----
|
||
|
||
## 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.6.2-mobile-ui-fixes ◄─────┘
|
||
│
|
||
▼
|
||
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.
|