feat: BooCode 2.0 UI — Ember theme, brand banner, coder tabs, model-attribution chips

- Ember theme (Obsidian charcoal + #ff7a18 orange), now DEFAULT_THEME_ID; server theme_id whitelist gains 'ember'
- Brand banner: transparent Westie mascot + >_BooCode wordmark, big/edge-to-edge (flood-filled to transparency + cropped)
- Coder panes are multi-tab: + opens a BooCode tab, split opens a pane (shared ChatTabBar via tabKind + createCoderTab; closeOtherTabs/tab-numbering extended to coder)
- Model-attribution: new messages.model column stamped at finalizeCompletion (BooChat/native coder) + dispatcher assistant-row creation (external coder); surfaced via view + wire types + live frame; rendered as a subtle shortened-name chip (shortenModelName)
- Composer Web toggle moved into a boxed focus-ringed input; glowing accent dot on tool rows
- Claude SDK follow-ups (1M context, follow-up-message fix, collapsed thinking/tool chips) + CLAUDE_SDK_BACKEND=1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 22:30:47 +00:00
parent 7098014261
commit 3a646fd6df
34 changed files with 482 additions and 163 deletions

View File

@@ -6,6 +6,7 @@ import type { Chat, ErrorReason, Message } from '@/api/types';
import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
import { sendToTerminal, terminalsRegistry, type TerminalRegistration } from '@/lib/events';
import { shortenModelName } from '@/lib/modelName';
import { CapHitSentinel } from './CapHitSentinel';
import { DoomLoopSentinel } from './DoomLoopSentinel';
import { MarkdownRenderer } from './MarkdownRenderer';
@@ -608,12 +609,12 @@ function SummaryCard({ message }: { message: Message }) {
// Collapsible "Thinking" block for assistant reasoning. Fed by either
// reasoning_text (coder wire / live reasoning_delta stream) or reasoning_parts
// (native inference, persisted from message_parts). Auto-expands while the turn
// is still streaming so the user watches it think (Paseo-style), then stays
// where the user left it once the turn completes — initial state is captured
// once at mount, so we never fight a manual collapse on later re-renders.
// (native inference, persisted from message_parts). Starts COLLAPSED to start
// (a quiet chip) — for native BooChat/BooCode and the external agents (opencode,
// claude SDK) alike — so the transcript stays tidy; click to expand. The
// `streaming` pulse still animates while the turn runs.
function ReasoningBlock({ text, streaming }: { text: string; streaming: boolean }) {
const [expanded, setExpanded] = useState(() => streaming);
const [expanded, setExpanded] = useState(false);
return (
<div className="max-w-[90%] rounded-lg border bg-muted/30 text-sm">
<button
@@ -768,7 +769,7 @@ export function MessageBubble({
return (
<div className="group flex flex-col items-end gap-1">
<SendToTerminalMenu>
<div className="max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap break-words min-w-0">
<div className="boo-user-bubble max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap break-words min-w-0">
{message.content}
</div>
</SendToTerminalMenu>
@@ -782,6 +783,8 @@ export function MessageBubble({
// v1.13.7: match the MessageList.flatten trim guard so a whitespace-only
// assistant turn doesn't render an empty bubble + dangling ActionRow.
const hasContent = message.content.trim().length > 0;
// model-attribution chip: short label for the model that produced this turn.
const modelLabel = shortenModelName(message.model);
// Reasoning arrives as a pre-joined string (coder wire) or as parts (native
// inference). Read whichever is present; loose ?? chain tolerates the coder
// shape where reasoning_parts is undefined (see CLAUDE.md null-guard note).
@@ -823,6 +826,14 @@ export function MessageBubble({
)}
</div>
)}
{!isStreaming && (modelLabel || null) && (
<span
className="inline-flex w-fit items-center rounded-full border border-primary/25 bg-primary/10 px-2 py-0.5 text-[10px] font-mono text-primary/90"
title={message.model ?? undefined}
>
{modelLabel}
</span>
)}
{!isStreaming && <StatsLine message={message} />}
{!isStreaming && hasContent && (
<ActionRow