Commit Graph

61 Commits

Author SHA1 Message Date
16c69a38a1 Merge v1.12 track B: codecontext sidecar
# Conflicts:
#	apps/web/src/components/ToolCallLine.tsx
#	docker-compose.yml
2026-05-21 15:12:30 +00:00
be3c38ff2f Merge v1.12 track A: container guidance + skills 2026-05-21 15:11:12 +00:00
a2e2481ef9 v1.12 track A: container guidance + skills 2026-05-21 15:11:04 +00:00
136e9538aa v1.12 track B.2: codecontext tool wrappers + tests 2026-05-21 13:35:44 +00:00
5cd3f63df5 mobile: add explicit close button to nav drawer 2026-05-21 04:06:35 +00:00
2fdbb05477 v1.11.8: web_search + web_fetch tools via SearXNG
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>
2026-05-20 21:38:02 +00:00
f92b0810c3 v1.11.6: doom-loop guard (3 identical tool calls aborts recursion) 2026-05-20 20:28:45 +00:00
1ffcf67c47 v1.11.5: ContextBar inline next to agent picker; remove ChatContextPopover
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>
2026-05-20 20:11:49 +00:00
8cd270a5da ContextBar: persistent context-usage indicator above MessageList
Walks chat messages newest-first for the latest ctx_used/ctx_max pair.
Color tiers fire against (max - 20k compaction reserve) so the bar warns
amber/orange/red at the same boundaries auto-compaction triggers.
"Context" → "Ctx" at <640px, (NN%) drops at <380px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:18:27 +00:00
dc43dd44f9 v1.11: opencode-style compaction port
- compaction.ts: usable/isOverflow/estimate/turns/select/buildPrompt/process
- compaction-prompt.ts: SUMMARY_TEMPLATE verbatim from opencode
- schema: messages.{compacted_at,summary,tail_start_id} + chats.needs_compaction
- inference: auto-trigger on overflow, pre-fetch compaction before next turn
- /compact slash command rewired to new path
- WS: chat_status working/idle around compaction + compacted frame
- frontend: SummaryCard + sonner toast on compacted
- 24 unit tests for pure functions
2026-05-20 19:05:35 +00:00
6aab4f7d2a ChatTabBar: + button dropdown to add chat / terminal / agent pane
Replaces single onNewChat handler with onAddPane(kind). Terminal pane
header gets matching + dropdown. Context menu "New chat" stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:13:55 +00:00
2d841ee0b4 handoff 2026-05-20 14:56:02 +00:00
3fceea064a booterm: fitFull() bypasses FitAddon scrollbar subtraction; push initial PTY size
FitAddon's proposeDimensions() always subtracts a phantom scrollbar width even
when CSS hides the scrollbar — losing one column of usable width. fitFull()
divides host clientWidth/clientHeight by the renderer's reported cell size
directly. Also POSTs the resized cols/rows back to /api/term/.../resize on
initial mount and after fonts.ready so bash/opencode get the correct PTY
size before the user types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:32:42 +00:00
ea9d261f0f v1.10.4: booterm mobile UX — copy/paste, swipe-close, send-to-chat, search
- Long-press selection + floating menu (mobile + desktop right-click): Copy,
  Paste, Select All, Search, Send to chat. Tap-outside / Esc dismiss.
- Pane-header Paste button (📋) for iOS user-gesture clipboard read.
- Swipe-left-to-close on mobile pane pill with red "Close" overlay and
  translateX visual hint; spring-back below 80px threshold.
- Send-to-chat reverse path: chatInputsRegistry + sendToChat event mirror
  the existing terminalsRegistry pattern. ChatInput appends with newline
  separator on receive and focuses (no auto-send).
- Scrollback search via xterm-addon-search@^0.13.0: SearchBar overlay with
  N-of-M match counter (onDidChangeResults), Enter/Shift-Enter cycling.
- Cmd/Ctrl+F intercept in Session.tsx when active pane is terminal; xterm
  also intercepts when focused. Browser native find passes through elsewhere.
- terminalsRegistry signature extended with openSearch + paste callbacks.

