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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user