v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { PanelRight, MessageSquare, Terminal, Code, Clipboard, Plus, X } from 'lucide-react';
|
||||
import { MessageSquare, Terminal, Code, Clipboard, Plus, X } from 'lucide-react';
|
||||
import type { Chat, Project, Session, WorkspacePane } from '@/api/types';
|
||||
import { MAX_PANES, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes';
|
||||
import { MAX_PANES, activePaneChatId, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes';
|
||||
import type { UseSessionChatsResult } from '@/hooks/useSessionChats';
|
||||
import { useViewport } from '@/hooks/useViewport';
|
||||
import { terminalsRegistry } from '@/lib/events';
|
||||
@@ -34,6 +34,8 @@ interface Props {
|
||||
// v1.9: passed through to SettingsPane when one is mounted in the grid.
|
||||
session: Session;
|
||||
project: Project | null;
|
||||
/** New BooCode opens a fresh coder session; chat/terminal split in-place. */
|
||||
onAddPane: (kind: 'chat' | 'terminal' | 'coder') => void;
|
||||
}
|
||||
|
||||
export function Workspace({
|
||||
@@ -45,6 +47,7 @@ export function Workspace({
|
||||
chatsHook,
|
||||
session,
|
||||
project,
|
||||
onAddPane,
|
||||
}: Props) {
|
||||
const {
|
||||
panes,
|
||||
@@ -59,6 +62,7 @@ export function Workspace({
|
||||
showLandingPage,
|
||||
addSplitPane,
|
||||
removePane,
|
||||
isPaneChatPending,
|
||||
handlePaneDragStart,
|
||||
handlePaneDragOver,
|
||||
handlePaneDragLeave,
|
||||
@@ -134,44 +138,11 @@ export function Workspace({
|
||||
return out;
|
||||
}, [panes]);
|
||||
|
||||
// Per-coder-pane WS connection (status dot lives in the pane header).
|
||||
const [coderConnected, setCoderConnected] = useState<Record<string, boolean>>({});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
{!isMobile && (
|
||||
<div className="flex items-center gap-2 border-b border-border bg-muted/20 px-3 py-1.5 shrink-0">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
// v1.9: settings panes excluded from the MAX cap (decision c).
|
||||
disabled={panes.filter((p) => p.kind !== 'settings').length >= MAX_PANES}
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-xs px-2 py-1 rounded hover:bg-muted',
|
||||
panes.filter((p) => p.kind !== 'settings').length >= MAX_PANES &&
|
||||
'opacity-40 cursor-not-allowed hover:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<PanelRight size={14} />
|
||||
Split
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('chat')}>
|
||||
<MessageSquare size={14} /> Chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('terminal')}>
|
||||
<Terminal size={14} /> Terminal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('coder')}>
|
||||
<Code size={14} /> Coder
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* v1.8: mobile multi-pane SwipeablePaneTab strip removed; the header
|
||||
pill (MobileTabSwitcher) is the mobile pane switcher. */}
|
||||
|
||||
<div
|
||||
className={cn('flex-1 min-h-0', isMobile ? 'flex' : 'grid')}
|
||||
style={
|
||||
@@ -185,6 +156,7 @@ export function Workspace({
|
||||
{panes.map((pane, idx) => {
|
||||
const isSettings = pane.kind === 'settings';
|
||||
const isTerminal = pane.kind === 'terminal';
|
||||
const isCoder = pane.kind === 'coder';
|
||||
const isArtifact = pane.kind === 'markdown_artifact' || pane.kind === 'html_artifact';
|
||||
// v1.9: when maximized, hide every pane except the settings one.
|
||||
// display:none keeps the React tree mounted so streams / drafts
|
||||
@@ -197,9 +169,8 @@ export function Workspace({
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Terminal panes own their tab strip (no chats, no ChatTabBar) and
|
||||
// are not drag-reorderable for now — keeps the layout grid simple.
|
||||
const isChromeless = isSettings || isTerminal || isArtifact;
|
||||
// Terminal + coder panes own their tab strip (no chats, no ChatTabBar).
|
||||
const isChromeless = isSettings || isTerminal || isCoder || isArtifact;
|
||||
return (
|
||||
<div
|
||||
key={pane.id}
|
||||
@@ -233,13 +204,66 @@ export function Workspace({
|
||||
onCloseAll={() => closeAllTabs(idx)}
|
||||
onAddPane={(kind) => {
|
||||
if (kind === 'chat') void createChat(idx);
|
||||
else addSplitPane(kind);
|
||||
else onAddPane(kind);
|
||||
}}
|
||||
onShowHistory={() => showLandingPage(idx)}
|
||||
onRename={renameChat}
|
||||
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
|
||||
/>
|
||||
)}
|
||||
{isCoder && (
|
||||
<div className="flex items-center gap-2 border-b border-border bg-muted/30 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.5">
|
||||
<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 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={() => 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>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block w-1.5 h-1.5 rounded-full shrink-0',
|
||||
coderConnected[pane.id] ? 'bg-green-500' : 'bg-red-500',
|
||||
)}
|
||||
title={coderConnected[pane.id] ? 'Connected' : 'Disconnected'}
|
||||
/>
|
||||
{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 max-md:size-7"
|
||||
aria-label="Close BooCode pane"
|
||||
title="Close BooCode pane"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isTerminal && (
|
||||
<div className="flex items-center gap-2 border-b border-border bg-muted/30 px-2 py-1 shrink-0">
|
||||
<Terminal size={12} className="text-muted-foreground" />
|
||||
@@ -259,14 +283,14 @@ export function Workspace({
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-40">
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('chat')}>
|
||||
<MessageSquare size={14} /> New chat
|
||||
<DropdownMenuItem onSelect={() => onAddPane('chat')}>
|
||||
<MessageSquare size={14} /> New BooChat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('terminal')}>
|
||||
<Terminal size={14} /> New terminal
|
||||
<DropdownMenuItem onSelect={() => onAddPane('terminal')}>
|
||||
<Terminal size={14} /> New BooTerm
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => addSplitPane('coder')}>
|
||||
<Code size={14} /> New coder
|
||||
<DropdownMenuItem onSelect={() => onAddPane('coder')}>
|
||||
<Code size={14} /> New BooCode
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -323,7 +347,18 @@ export function Workspace({
|
||||
active={idx === activePaneIdx}
|
||||
/>
|
||||
) : pane.kind === 'coder' ? (
|
||||
<CoderPane sessionId={sessionId} />
|
||||
<CoderPane
|
||||
sessionId={sessionId}
|
||||
paneId={pane.id}
|
||||
chatId={activePaneChatId(pane)}
|
||||
chatPending={isPaneChatPending(pane.id)}
|
||||
projectPath={project?.path}
|
||||
onConnectedChange={(connected) =>
|
||||
setCoderConnected((prev) =>
|
||||
prev[pane.id] === connected ? prev : { ...prev, [pane.id]: connected },
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : pane.kind === 'markdown_artifact' && pane.markdown_artifact_state ? (
|
||||
<MarkdownArtifactPane
|
||||
chatId={pane.markdown_artifact_state.chat_id}
|
||||
|
||||
Reference in New Issue
Block a user