Includes deferred CLAUDE.md updates documenting v1.10/v1.10.1/v1.10.2/v1.10.3
learnings (uid 1000 collision, libc match, two event buses, vite proxy order,
mobile pane URL sync, xterm canvas selection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:16:47 +00:00
875db86e31 v1.10.3: booterm mobile/UX fixes + global keyboard shortcuts
Five issues + keyboard shortcuts across booterm and the workspace shell.

Auto-switch on create (mobile): addSplitPane now returns the new pane id;
Session.tsx wraps it with addPaneAndSwitch which pushes ?pane=<newId> on
mobile so the URL-sync effect doesn't fight the just-set activePaneIdx.
NewPaneMenu uses the wrapper; desktop Split dropdown is unaffected.

Tab-away reconnect: TerminalPane has a connect()/manualReconnect() state
machine. ws.onclose backs off 500ms/1s/2s × 3 attempts, then surfaces a
[Disconnected] banner with a Reconnect button. visibilitychange listener
calls manualReconnect when the tab returns and the WS isn't OPEN. tmux
session persists server-side so scrollback is intact on resume.

Copy/paste: attachCustomKeyEventHandler binds Cmd/Ctrl-C (copy if
selection, else send ^C), Cmd/Ctrl-Shift-C (always swallow — copy if any,
no-op otherwise — never sends ^C), Cmd/Ctrl-V and Cmd/Ctrl-Shift-V
(navigator.clipboard.readText → ws.send). No custom right-click menu —
browser's native menu is preserved.

Scroll: removed `set -g mouse on` from tmux.conf so xterm.js sees wheel
and touch events natively. scrollback: 10_000, fastScrollModifier: 'shift',
altClickMovesCursor: false. Container has touch-action: pan-y for mobile.

Right-edge gap: inline <style> overrides xterm's defaults to width:100%
height:100% and hides the scrollbar chrome. Host container is
flex-1 min-w-0 self-stretch w-full. Three refit triggers: ResizeObserver
(rAF-wrapped), document.fonts.ready, and useEffect on the new active prop.
Background color matched between outer div, inner div, and xterm theme.

Keyboard shortcuts in Session.tsx (window-level keydown):
  Cmd/Ctrl+`              focus active terminal, else jump to last
  Cmd/Ctrl+Shift+T        new terminal pane
  Cmd/Ctrl+Shift+C        new chat pane (defers to xterm copy if focused)
  Cmd/Ctrl+W              close active pane
  Cmd/Ctrl+Tab/Shift+Tab  cycle next / prev pane
  Cmd/Ctrl+1..9           jump to pane N
terminalsRegistry gains a focus() callback per registration so Cmd+`
can call term.focus() on the active terminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:52:44 +00:00
7486e7d3e0 v1.10: booterm container — xterm.js + tmux + node-pty 2026-05-18 14:06:46 +00:00
d85b17081e v1.9.7: ask_user_input elicitation tool 2026-05-18 02:15:18 +00:00
80fd3d9fa9 feat(web): /skill slash command with autocomplete
Trigger /<name>, dropdown lists all skills filtered by name prefix,
arg passthrough sends the rest as the user message. Synthetic
skill_use tool_use renders identically to model-invoked skills.
2026-05-18 01:10:51 +00:00
eaacd432e8 feat(web): skills API types + client methods 2026-05-18 01:10:51 +00:00
98b432ebce refactor: drop type-to-confirm gate on chat delete
The chat-delete dialog required typing the chat name to confirm
deletion. Single-user app — typing friction is annoying, not safety.
Match the archive dialog pattern in SettingsPane.tsx: title +
description naming the chat in mono font, plain Cancel + destructive
Delete button.

Removes the deleteInput state, deleteExpected / deleteEnabled
deriveds, the <Input> field, and its lone <Input> import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:50:30 +00:00
1ecccc112f fix: settings pane close affordance + sidebar toggle
The v1.9 settings pane had no way to dismiss once opened. ChatTabBar
(which owns the per-pane close X for chat panes) is skipped for
settings panes, and the pane header itself only rendered the maximize
toggle (desktop-only). Mobile users had zero controls beyond the
section tabs.

Add three close paths:

- X button in SettingsPane header, visible on mobile + desktop, sits
  next to the maximize toggle. Tap-target sized per the v1.6 mobile
  convention (max-md:min-h-[44px]).
- Esc when the settings pane is the active pane and no input/textarea/
  dialog has focus. Maximize-restore still wins when maximized.
- Sidebar Settings button is now a strict toggle: opens on first click,
  closes on second. Renamed openOrFocusSettingsPane →
  toggleSettingsPane in the panes hook.

Edge case: removing the settings pane when it's the only pane left
falls back to an empty pane to preserve the "always one pane"
invariant. In normal flow this is unreachable (the toggle only
appends), but defensive against future entry points.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:50:25 +00:00
09aecc4ee9 v1.9: settings pane + per-project defaults + bulk archive + themes lift
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>
2026-05-17 17:37:29 +00:00
9b174cdb5e themes-v1: 18 preset palettes + Settings picker
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>
2026-05-17 16:25:15 +00:00
5c61cc7281 v1.8.2: tool loop cap-hit summary + tool call UI compaction
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>
2026-05-17 10:31:32 +00:00
12d91c9a12 v1.8.1: global agents + parser robustness + WS reconnect toast
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>
2026-05-16 23:16:02 +00:00
2bce4d85fa feat(mobile): v1.8 tab switcher + branch indicator + git_status tool
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>
2026-05-16 20:07:53 +00:00
92bd3b1cdf feat(agents): Tier 2 — AGENTS.md + per-session picker
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>
2026-05-16 20:06:51 +00:00
934f739ca1 Merge branch 'v1.7-drag-drop' 2026-05-16 15:35:07 +00:00
50a756aca1 feat(input): drag-drop + paste-as-attachment for long text 2026-05-16 15:23:41 +00:00
3cb1ead5e2 feat(mobile): add hamburger + file explorer button to root empty state 2026-05-16 15:23:33 +00:00
5932682193 feat(mobile): right-rail as drawer on mobile, header toggle button
Reverts v1.6.1's max-md:hidden wrapper around RightRail. On mobile,
RightRail now renders as a fixed right-side drawer (w-[85vw],
max-w-sm) toggled by a new FolderTree button in the Session header.

- New useRightRailDrawer hook mirrors useSidebarDrawer (Context +
  auto-close on route change).
- New MobileRightRailBackdrop component in App.tsx mirrors the
  existing MobileBackdrop for the left sidebar.
- RightRail computes an isOpen synthesis: on mobile, reads the
  drawer Context; on desktop, reads the persistent internal state.
  The existing tree-load effect and open_file_in_browser
  subscription share this plumbing via openRail / closeRail
  helpers.
- The desktop floating chevron handle is hidden on mobile (the
  Session header's FolderTree button replaces it).
- Session header gains a mobile-only FolderTree button after the
  ModelPicker, calling toggle() on the drawer Context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 06:37:13 +00:00
9d0d41bcb3 feat(mobile): add "New chat" to tab long-press context menu
With the Split button hidden on mobile (G1), users need another path
to create additional chat panes. Add a "New chat" ContextMenuItem at
the top of each tab's context menu, separated from Rename / Close /
etc. by a ContextMenuSeparator. Wired to the existing onNewChat prop
— no plumbing change. Available on both long-press (mobile) and
right-click (desktop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 06:37:03 +00:00
e167f851fd feat(mobile): rework Session and Project headers for narrow viewports
Session header: breadcrumb (Projects > project) wrapped in
hidden sm:flex; active file path hidden on mobile; session name cap
max-w-[140px] sm:max-w-[280px]; padding px-3 sm:px-4. Mobile gets
just hamburger | session name | model pill.

Project header: px-3 sm:px-6, py-2 sm:py-3, heading text-base
sm:text-lg, project path hidden sm:block, "New session" button is
icon-only on mobile via <span className="hidden sm:inline">. Both
headers retain the safe-area-inset-top padding from v1.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 06:36:36 +00:00
f6c7e12dbf fix(mobile): hide Split button + single-pane navigator chrome
v1.6 left the Workspace's Split-button row visible on mobile even
when only one pane was open — ~36px of dead chrome above the chat.
Wrap the entire Split-row in !isMobile so mobile gets header → chat
with no intermediate strip. The existing mobile pane-navigator strip
(gated to panes.length > 1) is unchanged and still appears once a
second pane is created via the long-press "New chat" menu item (G3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 06:36:03 +00:00
6a9fe187bd fix(mobile): hide RightRail below md breakpoint
v1.6 left the right-rail file browser visible on phones (~32px column
when collapsed). Wrap the RightRail render in <div class="max-md:hidden
contents"> inside RightRailForSession so it's hidden entirely below
the md (768px) breakpoint. The `contents` class keeps the wrapper
layout-transparent on desktop. No behavior change on desktop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 06:00:25 +00:00
4b5b9b2cb3 feat(mobile): pull-to-refresh sidebar list
- usePullToRefresh: hand-rolled hook. Records startY only when the
  scroll container is at scrollTop=0 to avoid hijacking mid-scroll
  pulls. Tracks downward delta on touchmove; fires onRefresh on
  touchend if delta >= 80px threshold. Holds the refreshing state for
  600ms minimum so the action feels intentional.
- ProjectSidebar: wires usePullToRefresh(() => retry()) on the nav
  element, mobile-only. A status indicator above the nav grows with
  pullDist (max 80px) and cycles 'Pull to refresh' -> 'Release to
  refresh' -> 'Refreshing...'. retry() is from useSidebar and refetches
  GET /api/sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 05:55:47 +00:00
273eeac68c feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety
- ChatInput: e.nativeEvent.isComposing early-return added (CJK IME
  safety — first Enter of a composition no longer submits). Bare-Enter
  send path gated by !isMobile so mobile inserts a newline; send is
  button-only. Cmd/Ctrl+Enter and Shift+Cmd/Ctrl+Enter retained as
  desktop secondary bindings. Placeholder is now viewport-aware. Outer
  wrapper gets paddingBottom: env(safe-area-inset-bottom) so iOS home
  indicator doesn't overlap.
- MessageBubble: ActionRow buttons (Copy / Regenerate / Fork / Trash)
  bumped to max-md min-h/min-w 44px; opacity-100 on mobile so actions
  don't hide behind a hover-to-reveal pattern. User bubble and
  assistant content wrapper gain break-words + min-w-0 so long
  unbreakable strings (URLs / paths) wrap rather than blowing out
  the column on narrow viewports.
- ChatPane: queued-message dropdown + close X + Stop-generating button
  hit max-md 44px sizing.
- ChatTabBar: per-tab X, +/History/Close-pane action buttons hit
  max-md 44px. Tab close X is force-visible on mobile (no
  hover-to-reveal).
- M8: CodeBlock / Markdown tables / ToolCallCard already wrap
  overflow-x-auto pre-existing — no source change needed there; the
  break-words + min-w-0 additions above are the new defensive layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 05:55:34 +00:00
cd897d6893 feat(mobile): single-pane stack + long-press tab menu + swipe-to-close
- Workspace: on mobile renders only panes[activePaneIdx] (rest skipped).
  When panes.length > 1, adds a horizontal pane-navigator strip built
  from SwipeablePaneTab above the active pane. URL state ?pane=<paneId>
  written by switchActivePane (user-initiated only) and read on URL
  change (back-button + deep-link). Bare URL resets activePaneIdx to 0.
- useLongPress: 500ms touchstart timer; on fire, dispatches a synthetic
  contextmenu event on target.closest('[data-tab-id]') so the existing
  Radix ContextMenuTrigger opens at the touch coordinates. Suppresses
  the synthetic click that follows touchend. Cancels on touchmove /
  touchend / touchcancel.
- ChatTabBar: each tab gets data-tab-id, touch handlers wired to
  useLongPress, and WebkitTouchCallout: 'none' to disable iOS Safari's
  text-selection callout.
- SwipeablePaneTab: tracks horizontal drag; bails if vertical delta
  exceeds 30px (so vertical scroll still works); past 60px on release
  fires onClose (removePane), else snaps back. Opacity fades 1->0.4
  approaching the threshold. Hand-rolled per spec.
- Pane drag-and-drop disabled on mobile (HTML5 drag is broken on touch
  anyway; mobile uses the navigator + swipe-to-close instead).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 05:55:05 +00:00
a643b5f67f feat(mobile): viewport hook + sidebar drawer + hamburger headers
- useViewport: matchMedia-based hook (no resize polling). Breakpoints
  mobile <768 / tablet 768-1023 / desktop >=1024. SSR-safe.
- useSidebarDrawer: Context provider with open/setOpen/toggle + auto-close
  on useLocation().pathname change.
- App.tsx: wraps SidebarDrawerProvider around AppShell, renders a
  MobileBackdrop (z-30) when the drawer is open on mobile.
- ProjectSidebar: aside is fixed/translate-x-full off-screen on mobile,
  slides in (z-40, 200ms transform) when drawerOpen. Inline column on
  desktop, unchanged.
- Session.tsx + Project.tsx: hamburger (Menu icon, >=44x44 min) on mobile
  opens the drawer. Headers gain paddingTop: max(0.75rem,
  env(safe-area-inset-top)) for notch devices. Home.tsx left alone
  (sidebar content duplicates the home page).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 05:54:33 +00:00
4a9f207fe8 v1.5.1: bootstrap fixes (git + ssh in container, Tailscale host rewrite, /opt/projects label)
- 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>
2026-05-16 05:11:39 +00:00
782c2b183d feat: persistent context-window tracker in ChatPane
Adds a floating popover above the chat input showing current
context-window usage. Modeled on Paseo's tracker.

- New hook useChatContextStats(chatId, messages) finds the latest
  assistant message in the chat with both ctx_used and ctx_max set,
  computes percent, and returns null when data unavailable.
- New component ChatContextPopover renders a small card with the
  "Context window" label, big percent, and "used / max tokens"
  subline. Hidden when stats is null.
- Color thresholds: <60% muted, 60-85 amber, >85 destructive.
- Not a portal — absolutely positioned inside a new relative
  wrapper around ChatInput in ChatPane.tsx, so it's pane-local
  (multi-pane safe).
- Live updates via the existing messages-array dependency.
- No API / schema / WS changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 04:36:08 +00:00
2f6be39efd chore: surface swallowed errors + remove dead session_renamed paths
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>
2026-05-16 04:35:49 +00:00
9436a81b5f refactor: split FileBrowserPane / Workspace / runAssistantTurn
- FileBrowserPane.tsx: deleted (unreferenced post-v1.4 PaneTab.tsx removal;
  the legacy file_browser pane kind isn't part of the active WorkspacePane
  taxonomy).
- Workspace.tsx (524 -> 172 lines): extracted useWorkspacePanes(sessionId)
  and useSessionChats(sessionId) hooks. Workspace is layout-only composition
  now. localStorage key + WS frame handling + drag semantics unchanged.
- inference.ts runAssistantTurn (~265 -> 48 lines): bundled args into
  TurnArgs interface, extracted executeStreamPhase / executeToolPhase /
  finalizeCompletion / handleAbortOrError. All WS publish ordering preserved
  byte-for-byte (mentally traced for tool / non-tool / abort / error /
  depth-exceeded paths). flushPromise chain + setImmediate + signal
  propagation unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 04:35:20 +00:00
59fe6f0522 v1.4-fork-header: fork from message + delete message + header polish + housekeeping
- 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>
2026-05-16 04:12:01 +00:00
e09c67d65c tab-close + chat archive/delete + landing-card buttons + 1000px content cap
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>
2026-05-16 03:21:26 +00:00
48a972e139 project-ux: archive/rename/Open-in-Gitea sidebar context menu, archived projects landing, create-project bootstrap with Gitea remote
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>
2026-05-16 02:51:59 +00:00
051f3b96ae batch4.1-5.1: dedup audit, archive 400 fix, sidebar Delete, landing-page enrichment, auto-name tool-call fix
- 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>
2026-05-15 23:36:01 +00:00
c35ec65fc4 batch4: chats-in-sessions, force-send, /compact, right-rail file browser
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>
2026-05-15 20:39:48 +00:00
89d685105a batch3 T8 review fix: preserve cloneElement key in linkifyChildren
cloneElement does not carry el.key through unless explicitly passed.
Without it, react-markdown's inline-element siblings (strong/em/text)
lose their reconciler keys on every render, causing potential diffing
churn. Pass el.key (with fallback) explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:58:45 +00:00
eca4aa8382 batch3 T8: chat->file click, Session.tsx rewires to Workspace, sidebar polish
- MessageBubble & ToolCallCard: detect path-like strings in rendered text
  via regex requiring slash+extension; clicks dispatch open_file_in_browser
- Session.tsx: now renders <Workspace sessionId projectId>; on mount,
  emits session_loaded so sidebar can highlight even deep-linked sessions
  not in the recent_sessions cache
- ProjectSidebar: active project's chevron visually disabled (50% opacity,
  cursor-not-allowed) and click no-op; activeSession from useSidebar used
  as fallback when active session isn't in cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:55:52 +00:00