ChatTabBar: + button dropdown to add chat / terminal / agent pane

Replaces single onNewChat handler with onAddPane(kind). Terminal pane
header gets matching + dropdown. Context menu "New chat" stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 18:13:55 +00:00
parent 2d841ee0b4
commit 6aab4f7d2a
2 changed files with 63 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { History, MessageSquare, Plus, X } from 'lucide-react'; import { Bot, History, MessageSquare, Plus, Terminal, X } from 'lucide-react';
import type { Chat, WorkspacePane } from '@/api/types'; import type { Chat, WorkspacePane } from '@/api/types';
import { StatusDot } from '@/components/StatusDot'; import { StatusDot } from '@/components/StatusDot';
import { import {
@@ -9,6 +9,12 @@ import {
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuTrigger, ContextMenuTrigger,
} from '@/components/ui/context-menu'; } from '@/components/ui/context-menu';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useLongPress } from '@/hooks/useLongPress'; import { useLongPress } from '@/hooks/useLongPress';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -20,7 +26,7 @@ interface Props {
onCloseOthers: (chatId: string) => void; onCloseOthers: (chatId: string) => void;
onCloseToRight: (chatId: string) => void; onCloseToRight: (chatId: string) => void;
onCloseAll: () => void; onCloseAll: () => void;
onNewChat: () => void; onAddPane: (kind: 'chat' | 'terminal' | 'agent') => void;
onShowHistory: () => void; onShowHistory: () => void;
onRename: (chatId: string, name: string) => Promise<void>; onRename: (chatId: string, name: string) => Promise<void>;
onRemovePane?: () => void; onRemovePane?: () => void;
@@ -34,7 +40,7 @@ export function ChatTabBar({
onCloseOthers, onCloseOthers,
onCloseToRight, onCloseToRight,
onCloseAll, onCloseAll,
onNewChat, onAddPane,
onShowHistory, onShowHistory,
onRename, onRename,
onRemovePane, onRemovePane,
@@ -125,7 +131,7 @@ export function ChatTabBar({
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem onSelect={() => onNewChat()}> <ContextMenuItem onSelect={() => onAddPane('chat')}>
New chat New chat
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator />
@@ -164,15 +170,29 @@ export function ChatTabBar({
)} )}
<div className="flex items-center ml-auto gap-0.5 px-1 shrink-0"> <div className="flex items-center ml-auto gap-0.5 px-1 shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button <button
type="button" type="button"
onClick={onNewChat}
className="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]" className="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]"
aria-label="New chat" aria-label="New pane"
title="New chat" title="New pane"
> >
<Plus size={12} /> <Plus size={12} />
</button> </button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-40">
<DropdownMenuItem onSelect={() => onAddPane('chat')}>
<MessageSquare size={14} /> New chat
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onAddPane('terminal')}>
<Terminal size={14} /> New terminal
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onAddPane('agent')}>
<Bot size={14} /> New agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<button <button
type="button" type="button"
onClick={onShowHistory} onClick={onShowHistory}

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { PanelRight, MessageSquare, Terminal, Bot, Clipboard, X } from 'lucide-react'; import { PanelRight, MessageSquare, Terminal, Bot, Clipboard, Plus, X } from 'lucide-react';
import type { Chat, Project, Session, WorkspacePane } from '@/api/types'; import type { Chat, Project, Session, WorkspacePane } from '@/api/types';
import { MAX_PANES, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes'; import { MAX_PANES, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes';
import type { UseSessionChatsResult } from '@/hooks/useSessionChats'; import type { UseSessionChatsResult } from '@/hooks/useSessionChats';
@@ -227,7 +227,10 @@ export function Workspace({
onCloseOthers={(chatId) => closeOtherTabs(idx, chatId)} onCloseOthers={(chatId) => closeOtherTabs(idx, chatId)}
onCloseToRight={(chatId) => closeTabsToRight(idx, chatId)} onCloseToRight={(chatId) => closeTabsToRight(idx, chatId)}
onCloseAll={() => closeAllTabs(idx)} onCloseAll={() => closeAllTabs(idx)}
onNewChat={() => void createChat(idx)} onAddPane={(kind) => {
if (kind === 'chat') void createChat(idx);
else addSplitPane(kind);
}}
onShowHistory={() => showLandingPage(idx)} onShowHistory={() => showLandingPage(idx)}
onRename={renameChat} onRename={renameChat}
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined} onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
@@ -239,6 +242,30 @@ export function Workspace({
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{terminalLabels.get(pane.id) ?? 'Terminal'} {terminalLabels.get(pane.id) ?? 'Terminal'}
</span> </span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
onClick={(e) => e.stopPropagation()}
className="ml-auto inline-flex items-center justify-center size-5 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:size-7"
aria-label="New pane"
title="New pane"
>
<Plus size={12} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-40">
<DropdownMenuItem onSelect={() => addSplitPane('chat')}>
<MessageSquare size={14} /> New chat
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => addSplitPane('terminal')}>
<Terminal size={14} /> New terminal
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => addSplitPane('agent')}>
<Bot size={14} /> New agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* v1.10.4: iOS Safari restricts navigator.clipboard.readText {/* v1.10.4: iOS Safari restricts navigator.clipboard.readText
outside direct user gestures. A real button click IS a outside direct user gestures. A real button click IS a
gesture, so this works where keystroke-driven paste may gesture, so this works where keystroke-driven paste may
@@ -250,7 +277,7 @@ export function Workspace({
e.stopPropagation(); e.stopPropagation();
terminalsRegistry.get(pane.id)?.paste(); terminalsRegistry.get(pane.id)?.paste();
}} }}
className="ml-auto inline-flex items-center justify-center size-5 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:size-7" className="inline-flex items-center justify-center size-5 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:size-7"
aria-label="Paste from clipboard" aria-label="Paste from clipboard"
title="Paste from clipboard" title="Paste from clipboard"
> >