tab-close + chat archive/delete + landing-card buttons + 1000px content cap
Feature 1 — Tab close menu (pure local pane state, no API):
- ChatTabBar context menu: Rename / sep / Close / Close others / Close to right / Close all
- Workspace bulk-tab primitives: closeOtherTabs, closeTabsToRight, closeAllTabs (manipulate panes[].chatIds, no fetch)
- Drop in-bar Delete; landing card's name-typed Delete is the canonical destructive path
Feature 2 — Chat archive + delete:
- chats.status vocabulary aligned with projects ('open' | 'archived'); DROP old inline CHECK, UPDATE 'closed' → 'archived', ADD new named chats_status_chk
- POST /api/chats/:id/archive (204) + POST /api/chats/:id/unarchive (200) + GET /api/sessions/:id/chats?status=archived; DELETE publishes chat_deleted; PATCH simplified to name-only
- 3 new WS frames: chat_archived, chat_unarchived, chat_deleted (renamed from chat_closed)
- Same dedup discipline: server-only publish, no local sessionEvents.emit in client
- SessionLandingPage: right-click ContextMenu (Open / Rename / Archive / sep / Delete-destructive), inline rename, archive confirm dialog, delete dialog with name-typed Input gated until typed text === chat.name, Archived chats collapsible section with Restore
- Card-level Archive + Delete icon buttons reusing the same dialog state setters; stopPropagation on both so card click still opens the chat; archived cards keep only Restore
UX — chat content width cap:
- ChatPane content (MessageList, queue chips, stop button, ChatInput) wrapped in inner max-w-[1000px] mx-auto w-full so messages center; outer border-t / scroll containers stay full-width so pane chrome and backgrounds remain edge-to-edge
- No new deps, no media queries (narrow viewports collapse to width naturally)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,14 +8,6 @@ import {
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
@@ -23,11 +15,12 @@ interface Props {
|
||||
tabs: Chat[];
|
||||
onSwitchTab: (tabIdx: number) => void;
|
||||
onRemoveTab: (chatId: string) => void;
|
||||
onCloseOthers: (chatId: string) => void;
|
||||
onCloseToRight: (chatId: string) => void;
|
||||
onCloseAll: () => void;
|
||||
onNewChat: () => void;
|
||||
onShowHistory: () => void;
|
||||
onRename: (chatId: string, name: string) => Promise<void>;
|
||||
onClose: (chatId: string) => Promise<void>;
|
||||
onDelete: (chatId: string) => Promise<void>;
|
||||
onRemovePane?: () => void;
|
||||
}
|
||||
|
||||
@@ -36,16 +29,16 @@ export function ChatTabBar({
|
||||
tabs,
|
||||
onSwitchTab,
|
||||
onRemoveTab,
|
||||
onCloseOthers,
|
||||
onCloseToRight,
|
||||
onCloseAll,
|
||||
onNewChat,
|
||||
onShowHistory,
|
||||
onRename,
|
||||
onClose,
|
||||
onDelete,
|
||||
onRemovePane,
|
||||
}: Props) {
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||
|
||||
function startRename(chatId: string, currentName: string | null) {
|
||||
setRenamingId(chatId);
|
||||
@@ -61,9 +54,10 @@ export function ChatTabBar({
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-border bg-muted/20 h-8 shrink-0 overflow-x-auto">
|
||||
{/* Chat tabs */}
|
||||
{tabs.map((chat, tabIdx) => {
|
||||
const isActive = tabIdx === pane.activeChatIdx;
|
||||
const isLast = tabIdx === tabs.length - 1;
|
||||
const onlyTab = tabs.length === 1;
|
||||
const label = chat.name ?? 'New chat';
|
||||
return (
|
||||
<ContextMenu key={chat.id}>
|
||||
@@ -103,7 +97,7 @@ export function ChatTabBar({
|
||||
onRemoveTab(chat.id);
|
||||
}}
|
||||
className="p-0.5 hover:bg-muted rounded opacity-0 group-hover:opacity-60 hover:!opacity-100 shrink-0"
|
||||
aria-label="Remove from tab bar"
|
||||
aria-label="Close tab"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
@@ -114,21 +108,29 @@ export function ChatTabBar({
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => void onClose(chat.id)}>
|
||||
<ContextMenuItem onSelect={() => onRemoveTab(chat.id)}>
|
||||
Close
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onSelect={() => setDeleteConfirm(chat.id)}
|
||||
disabled={onlyTab}
|
||||
onSelect={() => onCloseOthers(chat.id)}
|
||||
>
|
||||
Delete
|
||||
Close others
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={isLast}
|
||||
onSelect={() => onCloseToRight(chat.id)}
|
||||
>
|
||||
Close to right
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => onCloseAll()}>
|
||||
Close all
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Empty state label */}
|
||||
{tabs.length === 0 && (
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground">
|
||||
<History size={12} className="shrink-0" />
|
||||
@@ -136,7 +138,6 @@ export function ChatTabBar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center ml-auto gap-0.5 px-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@@ -171,31 +172,6 @@ export function ChatTabBar({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={deleteConfirm !== null} onOpenChange={(open) => { if (!open) setDeleteConfirm(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete chat</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently delete this chat and all its messages. This cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
if (deleteConfirm) void onDelete(deleteConfirm);
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user