feat: shared PaneHeaderActions + chat-resolve WorkspaceState fix (v2.7.7)
In-flight workspace UX work. - Extract a shared PaneHeaderActions cluster (+/Split/Reopen/History/Close) used by ChatTabBar + the Workspace coder/terminal pane headers, replacing the divergent per-header copies; SessionLandingPage history + useWorkspacePanes tweaks. - Fix coder-side correctness bug: resolveChatId read sessions.workspace_panes as a bare WorkspacePane[] but v2.6.5 widened it to a WorkspaceState envelope, so it mis-read panes and clobbered tabNumbers/nextTabNumber/closedPaneStack on every pane-chat write. New normalizeWorkspaceState handles either shape and preserves the envelope (+ regression test). - CLAUDE.md doc-sync (coder vitest suite, deploy-by-surface, dual-remote push, in-flight-web-WIP staging, release-branch naming). Web tsc + coder build + coder tests green. Builds on v2.7.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { MessageSquare, Terminal, Code, Clipboard, Plus, X } from 'lucide-react';
|
||||
import { Terminal, Code, Clipboard } from 'lucide-react';
|
||||
import { api } from '@/api/client';
|
||||
import type { Chat, Project, Session, WorkspacePane } from '@/api/types';
|
||||
import { MAX_PANES, activePaneChatId, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes';
|
||||
@@ -13,13 +13,8 @@ import { CoderPane } from '@/components/panes/CoderPane';
|
||||
import { MarkdownArtifactPane } from '@/components/MarkdownArtifactPane';
|
||||
import { HtmlArtifactPane } from '@/components/HtmlArtifactPane';
|
||||
import { ChatTabBar } from '@/components/ChatTabBar';
|
||||
import { PaneHeaderActions } from '@/components/PaneHeaderActions';
|
||||
import { SessionLandingPage } from '@/components/SessionLandingPage';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
@@ -223,41 +218,13 @@ export function Workspace({
|
||||
<div className="flex items-center gap-1 border-b border-border px-2 py-1 shrink-0">
|
||||
<Code size={12} className="text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">BooCode</span>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center justify-center size-5 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label="New pane"
|
||||
>
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-fit">
|
||||
<DropdownMenuItem onSelect={() => onAddPane('chat')}>
|
||||
<MessageSquare size={14} /> New BooChat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onAddPane('terminal')}>
|
||||
<Terminal size={14} /> New BooTerm
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onAddPane('coder')}>
|
||||
<Code size={14} /> New BooCode
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{panes.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); removePane(idx); }}
|
||||
className="inline-flex items-center justify-center size-5 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label="Close pane"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<PaneHeaderActions
|
||||
className="ml-auto"
|
||||
onSplitPane={onAddPane}
|
||||
onReopenPane={hasClosedPanes ? reopenPane : undefined}
|
||||
onShowHistory={() => showLandingPage(idx)}
|
||||
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isTerminal && (
|
||||
@@ -266,61 +233,31 @@ export function Workspace({
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{terminalLabels.get(pane.id) ?? 'Terminal'}
|
||||
</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="w-fit">
|
||||
<DropdownMenuItem onSelect={() => onAddPane('chat')}>
|
||||
<MessageSquare size={14} /> New BooChat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onAddPane('terminal')}>
|
||||
<Terminal size={14} /> New BooTerm
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onAddPane('coder')}>
|
||||
<Code size={14} /> New BooCode
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* v1.10.4: iOS Safari restricts navigator.clipboard.readText
|
||||
outside direct user gestures. A real button click IS a
|
||||
gesture, so this works where keystroke-driven paste may
|
||||
not on iOS. The action lives in TerminalPane behind the
|
||||
registry's paste() callback. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
terminalsRegistry.get(pane.id)?.paste();
|
||||
}}
|
||||
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"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
<Clipboard size={12} />
|
||||
</button>
|
||||
{panes.length > 1 && (
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
{/* v1.10.4: iOS Safari restricts navigator.clipboard.readText
|
||||
outside direct user gestures. A real button click IS a
|
||||
gesture, so this works where keystroke-driven paste may
|
||||
not on iOS. The action lives in TerminalPane behind the
|
||||
registry's paste() callback. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removePane(idx);
|
||||
terminalsRegistry.get(pane.id)?.paste();
|
||||
}}
|
||||
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="Close terminal pane"
|
||||
title="Close terminal pane"
|
||||
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="Paste from clipboard"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
<X size={12} />
|
||||
<Clipboard size={12} />
|
||||
</button>
|
||||
)}
|
||||
<PaneHeaderActions
|
||||
onSplitPane={onAddPane}
|
||||
onReopenPane={hasClosedPanes ? reopenPane : undefined}
|
||||
onShowHistory={() => showLandingPage(idx)}
|
||||
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -395,6 +332,8 @@ export function Workspace({
|
||||
chats={chats}
|
||||
onOpenChat={(chatId) => openChatInPane(idx, chatId)}
|
||||
onUnarchiveChat={unarchiveChat}
|
||||
onArchiveChat={archiveChat}
|
||||
onDeleteChat={deleteChat}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user