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

@@ -188,6 +188,8 @@ export interface UseWorkspacePanesResult {
// id to update mobile URL state so the URL-sync effect doesn't fight the
// freshly-set activePaneIdx.
addSplitPane: (kind: 'chat' | 'terminal' | 'coder') => string | null;
/** Append a new BooCode tab to an existing coder pane (the coder "+"). */
createCoderTab: (paneIdx: number) => Promise<void>;
// Open-on-first-click, close-on-second-click. Singleton — settings panes
// don't count toward MAX_PANES. Closing the only remaining pane (edge case)
// falls back to an empty pane to preserve the "always one pane" invariant.
@@ -265,6 +267,42 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
[sessionId, attachChatToPane, markPaneChatPending],
);
// Add a new BooCode tab to an existing coder pane (the "+" in the coder pane
// header). Creates a fresh chat row (= a new agent context that shares the
// session worktree) and APPENDS it to the pane's chatIds, keeping the pane
// kind 'coder' and focusing the new tab. Mirrors createChat for chat panes;
// the per-pane "split into a new pane" action stays addSplitPane.
const createCoderTab = useCallback(
async (paneIdx: number) => {
const paneId = panes[paneIdx]?.id;
if (!paneId) return;
markPaneChatPending(paneId, true);
try {
const chat = await api.chats.create(sessionId, { name: chatNameForPaneKind('coder') });
setPanes((prev) => {
const idx = prev.findIndex((p) => p.id === paneId);
if (idx < 0) return prev;
const pane = prev[idx]!;
const newIds = [...pane.chatIds, chat.id];
const next = [...prev];
next[idx] = {
...pane,
kind: 'coder',
chatId: chat.id,
chatIds: newIds,
activeChatIdx: newIds.length - 1,
};
return next;
});
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to create coder tab');
} finally {
markPaneChatPending(paneId, false);
}
},
[sessionId, panes, markPaneChatPending],
);
const seedEmptyScopedPanes = useCallback(
(paneList: WorkspacePane[]) => {
for (const pane of paneList) {
@@ -426,16 +464,16 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
}, [sessionId, panes, tabNumbers, nextTabNumber, closedPaneStack]);
// v2.6.x (Batch 3a): maintain stable, session-scoped tab numbers. Collect the
// chat ids that appear in CHAT-kind panes in deterministic order (pane index,
// then tab index). Assign numbers to any without one (global per session,
// only ever increasing, never reused) and prune entries whose chat is no
// longer in any chat-kind pane. Guarded against render loops: only setState
// when something actually changed.
// chat ids that appear in CHAT- or CODER-kind panes in deterministic order
// (pane index, then tab index). Assign numbers to any without one (global per
// session, only ever increasing, never reused) and prune entries whose chat
// is no longer in any tab-hosting pane. Guarded against render loops: only
// setState when something actually changed.
useEffect(() => {
const liveChatIds: string[] = [];
const liveSet = new Set<string>();
for (const pane of panes) {
if (pane.kind !== 'chat') continue;
if (pane.kind !== 'chat' && pane.kind !== 'coder') continue;
for (const id of pane.chatIds) {
if (!liveSet.has(id)) {
liveSet.add(id);
@@ -597,9 +635,9 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
const pane = next[paneIdx]!;
const keepIdx = pane.chatIds.indexOf(keepChatId);
if (keepIdx < 0) return prev;
// Preserve pane.kind (...pane) — a coder pane stays a coder pane.
next[paneIdx] = {
...pane,
kind: 'chat',
chatId: keepChatId,
chatIds: [keepChatId],
activeChatIdx: 0,
@@ -954,6 +992,7 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
closeAllTabs,
showLandingPage,
addSplitPane,
createCoderTab,
toggleSettingsPane,
removePane,
reopenPane,