diff --git a/CHANGELOG.md b/CHANGELOG.md index 51891ae..149ed6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes per release tag. Most recent on top, ordered by tag creation date (which matches the git history). Tag names follow `vMAJOR.MINOR.PATCH-slug` — the slug describes what shipped, so the tag name alone is enough to recall the batch. +## v2.7.8-ember-coder-tabs-model-chips — 2026-06-01 + +The BooCode 2.0 visual identity plus two workflow features. **Ember theme** (`styles/themes/ember.css`, now `DEFAULT_THEME_ID`) is the signature orange-on-near-black look — rebuilt on Obsidian's flat charcoal structure (`#0c0c0e`/`#15151a`/`#1f1f23`) with `#ff7a18` swapped in for the purple, after a Reinvented-direction detour (neon borders + a scanline/glow texture overlay) was dialed back to taste; the server `theme_id` whitelist gains `ember` so it can actually be selected. The **brand banner** (`ProjectSidebar`) shows the eye-patch Westie mascot + the `>_BooCode` wordmark big and edge-to-edge on transparent backgrounds — the source PNGs shipped with baked-white canvases, so they were flood-filled to transparency from the corners (preserving the white dog, which a naive white-key would have destroyed) and cropped to bounds. **Coder panes are now multi-tab**: `+` opens a new BooCode tab (a fresh chat = a new agent context sharing the session worktree) while the split button still opens a pane — coder panes reuse the shared `ChatTabBar` via a kind-aware `tabKind`, backed by a new `createCoderTab` action with `closeOtherTabs`/tab-numbering extended to coder kind. **Model-attribution chips**: a new `messages.model` column (both apps share the table) stamped at `finalizeCompletion` (BooChat + native coder) and at the dispatcher's assistant-row creation (external coder), surfaced through the `messages_with_parts` view + wire types + the live `message_complete` frame (the Zod already allowed `model`; nothing consumed it), and rendered as a subtle accent chip with a shortened label (`shortenModelName` → `Sonnet 4.6`, `Qwen3.6 35B`) beside the message stats — so swapping models mid-coder-session stays legible. Also the composer moved its Web toggle into a boxed, focus-ringed input, tool rows lead with a glowing accent dot, and the Claude-SDK-backend follow-ups validated live this session (1M context window, follow-up-message fix, collapsed thinking/tool chips) land with `CLAUDE_SDK_BACKEND=1` flipped on. One snag fixed mid-deploy: the view's new `m.model` was first inserted mid-list and `CREATE OR REPLACE VIEW` can't reorder columns (42P16) — appended at the end. Web tsc + server + coder builds green; deployed (docker + boocoder, tools:34). Builds on `v2.7.7-pane-header-actions`. + ## v2.7.7-pane-header-actions — 2026-06-01 In-flight workspace UX work, committed alongside the v2.7 review batches. Extracts a shared `PaneHeaderActions` cluster (the +/Split/Reopen-closed-pane/Session-history/Close controls) used across the `ChatTabBar` and the desktop coder + terminal pane headers in `Workspace`, replacing the divergent per-header copies, with `SessionLandingPage` history enhancements and `useWorkspacePanes` tweaks. Also fixes a coder-side correctness bug: `resolveChatId` (`apps/coder/src/routes/chat-resolve.ts`) still read `sessions.workspace_panes` as a bare `WorkspacePane[]`, but `v2.6.5-panes-tabs-composer` widened it to a `WorkspaceState` envelope — so it mis-read the panes and, worse, clobbered `tabNumbers`/`nextTabNumber`/`closedPaneStack` back to a bare array on every pane-chat write; a new `normalizeWorkspaceState` accepts either shape and preserves the envelope (with a regression test). Plus a CLAUDE.md doc-sync (apps/coder vitest suite, deploy-by-surface, dual-remote push, in-flight-web-WIP staging, release-branch naming). Web tsc + coder build + coder tests green. Builds on `v2.7.6-agent-status-normalize`. diff --git a/apps/coder/.env.host b/apps/coder/.env.host index 48b5c3c..86ff17a 100644 --- a/apps/coder/.env.host +++ b/apps/coder/.env.host @@ -14,3 +14,4 @@ GITEA_SSH_HOST=100.114.205.53:2222 MCP_CONFIG_PATH=/data/mcp.json SKILLS_ROOT=/opt/boocode/data/skills CODER_PROVIDERS_PATH=/opt/boocode/data/coder-providers.json +CLAUDE_SDK_BACKEND=1 diff --git a/apps/coder/src/routes/messages.ts b/apps/coder/src/routes/messages.ts index e620ec9..854344c 100644 --- a/apps/coder/src/routes/messages.ts +++ b/apps/coder/src/routes/messages.ts @@ -53,6 +53,9 @@ interface MessageRow { role: string; content: string | null; status: string | null; + model: string | null; + ctx_used: number | null; + ctx_max: number | null; tool_calls: Array<{ id: string; name: string; args?: Record }> | null; tool_results: { tool_call_id: string; @@ -88,6 +91,9 @@ function mapCoderMessageRow(row: MessageRow) { role: row.role as 'user' | 'assistant' | 'system', content: row.content ?? '', status: (row.status ?? 'complete') as 'streaming' | 'complete' | 'failed', + ...(row.model ? { model: row.model } : {}), + ...(row.ctx_used != null ? { ctx_used: row.ctx_used } : {}), + ...(row.ctx_max != null ? { ctx_max: row.ctx_max } : {}), ...(reasoningText ? { reasoning_text: reasoningText } : {}), ...(tool_calls?.length ? { tool_calls } : {}), }; @@ -126,13 +132,13 @@ export function registerMessageRoutes( const rows = chatId ? await sql` - SELECT id, role, content, status, tool_calls, tool_results, reasoning_parts + SELECT id, role, content, status, model, ctx_used, ctx_max, tool_calls, tool_results, reasoning_parts FROM messages_with_parts WHERE session_id = ${sessionId} AND chat_id = ${chatId} ORDER BY created_at ASC, id ASC ` : await sql` - SELECT id, role, content, status, tool_calls, tool_results, reasoning_parts + SELECT id, role, content, status, model, ctx_used, ctx_max, tool_calls, tool_results, reasoning_parts FROM messages_with_parts WHERE session_id = ${sessionId} ORDER BY created_at ASC, id ASC diff --git a/apps/coder/src/services/agent-backend.ts b/apps/coder/src/services/agent-backend.ts index efd3628..0f29fd7 100644 --- a/apps/coder/src/services/agent-backend.ts +++ b/apps/coder/src/services/agent-backend.ts @@ -82,6 +82,12 @@ export interface PromptCtx { export interface TurnResult { ok: boolean; error?: string; + // Optional context-window telemetry (claude SDK): the model's reported window + // (ctxMax, 1M-aware) and the peak request input ≈ current fill (ctxUsed). The + // dispatcher writes these onto the assistant message so the ContextBar renders a + // real fill for the turn. Omitted by backends that don't report a window. + ctxUsed?: number; + ctxMax?: number; } /** diff --git a/apps/coder/src/services/backends/claude-sdk.ts b/apps/coder/src/services/backends/claude-sdk.ts index 9191c19..fa40bf3 100644 --- a/apps/coder/src/services/backends/claude-sdk.ts +++ b/apps/coder/src/services/backends/claude-sdk.ts @@ -165,6 +165,12 @@ export class ClaudeSdkBackend implements AgentBackend { // Stream partial assistant messages so text/thinking/tool deltas arrive live // (the mapper reads them; without this only terminal messages land). includePartialMessages: true, + // BooCode default: enable the documented 1M-context-window beta. Active on + // models that support it (the SDK lists Sonnet 4/4.5); a non-supporting model + // simply doesn't get the larger window. The TRUE window is read back from + // `result.modelUsage[*].contextWindow` and shown in the ContextBar, so whatever + // window a model actually gets is surfaced truthfully (no guessing). + betas: ['context-1m-2025-08-07'], ...(model ? { model } : {}), ...(resumeId ? { resume: resumeId } : {}), ...(this.installPath ? { pathToClaudeCodeExecutable: this.installPath } : {}), @@ -192,6 +198,11 @@ export class ClaudeSdkBackend implements AgentBackend { this.busy = true; const state: ClaudeSdkMapState = createClaudeSdkMapState(); + // Peak per-request input (incl. cache) across the turn ≈ the conversation context + // held in the window. result.usage SUMS input over the turn's internal requests + // (overcounts for multi-tool turns), so the per-request peak is the accurate + // "context used" for the ContextBar (paseo's approach). + let maxInputTokens = 0; // Per-turn abort: interrupt the in-flight query on the SAME generator (never // tear down the warm query — that's the pool's lifetime). The generator then // emits its terminal result and the drain loop exits. @@ -214,7 +225,32 @@ export class ClaudeSdkBackend implements AgentBackend { queue.push(userMsg); try { - for await (const msg of gen) { + // Manual iteration — NOT `for await (… of gen)`. Returning out of a for-await + // loop calls gen.return(), which CLOSES the async generator; that killed the + // warm streaming-input query after a single turn, so every FOLLOW-UP message + // hit a dead generator and failed. gen.next() leaves the generator suspended + // (alive) for the next pushed user message — the warm query is only closed + // deliberately in teardownQuery()/dispose(). + while (true) { + const next = await gen.next(); + if (next.done) { + // Generator ended (e.g. disposed) without a result — non-fatal incomplete. + if (aborted) return { ok: false, error: 'aborted' }; + return { ok: false, error: 'claude-sdk: query ended before result' }; + } + const msg = next.value; + // Track the peak per-request input from message_start usage (delivered by + // includePartialMessages) — the largest single request's input is the real + // context fill, unlike the summed result.usage. + if (msg.type === 'stream_event') { + const sev = msg.event as { type?: string; message?: { usage?: Record } }; + if (sev?.type === 'message_start' && sev.message?.usage) { + const ru = sev.message.usage; + const reqInput = + num(ru.input_tokens) + num(ru.cache_read_input_tokens) + num(ru.cache_creation_input_tokens); + if (reqInput > maxInputTokens) maxInputTokens = reqInput; + } + } // Capture the provider session id from the init message (authoritative). if (msg.type === 'system' && msg.subtype === 'init' && msg.session_id) { if (this.agentSessionId !== msg.session_id) { @@ -234,19 +270,28 @@ export class ClaudeSdkBackend implements AgentBackend { await this.markIdle(); } if (aborted) return { ok: false, error: 'aborted' }; - return ok - ? { ok: true } - : { ok: false, error: resultErrorMessage(msg) }; + if (!ok) return { ok: false, error: resultErrorMessage(msg) }; + // Context-window telemetry for the ContextBar (paseo's method): + // ctxMax = the model's OWN reported window (1M-aware — reflects the active + // window, so the bar shows the truth per model); + // ctxUsed = peak request input (history in the window) + this turn's output. + const ctxMax = extractMaxContextWindow((msg as { modelUsage?: unknown }).modelUsage); + const fallbackInput = + num(msg.usage?.input_tokens) + + num(msg.usage?.cache_read_input_tokens) + + num(msg.usage?.cache_creation_input_tokens); + const ctxUsed = (maxInputTokens || fallbackInput) + num(msg.usage?.output_tokens); + return { + ok: true, + ...(ctxMax > 0 ? { ctxMax } : {}), + ...(ctxUsed > 0 ? { ctxUsed } : {}), + }; } // Map renderable content → AgentEvents for the dispatcher's onEvent. for (const ev of mapSdkMessage(msg, state)) { ctx.onEvent(ev); } } - // Generator ended without a result message (e.g. it was disposed) — treat as - // a non-fatal incomplete turn so the dispatcher still finalizes the row. - if (aborted) return { ok: false, error: 'aborted' }; - return { ok: false, error: 'claude-sdk: query ended before result' }; } catch (err) { if (aborted) return { ok: false, error: 'aborted' }; await this.markCrashed(); @@ -351,6 +396,22 @@ function numF(v: unknown): number { return Number.isFinite(x) && x > 0 ? x : 0; } +/** Largest context-window the SDK reports across `result.modelUsage` (a + * `Record`, each with a `contextWindow`). This is the model's + * OWN window — 1M when the 1M model/beta is active, 200K otherwise — so the + * ContextBar shows the true window without us mapping model→size ourselves. */ +function extractMaxContextWindow(modelUsage: unknown): number { + if (!modelUsage || typeof modelUsage !== 'object') return 0; + let max = 0; + for (const v of Object.values(modelUsage as Record)) { + if (v && typeof v === 'object') { + const cw = (v as { contextWindow?: unknown }).contextWindow; + if (typeof cw === 'number' && Number.isFinite(cw) && cw > max) max = cw; + } + } + return max; +} + /** Build a human-readable error from an SDK error-result message. */ function resultErrorMessage(result: Extract): string { if (result.subtype === 'success') return 'ok'; diff --git a/apps/coder/src/services/dispatcher.ts b/apps/coder/src/services/dispatcher.ts index 2fd5bae..f5a5bea 100644 --- a/apps/coder/src/services/dispatcher.ts +++ b/apps/coder/src/services/dispatcher.ts @@ -213,8 +213,8 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` - INSERT INTO messages (session_id, chat_id, role, content, status, created_at) - VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp()) + INSERT INTO messages (session_id, chat_id, role, content, status, model, created_at) + VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', ${task.model}, clock_timestamp()) RETURNING id `; const assistantId = assistantMsg!.id; @@ -380,8 +380,8 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` - INSERT INTO messages (session_id, chat_id, role, content, status, created_at) - VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp()) + INSERT INTO messages (session_id, chat_id, role, content, status, model, created_at) + VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', ${task.model}, clock_timestamp()) RETURNING id `; const assistantId = assistantMsg!.id; @@ -723,8 +723,8 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` - INSERT INTO messages (session_id, chat_id, role, content, status, created_at) - VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp()) + INSERT INTO messages (session_id, chat_id, role, content, status, model, created_at) + VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', ${task.model}, clock_timestamp()) RETURNING id `; const assistantId = assistantMsg!.id; @@ -1004,8 +1004,8 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` - INSERT INTO messages (session_id, chat_id, role, content, status, created_at) - VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp()) + INSERT INTO messages (session_id, chat_id, role, content, status, model, created_at) + VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', ${task.model}, clock_timestamp()) RETURNING id `; const assistantId = assistantMsg!.id; @@ -1260,8 +1260,8 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` - INSERT INTO messages (session_id, chat_id, role, content, status, created_at) - VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp()) + INSERT INTO messages (session_id, chat_id, role, content, status, model, created_at) + VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', ${task.model}, clock_timestamp()) RETURNING id `; const assistantId = assistantMsg!.id; @@ -1373,9 +1373,12 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise` SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq, tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata, - summary, tail_start_id, compacted_at + summary, tail_start_id, compacted_at, model FROM messages_with_parts WHERE chat_id = ${req.params.id} ORDER BY created_at ASC, id ASC diff --git a/apps/server/src/routes/messages.ts b/apps/server/src/routes/messages.ts index 690a3df..d81e2d9 100644 --- a/apps/server/src/routes/messages.ts +++ b/apps/server/src/routes/messages.ts @@ -118,7 +118,7 @@ export function registerMessageRoutes( const rows = await sql` SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq, tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata, - summary, tail_start_id, compacted_at + summary, tail_start_id, compacted_at, model FROM messages_with_parts WHERE session_id = ${req.params.id} ORDER BY created_at ASC, id ASC diff --git a/apps/server/src/routes/settings.ts b/apps/server/src/routes/settings.ts index 858acac..d9ced66 100644 --- a/apps/server/src/routes/settings.ts +++ b/apps/server/src/routes/settings.ts @@ -22,8 +22,9 @@ export async function setSetting( `; } -// themes-v1: whitelist of the 18 preset theme ids. Kept in sync with +// themes-v1: whitelist of the preset theme ids. Kept in sync with // docs/themes_v1.md §1 and apps/web/src/lib/theme.ts THEMES. +// (+ 'ember' — the BooCode 2.0 signature, now the default.) const THEME_IDS = [ 'obsidian', 'gunmetal', @@ -43,6 +44,7 @@ const THEME_IDS = [ 'chalk', 'cobalt', 'midnight-sapphire', + 'ember', ] as const; const THEME_MODES = ['dark', 'light', 'system'] as const; diff --git a/apps/server/src/routes/ws.ts b/apps/server/src/routes/ws.ts index 55d2c33..f8d8833 100644 --- a/apps/server/src/routes/ws.ts +++ b/apps/server/src/routes/ws.ts @@ -27,7 +27,7 @@ export function registerWebSocket( const messages = await sql` SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq, tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata, - summary, tail_start_id, compacted_at + summary, tail_start_id, compacted_at, model FROM messages_with_parts WHERE session_id = ${sessionId} ORDER BY created_at ASC, id ASC diff --git a/apps/server/src/schema.sql b/apps/server/src/schema.sql index a2487d4..83b080c 100644 --- a/apps/server/src/schema.sql +++ b/apps/server/src/schema.sql @@ -107,6 +107,11 @@ END $$; -- a single jsonb object {tool_call_id, output, truncated, error?}. -- reasoning_parts is consumed by the inference history fetch (payload.ts) -- for v1.13.1-C reasoning round-tripping. Not surfaced in external APIs. +-- model-attribution: which model produced an assistant message (NULL for +-- user/system rows and pre-existing messages). Stamped at finalize (BooChat / +-- native coder) and at assistant-row creation (external coder dispatcher). +ALTER TABLE messages ADD COLUMN IF NOT EXISTS model TEXT; + CREATE OR REPLACE VIEW messages_with_parts AS SELECT m.id, m.session_id, m.chat_id, m.role, m.content, m.kind, m.status, @@ -122,7 +127,10 @@ SELECT ORDER BY p.sequence LIMIT 1) AS tool_results, (SELECT jsonb_agg(p.payload ORDER BY p.sequence) FROM message_parts p - WHERE p.message_id = m.id AND p.kind = 'reasoning' AND p.hidden_at IS NULL) AS reasoning_parts + WHERE p.message_id = m.id AND p.kind = 'reasoning' AND p.hidden_at IS NULL) AS reasoning_parts, + -- NEW columns MUST be appended at the end: CREATE OR REPLACE VIEW can't + -- reorder/rename existing columns (42P16). m.model added last. + m.model FROM messages m; -- v1.13.20: drop legacy tool_calls/tool_results columns. Reads have routed diff --git a/apps/server/src/services/inference/error-handler.ts b/apps/server/src/services/inference/error-handler.ts index 77b4488..584ab9a 100644 --- a/apps/server/src/services/inference/error-handler.ts +++ b/apps/server/src/services/inference/error-handler.ts @@ -119,6 +119,7 @@ export async function finalizeCompletion( tokens_used = ${completionTokens}, ctx_used = ${promptTokens}, ctx_max = ${nCtx}, + model = ${session.model}, finished_at = clock_timestamp() WHERE id = ${assistantMessageId} RETURNING tokens_used, ctx_used, ctx_max, finished_at diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index e12afbc..462f084 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -201,6 +201,9 @@ export interface Message { tokens_used: number | null; ctx_used: number | null; ctx_max: number | null; + // model-attribution: which model produced this assistant message (null for + // user/system rows + pre-attribution messages). Rendered as a chip. + model: string | null; started_at: string | null; finished_at: string | null; created_at: string; @@ -351,7 +354,13 @@ export interface CoderMessageWire { role: 'user' | 'assistant' | 'system'; content: string; status?: 'streaming' | 'complete' | 'failed'; + // model-attribution: which model produced this coder assistant message. + model?: string | null; reasoning_text?: string; + // Context-window fill for the ContextBar (claude SDK turns set these from the + // SDK's reported window; other agents omit them). Read via the Message cast. + ctx_used?: number | null; + ctx_max?: number | null; tool_calls?: Array<{ id: string; function: { name: string; arguments: string }; @@ -571,6 +580,8 @@ export type WsFrame = ctx_max?: number | null; started_at?: string | null; finished_at?: string | null; + // model-attribution: the model that produced this assistant message. + model?: string | null; // v1.8.2: piggybacks the persisted metadata onto the terminal frame so // cap-hit sentinels (and any future stamped-on-complete metadata) flow // to the client without a refetch. diff --git a/apps/web/src/assets/brand/banner-mascot.png b/apps/web/src/assets/brand/banner-mascot.png new file mode 100644 index 0000000..1e0f3d3 Binary files /dev/null and b/apps/web/src/assets/brand/banner-mascot.png differ diff --git a/apps/web/src/assets/brand/banner-wordmark.png b/apps/web/src/assets/brand/banner-wordmark.png new file mode 100644 index 0000000..0140c66 Binary files /dev/null and b/apps/web/src/assets/brand/banner-wordmark.png differ diff --git a/apps/web/src/assets/brand/boo-badge.png b/apps/web/src/assets/brand/boo-badge.png new file mode 100644 index 0000000..09418cd Binary files /dev/null and b/apps/web/src/assets/brand/boo-badge.png differ diff --git a/apps/web/src/assets/brand/boocode-icon.png b/apps/web/src/assets/brand/boocode-icon.png new file mode 100644 index 0000000..427f342 Binary files /dev/null and b/apps/web/src/assets/brand/boocode-icon.png differ diff --git a/apps/web/src/assets/brand/boocode-wordmark-tight.png b/apps/web/src/assets/brand/boocode-wordmark-tight.png new file mode 100644 index 0000000..d164df4 Binary files /dev/null and b/apps/web/src/assets/brand/boocode-wordmark-tight.png differ diff --git a/apps/web/src/assets/brand/boocode-wordmark.png b/apps/web/src/assets/brand/boocode-wordmark.png new file mode 100644 index 0000000..77dad7f Binary files /dev/null and b/apps/web/src/assets/brand/boocode-wordmark.png differ diff --git a/apps/web/src/components/ChatInput.tsx b/apps/web/src/components/ChatInput.tsx index 57ed110..e73a80d 100644 --- a/apps/web/src/components/ChatInput.tsx +++ b/apps/web/src/components/ChatInput.tsx @@ -1,14 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from 'react'; -import { Check, ListPlus, Plus, Send, Square } from 'lucide-react'; +import { Globe, ListPlus, Send, Square } from 'lucide-react'; import { toast } from 'sonner'; import { Textarea } from '@/components/ui/textarea'; import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { flattenToMessage, inferLanguage, @@ -598,39 +592,9 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session onChange={onAgentChange} /> )} - {sessionId && ( - - - - - - { - // v1.9: tri-state collapses to two on the wire when toggled - // here. null (inherit) treated as off; click flips to true. - // To restore "inherit" the user opens SettingsPane. - const next = webSearchEnabled === true ? false : true; - try { - await api.sessions.update(sessionId, { web_search_enabled: next }); - } catch (err) { - toast.error(err instanceof Error ? err.message : 'failed to toggle web search'); - } - }} - className="text-xs" - > - - Enable web search and fetch - - - - )} + {/* BooCode 2.0: the web-search toggle moved out of this top toolbar + into the composer box's bottom controls row (the Web pill below), + leaving the top row as just the agent picker + context bar. */} {/* v1.11.5.1: ContextBar fills the remaining horizontal space. `flex-1 min-w-0` is set inside the component. Mounts only when the caller passes `messages` so older call sites (without the @@ -640,54 +604,86 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session )} )} -
-