- stream-phase.ts: streamCompletion, executeStreamPhase (plus sseLines,
StreamOptions, ChatCompletionDelta/Chunk as private helpers)
- tool-phase.ts: executeToolPhase + private executeToolCall
- types.ts: shared StreamPhaseState + DB_FLUSH_INTERVAL_MS so the
summary functions still in inference.ts can reference them without
pulling from a phase file
Cycle: executeToolPhase recurses into runAssistantTurn, which stays in
inference.ts. Resolved by direct value back-edge — tool-phase.ts does
`import { runAssistantTurn } from '../inference.js'` and runAssistantTurn
is now exported. Safe because the dereference happens inside an async
function body, after both modules have fully evaluated. No
callback-through-args fallback needed.
inference.ts shrinks from ~1401 to ~828 LoC. Final Dispatch D moves the
sentinel summaries out and renames the residue to inference/turn.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- payload.ts: buildMessagesPayload (re-exported), loadContext,
maybeFlagForCompaction
- error-handler.ts: handleAbortOrError, finalizeCompletion
Both new files type-import InferenceContext/StreamResult/TurnArgs from
inference.ts; ESM elides type imports so there's no runtime cycle.
handleAbortOrError turned out not to call the summary functions, so
no back-edge needed.
inference.ts shrinks from ~1676 to ~1401 LoC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure file moves. No behavior change. inference.ts retains createInferenceRunner
public surface; new files are internal to services/inference/.
- budget.ts: resolveToolBudget
- sentinels.ts: detectDoomLoop (re-exported through inference.ts),
isCapHitSentinel, isDoomLoopSentinel, isAnySentinel
- xml-parser.ts: parseXmlToolCall, partialXmlOpenerStart
First of four refactor batches preparing inference.ts for the v1.13
AI SDK migration. inference.ts goes from 1780 LoC to ~1620.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an assistant message sits status='streaming' with no token activity
for 60+ seconds, the chat shows a banner above the input offering Retry
or Discard. Both clear the stale row via a new backend endpoint
POST /api/chats/:id/discard_stale that updates status='failed' and
publishes chat_status='idle'.
Closes the UX gap that caused the 2026-05-21 debugging spiral —
slow streams and dead streams now look different to the user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatThroughput renders inline beside StatusDot while streaming or
tool_running. Subscribes to existing usage frames via sessionEvents.
Hides when status drops to idle/error or data is older than 10s.
Addresses the 2026-05-21 spike's UX gap where slow streams looked
identical to dead streams — now there's a live token velocity readout
that immediately distinguishes the two.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- handleAbortOrError now writes status='cancelled' on user stop; rows
no longer stuck 'streaming' forever
- Drop stale messages_status_check constraint (only messages_status_chk
remains, allowing 'cancelled' via TS MESSAGE_STATUSES)
- Remove detectSameNameLoop and DOOM_LOOP_SAME_NAME_THRESHOLD (added
during 2026-05-21 debugging spike, never fired in any real run,
existing detectDoomLoop covers actual failure modes)
- Remove 12 ctx.log.info diagnostic markers added during the same
spike (verbose for production)
- Bundles workspace pane sync + status indicator overhaul +
startup hung-row sweep landed earlier in v1.12.1 work
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status indicator (StatusDot): drops the flat amber pulse for a richer set
of states — orbiting amber for streaming, spinning sky ring for tool_running,
static violet for waiting_for_input, plus the existing idle/error. Backend
chat_status frame widens from 'working|idle|error' to discriminate streaming
vs tool execution vs paused for user input.
Workspace pane sync: pane layout moves from per-device localStorage to
server-side sessions.workspace_panes jsonb. PATCH /api/sessions/:id/workspace
broadcasts session_workspace_updated on the user channel for cross-device live
sync. Echo dedup via JSON comparison so the round-trip frame doesn't loop.
Legacy localStorage seeds the server on first hydrate, then is deleted.
Deprecated session_panes table dropped.
Resilience: startup sweep marks any stale 'streaming' message older than
5 minutes as 'failed' so v1.12.0-style hung rows clear on container restart.
useWorkspacePanes gains validatePanes() to prune dead chatId references from
saved pane state when the chat list lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add detectSameNameLoop (threshold 5) to catch over-verification hangs
where tool args vary but the model is stuck on one tool. Add 12 structured
log points across the inference state machine (runAssistantTurn,
executeToolPhase, runDoomLoopSummary) to diagnose the deterministic hang
surfaced in v1.12.0 smoke testing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed /opt/boocode/AGENTS.md (per-project override) — the project's
agents now resolve from the global /data/AGENTS.md only. Eliminates the
two-files-must-stay-in-sync footgun that surfaced during B.3
verification.
Fix: agents.ts ALL_TOOL_NAMES was a hardcoded 9-item whitelist that
silently filtered any unknown tool name from agent.tools arrays. This
caused web_search/web_fetch (v1.11.8) and the 8 codecontext tools to be
dropped at parse time. Replaced with ALL_TOOLS.map(t => t.name) for
single source of truth. Pre-existing exposure was dormant since no
builtin agent listed web_search; surfaced by adding codecontext.
Adds two new tools registered through the existing ALL_TOOLS registry:
- web_search hits SearXNG's JSON API (Fathom, internal Tailscale URL,
no auth) and returns top results
- web_fetch retrieves a URL's text content, gated by isPublicUrl
(url_guard.ts) which blocks loopback / RFC1918 / Tailscale CGNAT /
link-local / .local / .internal / non-http schemes
Both tools are opt-in via the existing session.web_search_enabled flag
(plumbed in v1.9, activated here). Default off. UI labels updated to
"Enable web search and fetch" / "Web search and fetch" since fetch joins
the same store. Counts against the v1.8.2 per-turn budget; covered by
the v1.11.6 doom-loop guard.
Native Node 20 fetch — no new prod dep. HTML stripping via regex (script
and style content elided wholesale). 5MB body cap, 15s fetch timeout,
8000-char default output, 32000-char cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports continue.dev's DEFAULT_SECURITY_IGNORE_FILETYPES + ignored-dir lists
into apps/server/src/services/secret_guard.ts plus a small BooCode
additions block (id_rsa*, *credentials*, .netrc, *.kdbx). Tiny glob-to-
regex matcher; no new prod dep.
view_file hard-refuses via SecretBlockedError. list_dir / grep /
find_files filter their results and surface a pathguard_note string
field with the hidden count — never list the offending paths back.
Named secret_guard.ts (not safety/pathGuard.ts) to avoid collision with
the existing path_guard.ts which already exports a pathGuard() function.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was picking the alphabetically-first agent from AGENTS.md ("Code
Reviewer") which felt presumptuous. New sessions now create with
agent_id=null; user picks from the AgentPicker if they want one.
Removes resolveDefaultAgent helper + the getAgentsForProject import
since this was the only caller. The project SELECT no longer needs
the path column either.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ContextBar relocated from a dedicated row above MessageList to inline with
the agent-picker row, filling the space to the right of the picker + plus
button. Always-visible (zero-state when no assistant message has run yet)
via chat.model_context_limit, which GET /api/sessions/:id/chats now
populates from a single getModelContext lookup per session.
ChatContextPopover above the input is removed entirely along with its
useChatContextStats hook (no remaining callers). Color tiers and the
auto-compaction threshold tooltip unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- llama-server does not emit n_ctx in timings (confirmed empirically);
dead code at inference.ts:479 and compaction.ts:300 never fired
- New model-context.ts: cached fetch of /upstream/<model>/props
with positive-cache (no TTL) and 60s negative-cache
- Wired into all 4 ctx_max write sites: 3 in inference.ts
(executeToolPhase, finalizeCompletion, runCapHitSummary) and
1 in compaction.ts (summary row INSERT)
- AbortController 3s timeout, lenient parsing with sensible defaults
- 12 new vitest cases for the cache module (59 total)
- 7 historical assistant rows backfilled manually (see notes)
Some local models (qwen3-coder via llama-swap) emit tool calls as inline XML
inside delta.content rather than structured delta.tool_calls. streamCompletion
now buffers delta.content, extracts complete <tool_call>...</tool_call> blocks
via parseXmlToolCall, and pushes synthetic entries (id prefix xml_call_) into
the existing toolCallsBuffer. Native JSON path unchanged — both coexist.
Partial openers are held back so a tool tag never leaks to the chat mid-tag.
Unclosed XML at end-of-stream is flushed as plain content (no silent drops).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /data/skills mount (host: /opt/skills)
- skill_find, skill_use, skill_resource added to default read-only
tool set; opt-in for agents with explicit tools: whitelist
- AGENTS.md builtin agents drop explicit tools: arrays to inherit
the new default (now includes skill tools)
- POST /api/chats/:id/skill_invoke for slash-command flow
- 19 SKILL.md files seeded at /opt/skills/ across 6 source groups
Adds a singleton, ephemeral 'settings' pane kind to the workspace.
Opened via a new bottom-pinned button in ProjectSidebar (emits an
open_settings_pane event when a session is mounted; navigates to
/settings otherwise). Pane has three sections — Session, Project,
Theme — and a maximize toggle that hides sibling pane columns via
display:none on desktop only. Settings panes don't count toward
MAX_PANES and are filtered out of the localStorage persistence layer
so reload always restores a clean workspace.
Schema (additive):
- projects.default_system_prompt TEXT NOT NULL DEFAULT ''
- projects.default_web_search_enabled BOOLEAN NOT NULL DEFAULT false
- sessions.web_search_enabled BOOLEAN (nullable; null = inherit)
Inference resolves user_prompt = session.system_prompt.trim() ||
project.default_system_prompt.trim() — empty/whitespace at either
layer means "no override". Keeps the columns NOT NULL and matches
the existing inherit semantics.
Server routes:
- GET /api/projects/:id (new; settings pane refetches on
project_updated)
- PATCH /api/projects/:id accepts default_system_prompt,
default_web_search_enabled
- PATCH /api/sessions/:id accepts web_search_enabled (tri-state)
- POST /api/projects/:id/sessions/archive-all + GET
/api/projects/:id/sessions/open-count
- POST /api/sessions/:id/chats/archive-all + GET
/api/sessions/:id/chats/open-count
- PATCH /api/sessions/:id now broadcasts session_updated on every
successful PATCH (was rename-only). Lets SettingsPane open in
another tab pick up edits without a refetch.
Bulk-archive publishes one session_archived / chat_archived frame
per affected id so useSidebar's existing reducer cases handle them
incrementally — no new frame type, no payload widening.
ModelPicker refactored: shared ModelList inside a responsive shell.
Desktop = labeled trigger + DropdownMenu, mobile = icon-only Cpu
button + BottomSheet. Header in Session.tsx drops the pill wrap on
mobile since the new trigger is the visual.
ChatInput gains an icon-only '+' DropdownMenu next to AgentPicker
when sessionId + webSearchEnabled props are provided. One item for
now — Web search — with a checkmark reflecting the stored value
(true), not the effective one. Click PATCHes the override; to
restore inherit-from-project the user opens SettingsPane.
ThemePicker lifted out of pages/Settings.tsx into a reusable
component. The standalone /settings route is now a thin wrapper
that mounts <ThemePicker /> with a Back button on top
(navigate(-1) with fallback to '/'); the SettingsPane Theme tab
renders the same picker bare.
Project section delete-flow removed (button + confirm dialog +
handler). Replaced with "Archive all sessions" using the same
two-step count → confirm → fire pattern as "Archive all chats" in
the Session section. api.projects.remove() stays in the client
because useProjects.ts still uses it.
Hand-rolled Switch primitive in SettingsPane (no shadcn switch in
the project; spec said no new deps). Section nav is plain buttons
(no shadcn Tabs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 18 preset themes (16 dual-mode + 2 light-only) selectable from
a new /settings route. Persists per-user via the existing key-value
settings table — no schema refactor. Default on first load is
obsidian dark.
Storage: two new seeded keys (theme_id, theme_mode) inserted
idempotently from schema.sql. PATCH /api/settings tightens validation
with a discriminated branch — theme_id must be one of the 18
whitelisted ids, theme_mode ∈ {dark,light,system}, anything else
rejects 400. Other keys pass through the loose record schema.
CSS layer: 18 files in apps/web/src/styles/themes/, each declaring
.theme-<id> (light) and .theme-<id>.dark (dark) — except ivory and
chalk which are light-only. Anchor-to-token mapping per spec §3.
--destructive stays red across all themes. --radius unchanged at
0.625rem (spec parenthetical was about "not per-theme", not a
specific value swap).
Frontend: lib/theme.ts owns THEMES, applyTheme(), setTheme(), and
useTheme() — module-singleton with optimistic PATCH + revert on
failure (mirrors useChatStatus / useSidebar pattern). Settings.tsx
renders a 3-col (md) / 2-col (mobile) grid of shadcn Card swatches
with a Dark/Light/System radio group on top. App.tsx mounts
useTheme() at AppShell top and wires the /settings route.
index.html ships a pre-React FOUC script that reads localStorage
'boocode.theme' and stamps the className on <html> before any
paint. Stripped two pre-existing dark-mode lock-ins (AppShell's
hardcoded 'dark' className and body's neutral-950/100 tailwind
utilities) that would have fought theme tokens.
Light-only + dark request → falls back to obsidian dark in three
places: lib/theme.ts effectiveThemeId(), the FOUC script, and the
picker's "Light only" badge. No inline message; matches spec §8
decision 1.
shadcn primitives card and radio-group installed via shadcn CLI
(no hand-rolling). card.tsx and radio-group.tsx are the only ui/
additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old hardcoded MAX_TOOL_LOOP_DEPTH=15 replaced by per-agent
max_tool_calls (1-100, AGENTS.md frontmatter) with defaults: 30 for
read-only-only agents, 10 for agents that include any non-read-only
tool, 15 for raw chat. When the loop hits cap, fire one final summary
call with tools disabled, stream the wrap-up into the in-flight
assistant message, then insert a system sentinel with
metadata.kind='cap_hit'. The sentinel renders an amber bubble with a
Continue button (latest sentinel only) that POSTs to a new
/api/chats/:id/continue route to extend. Hard ceiling: 3 cap-hits per
chat (2 continues max) — third sentinel reports can_continue=false.
Error frames carry a machine-readable reason code alongside human
error text. Failed messages persist the reason via
metadata.kind='error' so the bubble renders specifics on reload (WS
error frame is one-shot).
Tool call UI rewired: ToolCallLine renders inline (↳ name args
spinner/check/✗, expand-on-tap for args+result); ToolCallGroup
collapses 3+ consecutive same-tool runs into a compact card.
MessageList owns a three-pass pre-render (flatten + fold tool
results onto matching runs by id + group same-tool runs + number
sentinels). MessageBubble drops tool rendering and adds the
sentinel / error-reason branches. ToolCallCard deleted.
Roadmap follow-up logged: add explicit max_tool_calls: 30 to the 6
agents in /data/AGENTS.md and /opt/boocode/AGENTS.md post-ship for
discoverability (defaults handle behavior identically).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builtins move out of code into /data/AGENTS.md (always-on, mounted ro
into the container); per-project AGENTS.md is now an optional override.
agents.ts merges global + project entries with project-wins-by-name and
caches per-source mtimes (60s TTL). Parser switches to per-block
try/catch and returns AgentsResponse { agents, errors[] } so one
malformed block no longer fails the file. AgentPicker shows a
non-blocking amber chip listing skipped blocks and only fires a gray
toast when zero agents loaded.
WS reconnect UX (useUserEvents + useSessionStream) now silent on the
first disconnect; createWsReconnectToast escalates to gray after 3
failures or 15 s, then to red with a Retry Now action after 60 s.
useSessionStream also gained the exponential-backoff reconnect it was
missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile header is now two rows. Row 1: hamburger | project · branch
indicator (live via GET /api/projects/:id/git, 30s poll) | ModelPicker |
FolderTree. Row 2: pane-switcher pill (hand-rolled BottomSheet) +
NewPaneMenu. Chat-within-pane navigation hidden on mobile; users switch
panes via the sheet. Cross-tab status sync via chat_status frames
published from inference.ts at working/idle/error transitions; StatusDot
component renders amber-pulse/green/red/gray on each pane row and on
desktop ChatTabBar tabs. Level 1 git awareness exposes a read-only
git_status tool to the model, backed by services/git_meta.ts (execFile
+ 2s timeout + 30s cache). Workspace.tsx now receives panes/chats hooks
as props (hoisted into Session.tsx) so the header pill shares state
with the pane grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six builtin defaults (Code Reviewer, Debugger, Refactorer, Architect,
Security Auditor, Prompt Builder) with no model field so session.model
wins. Project root AGENTS.md parsed on demand with mtime cache; when
present, only its agents are shown. sessions.agent_id resolves per turn
into effective system prompt, temperature, and a tool whitelist applied
in inference. AgentPicker mounts in the ChatInput toolbar; SettingsDrawer
agent surface deferred to Batch 7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a chat is auto-named, also rename the parent session if it is
still on its default 'New session' label. UPDATE is gated by an
atomic WHERE clause so user renames and prior propagations are not
clobbered. Publishes session_renamed via broker.publishUser; useSidebar
already listens.
Closes the gap where sessions auto-created from the sidebar would
stay 'New session' forever.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.4 publisher fired whenever the PATCH body included `name`,
including no-op rename calls (PATCH { name } where name ===
currentName). Read the prior name with a fast SELECT before the
UPDATE and only publish session_renamed when the post-update name
actually differs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows assistant turns up to 15 tool calls in a single chain before
the loop-depth guard trips. Real chats commonly need 6-10 tool calls
(grep -> view_file -> view_file -> grep -> view_file -> answer); the
old cap of 5 was firing on legitimate investigation patterns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The scope check at routes/projects.ts:56 short-circuited when
real === whitelistReal, allowing the whitelist directory itself to
resolve as a valid project root. Dropped the `real !== whitelistReal`
half of the && so the predicate becomes the strict prefix check.
Flipped the unit test from a "BEHAVIOR GAP" assertion (documenting
the bug) to a strict-rejection assertion. 23/23 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Dockerfile: install git + openssh-client in runtime image; pre-populate
/root/.ssh/known_hosts with the Tailscale ssh-keyscan for
100.114.205.53:2222 (Gitea SSH). Without these, the bootstrap push
step from inside the container fails with "command not found" or
host-key prompts.
- docker-compose.yml: mount ./secrets/boocode_gitea as
/root/.ssh/id_ed25519:ro so the container can authenticate to Gitea
over SSH for the initial push.
- .gitignore: add secrets/ so the keypair never lands in the repo.
- project_bootstrap.ts: rewrite the Gitea-returned ssh_url's hostname
from git.indifferentketchup.com to 100.114.205.53 before adding it
as origin, so the push hits the Tailscale interface that the
known_hosts entry covers.
- CreateProjectModal.tsx: preview label now reads "Folder:
/opt/projects/<name>" to match the new BOOTSTRAP_ROOT (was /opt/).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the previous /opt:/opt:rw bind into two mounts to narrow the
writable scope of the container:
- /opt:/opt:ro — read-only mount for legacy/existing project
add-existing flow. resolveProjectPath still uses
PROJECT_ROOT_WHITELIST (/opt by default) so existing projects under
/opt/<name> (analytics, boolab, boocode itself) continue to resolve
and serve their file-tree via the read-only tools.
- /opt/projects:/opt/projects:rw — writable mount targeted at the
create-new-project bootstrap path.
Picked Option B from the spec (simpler than two scan roots):
PROJECT_ROOT_WHITELIST stays /opt, new BOOTSTRAP_ROOT env var defaults
to /opt/projects and is used by project_bootstrap.ts as the mkdir
target. Bootstrap path-escape check now compares against
BOOTSTRAP_ROOT.
Prereq: host must `mkdir -p /opt/projects` before next container
restart. Documented in CLAUDE.md and .env.example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swallowed-error logging (audit Feature 3):
- file_index.ts:36-37 (git mtime probes): comment — best-effort, project
may not be a git repo.
- useUserEvents.ts:44 / 53 (ws.close on error / unmount): comments —
best-effort, socket may already be closing.
- RightRail.tsx:38 (localStorage write): comment — best-effort, quota or
private mode.
- App.tsx:21 (api.sessions.get for RightRail projectId): replaced silent
catch with console.warn.
- Session.tsx:38, 41 (session fetch + project list for breadcrumb):
replaced silent catches with console.warn.
H1: ProjectSidebar.tsx:189 — dropped the local sessionEvents.emit
({type:'session_renamed'}) after PATCH. Server publishes via
broker.publishUser since v1.4; useUserEvents forwards.
H2: useSessionStream.ts session_renamed case removed (dead — no
server code path publishes session_renamed on the per-session WS
channel; only user channel via broker.publishUser). Also dropped the
session_renamed variant from WsFrame (in apps/web/src/api/types.ts)
to keep the discriminated-union switch exhaustive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds vitest 3.x (pinned to ^3 because vitest 4 requires Vite 6, while the
web app pins Vite 5). Tests live under src/**/__tests__/**.
Three target functions:
- sanitizeFolderName (project_bootstrap.ts): 8 cases covering happy path,
path-traversal stripping, empty-after-sanitize, control chars, truncation
at 64, null bytes, leading/trailing dot/slash stripping.
- resolveProjectPath (projects.ts): 7 cases including symlink-escape via
realpath, outside-whitelist rejection, nonexistent path, AND a flagged
BEHAVIOR GAP: passing the whitelist path itself currently returns success
rather than erroring out (function early-exits the scope check when
real === whitelistReal). Test asserts current behavior with explicit
comment flagging the spec violation — function NOT silently patched.
Function made exportable for testing (single keyword change).
- buildMessagesPayload (inference.ts): 8 cases for compact-marker logic
(no marker, marker present, multiple compacts, tool-message position).
tsconfig.json excludes __tests__ + *.test.ts from emit so dist/ stays clean.
pnpm -C apps/server test => 23 passed in ~340ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fork: POST /api/chats/:id/fork creates a new chat in the same session,
copies messages up to target (status=complete) with row-offset
clock_timestamp() for stable ordering. Client emits open_chat_in_active_pane
event; Workspace opens it in the active pane. No maybeAutoNameChat on forks.
- Delete: DELETE /api/chats/:id/messages/:message_id with 409 if the chat is
currently streaming. Cascading-forward delete (created_at >= target).
MessageBubble Trash button + confirm Dialog.
- Header: Projects -> Project -> Session breadcrumb, model badge pill,
inline session rename, active file path via new useActivePane() hook.
Server now publishes session_renamed on PATCH /api/sessions/:id;
client-side dup emit removed from Session.tsx.
- Housekeeping: NOW() -> clock_timestamp() in schema.sql defaults, dead
PaneTab.tsx and panes/PaneShell.tsx removed, session_panes backfill
INSERT removed (CREATE TABLE retained), Tailnet trust comment near
app.listen().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Feature 1 — Tab close menu (pure local pane state, no API):
- ChatTabBar context menu: Rename / sep / Close / Close others / Close to right / Close all
- Workspace bulk-tab primitives: closeOtherTabs, closeTabsToRight, closeAllTabs (manipulate panes[].chatIds, no fetch)
- Drop in-bar Delete; landing card's name-typed Delete is the canonical destructive path
Feature 2 — Chat archive + delete:
- chats.status vocabulary aligned with projects ('open' | 'archived'); DROP old inline CHECK, UPDATE 'closed' → 'archived', ADD new named chats_status_chk
- POST /api/chats/:id/archive (204) + POST /api/chats/:id/unarchive (200) + GET /api/sessions/:id/chats?status=archived; DELETE publishes chat_deleted; PATCH simplified to name-only
- 3 new WS frames: chat_archived, chat_unarchived, chat_deleted (renamed from chat_closed)
- Same dedup discipline: server-only publish, no local sessionEvents.emit in client
- SessionLandingPage: right-click ContextMenu (Open / Rename / Archive / sep / Delete-destructive), inline rename, archive confirm dialog, delete dialog with name-typed Input gated until typed text === chat.name, Archived chats collapsible section with Restore
- Card-level Archive + Delete icon buttons reusing the same dialog state setters; stopPropagation on both so card click still opens the chat; archived cards keep only Restore
UX — chat content width cap:
- ChatPane content (MessageList, queue chips, stop button, ChatInput) wrapped in inner max-w-[1000px] mx-auto w-full so messages center; outer border-t / scroll containers stay full-width so pane chrome and backgrounds remain edge-to-edge
- No new deps, no media queries (narrow viewports collapse to width naturally)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server:
- projects.status + projects.gitea_remote (additive) with CHECK ('open','archived')
- GET /api/projects?status=archived; PATCH /api/projects/:id (rename);
POST /api/projects/:id/archive | unarchive; POST /api/projects/create
- POST /api/projects ON CONFLICT (path) DO UPDATE SET status='open': re-add
of archived path restores existing row (preserves id + FKs); already-open
path returns 409. Detected-repos picker now excludes only status='open'.
- New gitea.ts (createGiteaRepo + GiteaRepoExistsError) and
project_bootstrap.ts (sanitize name, mkdir under PROJECT_ROOT_WHITELIST,
git init -b main + first commit with -c user.name/email per-command, optional
Gitea repo create + remote add + push; all via execFile, no shell).
- 3 new user-stream frames: project_archived, project_unarchived, project_updated.
- sidebar.ts now selects path + gitea_remote and filters status='open'.
- Gitea env added to config.ts (GITEA_BASE_URL, GITEA_USER, GITEA_TOKEN,
GITEA_SSH_HOST).
- docker-compose.yml /opt mount flipped to rw so create-project can mkdir.
- auto_name.ts gate relaxed from `!== 1` to `< 1` (fires on every turn while
chat name is empty, not only the first).
Web:
- ProjectSidebar: project rows use proper Radix ContextMenu; items Rename /
Archive / Open in Gitea. Inline rename, archive confirm dialog.
Removed obsolete handleRemove + DropdownMenu hack.
- Home: Add-existing + Create-new buttons; collapsible Archived Projects
section with Restore.
- New CreateProjectModal: name + live folder preview, commit msg, Private/
Public radio, create-Gitea-remote checkbox, toast on success/warnings.
- New projectUrls.ts giteaUrlFor() — uses gitea_remote when present,
falls back to convention URL.
- 3 new event types in sessionEvents.ts with idempotent useSidebar handlers.
- SidebarProject extended with path + gitea_remote so Open-in-Gitea can
resolve without a separate fetch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fastify global empty-JSON-body parser fixes archive/unarchive/stop 400s
- Removed redundant local sessionEvents.emit at all 5+2 sites with server-side WS publishers; added dedupe guards in useSidebar/Workspace/Project handlers
- Sidebar session right-click adds Delete (destructive) with confirm Dialog
- Session.tsx navigates away on session_deleted/session_archived for the active session
- SessionLandingPage chat rows show message_count, effective_context_tokens, last_message_preview via LATERAL joins on GET /api/sessions/:id/chats
- Workspace.tsx pane drag-to-reorder using native HTML5 events (no new deps)
- CompactCard: Copy toast, Send-to-chat with target chat name, empty-state in share popover, Re-run button
- auto_name.ts: filter count gate and assistant-fetch by content <> '' so tool-call assistant rows don't trip the once-and-only-once guard
- Adds CLAUDE.md and apps/web/src/lib/format.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session 1:N Chat data model with backfill. Workspace switches to client-side
multi-tab pane management. Right-rail file browser with float-over viewer and
click-drag line selection replaces FileBrowserPane. Adds /compact streaming
summarizer (respects compact markers in context builder), force-send (cancels
in-flight, persists partial as 'cancelled', awaits cancellation completion via
deferred Promise + 5s timeout), message queue, stop generation, chat
auto-rename, session archive/unarchive with Closed Sessions section on repo
landing page. CHECK constraints on sessions.status, messages.role,
messages.status with KEEP IN SYNC comments tying to MESSAGE_ROLES /
MESSAGE_STATUSES const arrays. Deletes dead pane routes/hook and the
api.panes.* client block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sessions created before Batch 3 have no rows in session_panes, so the
Workspace renders "No panes" on first open. Idempotent INSERT inserts
a default chat pane at position 0 for any session without one. NOT
EXISTS guard makes the statement a no-op after the first run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop unused Stats type import and its no-op suppression expression
- Comment getProjectFiles concurrent-miss race (benign, accepted)
- Comment findFiles deliberate post-limit counting (differs from grep)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>