- 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>
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
import { Code, Columns2, History, MessageSquare, Plus, RotateCcw, Terminal, X } from 'lucide-react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
// Shared pane-header action cluster: + (new) / Split / Reopen-closed-pane /
|
|
// Session history / Close. Rendered in the chat tab bar (ChatTabBar) and the
|
|
// desktop coder + terminal pane headers (Workspace) so all pane kinds share one
|
|
// control set. Extracted to avoid a divergent copy per header.
|
|
interface Props {
|
|
// When provided, the "+" menu item matching `tabKind` opens an in-pane tab
|
|
// (e.g. chat panes: New BooChat → tab; coder panes: New BooCode → tab). Every
|
|
// OTHER kind splits into a new pane. When onNewTab is omitted (terminal
|
|
// panes, which can't host tabs) all three items split.
|
|
onNewTab?: () => void;
|
|
// The host pane's own kind — the "+" item of this kind becomes "new tab".
|
|
// Defaults to 'chat' for back-compat with the chat tab bar.
|
|
tabKind?: 'chat' | 'terminal' | 'coder';
|
|
onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void;
|
|
onReopenPane?: () => void;
|
|
onShowHistory: () => void;
|
|
onRemovePane?: () => void;
|
|
// Highlights the History button when the pane is showing the landing page.
|
|
historyActive?: boolean;
|
|
// Positioning/spacing supplied by the parent (e.g. "ml-auto px-1").
|
|
className?: string;
|
|
}
|
|
|
|
const BTN =
|
|
'inline-flex items-center justify-center p-1 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:min-h-[44px] max-md:min-w-[44px]';
|
|
|
|
export function PaneHeaderActions({
|
|
onNewTab,
|
|
tabKind = 'chat',
|
|
onSplitPane,
|
|
onReopenPane,
|
|
onShowHistory,
|
|
onRemovePane,
|
|
historyActive,
|
|
className,
|
|
}: Props) {
|
|
// The "+" item of the host pane's own kind adds a tab; every other kind
|
|
// splits into a new pane. Falls back to split when onNewTab is absent.
|
|
const newOrSplit = (kind: 'chat' | 'terminal' | 'coder') =>
|
|
onNewTab && tabKind === kind ? onNewTab : () => onSplitPane(kind);
|
|
return (
|
|
<div className={cn('flex items-center gap-0.5 shrink-0', className)}>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className={BTN}
|
|
aria-label="New chat, terminal, or coder"
|
|
title="New chat / terminal / coder"
|
|
>
|
|
<Plus size={12} />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-fit">
|
|
{/* The item matching the host pane's kind opens an in-pane tab; the
|
|
others split into a new pane. (tabKind defaults to 'chat'.) */}
|
|
<DropdownMenuItem onSelect={newOrSplit('chat')}>
|
|
<MessageSquare size={14} /> New BooChat
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={newOrSplit('terminal')}>
|
|
<Terminal size={14} /> New BooTerm
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={newOrSplit('coder')}>
|
|
<Code size={14} /> New BooCode
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className={cn(BTN, 'max-md:hidden')}
|
|
aria-label="Split pane"
|
|
title="Split pane"
|
|
>
|
|
<Columns2 size={12} />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-fit">
|
|
<DropdownMenuItem onSelect={() => onSplitPane('chat')}>
|
|
<MessageSquare size={14} /> New BooChat
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={() => onSplitPane('terminal')}>
|
|
<Terminal size={14} /> New BooTerm
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={() => onSplitPane('coder')}>
|
|
<Code size={14} /> New BooCode
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{onReopenPane && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onReopenPane();
|
|
}}
|
|
className={cn(BTN, 'max-md:hidden')}
|
|
aria-label="Reopen closed pane"
|
|
title="Reopen closed pane"
|
|
>
|
|
<RotateCcw size={12} />
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onShowHistory();
|
|
}}
|
|
className={cn(BTN, 'max-md:hidden', historyActive && 'text-foreground bg-muted/50')}
|
|
aria-label="Session history"
|
|
title="Session history"
|
|
>
|
|
<History size={12} />
|
|
</button>
|
|
|
|
{onRemovePane && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onRemovePane();
|
|
}}
|
|
className={BTN}
|
|
aria-label="Close pane"
|
|
title="Close pane"
|
|
>
|
|
<X size={12} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|