wip: pane/session + tab-bar checkpoint
Second checkpoint of in-flight work (sessions route, api types, ChatTabBar, PaneHeaderActions, Workspace, useWorkspacePanes) so the Orchestrator branch can rebase onto current main before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Code, History, MessageSquare, X } from 'lucide-react';
|
||||
import type { Chat, WorkspacePane } from '@/api/types';
|
||||
import { Clipboard, Code, History, MessageSquare, Terminal, X } from 'lucide-react';
|
||||
import type { WorkspacePane, WorkspaceTabKind } from '@/api/types';
|
||||
import { StatusDot } from '@/components/StatusDot';
|
||||
import { PaneHeaderActions } from '@/components/PaneHeaderActions';
|
||||
import {
|
||||
@@ -14,32 +14,51 @@ import { useLongPress } from '@/hooks/useLongPress';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Mixed tabs: a pane can hold tabs of different kinds. Each tab is described by
|
||||
// its id, kind, and label (terminal tabs have no chats row, so their label is
|
||||
// supplied by the caller).
|
||||
export interface TabDescriptor {
|
||||
id: string;
|
||||
kind: WorkspaceTabKind;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pane: WorkspacePane;
|
||||
tabs: Chat[];
|
||||
// Host pane kind — 'coder' shows the Code glyph + routes the "+" to a new
|
||||
// BooCode tab. Defaults to 'chat' (the BooChat tab bar).
|
||||
tabKind?: 'chat' | 'coder';
|
||||
// v2.6.x (Batch 3a): stable session-scoped tab number per chat id. Keyed by
|
||||
// chat.id, NEVER by tab position.
|
||||
tabs: TabDescriptor[];
|
||||
// v2.6.x (Batch 3a): stable session-scoped tab number per id.
|
||||
tabNumbers: Record<string, number>;
|
||||
onSwitchTab: (tabIdx: number) => void;
|
||||
onRemoveTab: (chatId: string) => void;
|
||||
onCloseOthers: (chatId: string) => void;
|
||||
onCloseToRight: (chatId: string) => void;
|
||||
onRemoveTab: (id: string) => void;
|
||||
onCloseOthers: (id: string) => void;
|
||||
onCloseToRight: (id: string) => void;
|
||||
onCloseAll: () => void;
|
||||
onNewTab: () => void;
|
||||
// Mixed tabs: the "+" adds a tab of the chosen kind to THIS pane.
|
||||
onNewTab: (kind: WorkspaceTabKind) => void;
|
||||
onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void;
|
||||
onReopenPane?: () => void;
|
||||
onShowHistory: () => void;
|
||||
onRename: (chatId: string, name: string) => Promise<void>;
|
||||
onRemovePane?: () => void;
|
||||
// iOS-safe terminal paste, shown only when the active tab is a terminal.
|
||||
onTerminalPaste?: () => void;
|
||||
}
|
||||
|
||||
function iconForKind(kind: WorkspaceTabKind) {
|
||||
if (kind === 'coder') return Code;
|
||||
if (kind === 'terminal') return Terminal;
|
||||
return MessageSquare;
|
||||
}
|
||||
|
||||
function defaultName(kind: WorkspaceTabKind): string {
|
||||
if (kind === 'coder') return 'BooCoder';
|
||||
if (kind === 'terminal') return 'Terminal';
|
||||
return 'New chat';
|
||||
}
|
||||
|
||||
export function ChatTabBar({
|
||||
pane,
|
||||
tabs,
|
||||
tabKind = 'chat',
|
||||
tabNumbers,
|
||||
onSwitchTab,
|
||||
onRemoveTab,
|
||||
@@ -52,15 +71,13 @@ export function ChatTabBar({
|
||||
onShowHistory,
|
||||
onRename,
|
||||
onRemovePane,
|
||||
onTerminalPaste,
|
||||
}: Props) {
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const TabIcon = tabKind === 'coder' ? Code : MessageSquare;
|
||||
const newLabel = tabKind === 'coder' ? 'New BooCode' : 'New chat';
|
||||
|
||||
// Long-press: dispatch a synthetic contextmenu event on the tab so the
|
||||
// existing Radix ContextMenuTrigger opens at the touch coordinates. Works
|
||||
// because asChild composition makes the tab div the trigger element.
|
||||
// existing Radix ContextMenuTrigger opens at the touch coordinates.
|
||||
const longPress = useLongPress(({ clientX, clientY, target }) => {
|
||||
if (!target || !(target instanceof Element)) return;
|
||||
const tab = target.closest('[data-tab-id]') as HTMLElement | null;
|
||||
@@ -70,8 +87,8 @@ export function ChatTabBar({
|
||||
);
|
||||
});
|
||||
|
||||
function startRename(chatId: string, currentName: string | null) {
|
||||
setRenamingId(chatId);
|
||||
function startRename(id: string, currentName: string | null) {
|
||||
setRenamingId(id);
|
||||
setRenameValue(currentName ?? '');
|
||||
}
|
||||
|
||||
@@ -84,19 +101,19 @@ export function ChatTabBar({
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-border bg-muted/20 h-8 shrink-0 overflow-x-auto max-md:hidden">
|
||||
{tabs.map((chat, tabIdx) => {
|
||||
{tabs.map((tab, tabIdx) => {
|
||||
const isActive = tabIdx === pane.activeChatIdx;
|
||||
const isLast = tabIdx === tabs.length - 1;
|
||||
const onlyTab = tabs.length === 1;
|
||||
const label = chat.name ?? 'New chat';
|
||||
// v2.6.x: stable tab number keyed by chat.id (NOT tab position).
|
||||
// Omit gracefully when not yet assigned.
|
||||
const tabNumber = tabNumbers[chat.id];
|
||||
const TabIcon = iconForKind(tab.kind);
|
||||
const label = tab.name ?? defaultName(tab.kind);
|
||||
const canRename = tab.kind !== 'terminal';
|
||||
const tabNumber = tabNumbers[tab.id];
|
||||
return (
|
||||
<ContextMenu key={chat.id}>
|
||||
<ContextMenu key={tab.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
data-tab-id={chat.id}
|
||||
data-tab-id={tab.id}
|
||||
onClick={() => onSwitchTab(tabIdx)}
|
||||
onTouchStart={longPress.onTouchStart}
|
||||
onTouchMove={longPress.onTouchMove}
|
||||
@@ -111,8 +128,8 @@ export function ChatTabBar({
|
||||
)}
|
||||
>
|
||||
<TabIcon size={12} className="shrink-0" />
|
||||
<StatusDot chatId={chat.id} />
|
||||
{renamingId === chat.id ? (
|
||||
{tab.kind !== 'terminal' && <StatusDot chatId={tab.id} />}
|
||||
{renamingId === tab.id ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
@@ -137,7 +154,7 @@ export function ChatTabBar({
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveTab(chat.id);
|
||||
onRemoveTab(tab.id);
|
||||
}}
|
||||
className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded opacity-0 group-hover:opacity-60 hover:!opacity-100 shrink-0 max-md:min-h-[44px] max-md:min-w-[44px] max-md:opacity-100"
|
||||
aria-label="Close tab"
|
||||
@@ -147,34 +164,34 @@ export function ChatTabBar({
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onSelect={onNewTab}>
|
||||
{newLabel}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onSelect={() =>
|
||||
sessionEvents.emit({ type: 'open_chat_in_new_pane', chat_id: chat.id })
|
||||
}
|
||||
>
|
||||
Open in new pane
|
||||
<ContextMenuItem onSelect={() => onNewTab(tab.kind)}>
|
||||
New {defaultName(tab.kind)}
|
||||
</ContextMenuItem>
|
||||
{tab.kind !== 'terminal' && (
|
||||
<ContextMenuItem
|
||||
onSelect={() =>
|
||||
sessionEvents.emit({ type: 'open_chat_in_new_pane', chat_id: tab.id })
|
||||
}
|
||||
>
|
||||
Open in new pane
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{canRename && (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => startRename(tab.id, tab.name)}>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => startRename(chat.id, chat.name)}>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => onRemoveTab(chat.id)}>
|
||||
<ContextMenuItem onSelect={() => onRemoveTab(tab.id)}>
|
||||
Close
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={onlyTab}
|
||||
onSelect={() => onCloseOthers(chat.id)}
|
||||
>
|
||||
<ContextMenuItem disabled={onlyTab} onSelect={() => onCloseOthers(tab.id)}>
|
||||
Close others
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled={isLast}
|
||||
onSelect={() => onCloseToRight(chat.id)}
|
||||
>
|
||||
<ContextMenuItem disabled={isLast} onSelect={() => onCloseToRight(tab.id)}>
|
||||
Close to right
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => onCloseAll()}>
|
||||
@@ -192,16 +209,30 @@ export function ChatTabBar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PaneHeaderActions
|
||||
className="ml-auto px-1"
|
||||
onNewTab={onNewTab}
|
||||
tabKind={tabKind}
|
||||
onSplitPane={onSplitPane}
|
||||
onReopenPane={onReopenPane}
|
||||
onShowHistory={onShowHistory}
|
||||
onRemovePane={onRemovePane}
|
||||
historyActive={pane.kind === 'empty'}
|
||||
/>
|
||||
<div className="ml-auto flex items-center px-1 shrink-0">
|
||||
{onTerminalPaste && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTerminalPaste();
|
||||
}}
|
||||
className="inline-flex items-center justify-center p-1 rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label="Paste from clipboard"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
<Clipboard size={12} />
|
||||
</button>
|
||||
)}
|
||||
<PaneHeaderActions
|
||||
onNewTab={onNewTab}
|
||||
onSplitPane={onSplitPane}
|
||||
onReopenPane={onReopenPane}
|
||||
onShowHistory={onShowHistory}
|
||||
onRemovePane={onRemovePane}
|
||||
historyActive={pane.kind === 'empty'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,14 +12,9 @@ import { cn } from '@/lib/utils';
|
||||
// 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';
|
||||
// Mixed tabs: the "+" menu adds a tab of the chosen kind to THIS pane. Split
|
||||
// (the second control) adds a new pane.
|
||||
onNewTab: (kind: 'chat' | 'terminal' | 'coder') => void;
|
||||
onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void;
|
||||
onReopenPane?: () => void;
|
||||
onShowHistory: () => void;
|
||||
@@ -35,7 +30,6 @@ const BTN =
|
||||
|
||||
export function PaneHeaderActions({
|
||||
onNewTab,
|
||||
tabKind = 'chat',
|
||||
onSplitPane,
|
||||
onReopenPane,
|
||||
onShowHistory,
|
||||
@@ -43,10 +37,6 @@ export function PaneHeaderActions({
|
||||
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>
|
||||
@@ -55,22 +45,21 @@ export function PaneHeaderActions({
|
||||
type="button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={BTN}
|
||||
aria-label="New chat, terminal, or coder"
|
||||
title="New chat / terminal / coder"
|
||||
aria-label="New tab"
|
||||
title="New tab (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')}>
|
||||
{/* Mixed tabs: every item adds a tab of that kind to THIS pane. */}
|
||||
<DropdownMenuItem onSelect={() => onNewTab('chat')}>
|
||||
<MessageSquare size={14} /> New BooChat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={newOrSplit('terminal')}>
|
||||
<DropdownMenuItem onSelect={() => onNewTab('terminal')}>
|
||||
<Terminal size={14} /> New BooTerm
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={newOrSplit('coder')}>
|
||||
<DropdownMenuItem onSelect={() => onNewTab('coder')}>
|
||||
<Code size={14} /> New BooCode
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Terminal, Clipboard } from 'lucide-react';
|
||||
import { api } from '@/api/client';
|
||||
import type { Chat, Project, Session, WorkspacePane } from '@/api/types';
|
||||
import type { Project, Session, WorkspacePane, WorkspaceTabKind } from '@/api/types';
|
||||
import { activePaneChatId, type UseWorkspacePanesResult } from '@/hooks/useWorkspacePanes';
|
||||
import type { UseSessionChatsResult } from '@/hooks/useSessionChats';
|
||||
import { useViewport } from '@/hooks/useViewport';
|
||||
@@ -12,11 +11,17 @@ import { TerminalPane } from '@/components/panes/TerminalPane';
|
||||
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 { ChatTabBar, type TabDescriptor } from '@/components/ChatTabBar';
|
||||
import { SessionLandingPage } from '@/components/SessionLandingPage';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Mixed tabs: the kind of tab `i` in a pane, with a legacy fallback to pane.kind.
|
||||
function tabKindAt(pane: WorkspacePane, i: number): WorkspaceTabKind {
|
||||
const k = pane.tabKinds?.[i];
|
||||
if (k) return k;
|
||||
return pane.kind === 'coder' ? 'coder' : pane.kind === 'terminal' ? 'terminal' : 'chat';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
@@ -59,8 +64,7 @@ export function Workspace({
|
||||
historyPaneId,
|
||||
openSessionHistory,
|
||||
closeSessionHistory,
|
||||
addSplitPane,
|
||||
createCoderTab,
|
||||
createTab,
|
||||
removePane,
|
||||
reopenPane,
|
||||
hasClosedPanes,
|
||||
@@ -75,7 +79,6 @@ export function Workspace({
|
||||
} = panesHook;
|
||||
const {
|
||||
chats,
|
||||
createChat,
|
||||
archiveChat,
|
||||
unarchiveChat,
|
||||
deleteChat,
|
||||
@@ -121,26 +124,35 @@ export function Workspace({
|
||||
if (maximized && settingsIdx < 0) setMaximized(false);
|
||||
}, [maximized, settingsIdx]);
|
||||
|
||||
function chatsForPane(pane: WorkspacePane): Chat[] {
|
||||
return pane.chatIds
|
||||
.map((id) => chats.find((c) => c.id === id))
|
||||
.filter((c): c is Chat => c !== undefined);
|
||||
}
|
||||
|
||||
// v1.10 booterm: per-terminal label used by the registry that powers the
|
||||
// MessageBubble "Send to terminal" submenu. Numbered in workspace order.
|
||||
// v1.10 booterm + mixed tabs: per-terminal-TAB label, keyed by the terminal
|
||||
// tab id (which keys its tmux session). Numbered across the workspace.
|
||||
const terminalLabels = useMemo(() => {
|
||||
const out = new Map<string, string>();
|
||||
let n = 0;
|
||||
for (const p of panes) {
|
||||
if (p.kind === 'terminal') {
|
||||
n += 1;
|
||||
out.set(p.id, `Terminal ${n}`);
|
||||
}
|
||||
p.chatIds.forEach((id, i) => {
|
||||
if (tabKindAt(p, i) === 'terminal') {
|
||||
n += 1;
|
||||
out.set(id, `Terminal ${n}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}, [panes]);
|
||||
|
||||
// Mixed tabs: descriptors for the tab strip — each tab's id, kind, and label.
|
||||
// Terminal tabs have no chats row, so their label comes from terminalLabels.
|
||||
function paneTabs(pane: WorkspacePane): TabDescriptor[] {
|
||||
return pane.chatIds.map((id, i) => {
|
||||
const kind = tabKindAt(pane, i);
|
||||
const name =
|
||||
kind === 'terminal'
|
||||
? terminalLabels.get(id) ?? 'Terminal'
|
||||
: chats.find((c) => c.id === id)?.name ?? null;
|
||||
return { id, kind, name };
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div
|
||||
@@ -191,61 +203,35 @@ export function Workspace({
|
||||
onDragStart={!isMobile && !isChromeless && panes.length > 1 ? handlePaneDragStart(idx) : undefined}
|
||||
onDragEnd={!isMobile && !isChromeless && panes.length > 1 ? handlePaneDragEnd : undefined}
|
||||
>
|
||||
{/* Hidden on mobile; settings/terminal/artifact panes own their
|
||||
own header. Chat and coder panes share this strip — tabKind
|
||||
and onNewTab differ between the two. */}
|
||||
{!isMobile && (isCoder || !isChromeless) && (
|
||||
{/* Mixed tabs: one unified strip for every tabbed pane
|
||||
(chat / coder / terminal / empty-landing). The "+" adds a tab
|
||||
of any kind; Split adds a pane. Settings/artifact panes own
|
||||
their own headers. Hidden on mobile (mobile uses pane panes). */}
|
||||
{!isMobile && !isSettings && !isArtifact && (
|
||||
<ChatTabBar
|
||||
pane={pane}
|
||||
tabs={chatsForPane(pane)}
|
||||
tabKind={isCoder ? 'coder' : undefined}
|
||||
tabs={paneTabs(pane)}
|
||||
tabNumbers={tabNumbers}
|
||||
onSwitchTab={(tabIdx) => switchTab(idx, tabIdx)}
|
||||
onRemoveTab={(chatId) => removeTab(idx, chatId)}
|
||||
onCloseOthers={(chatId) => closeOtherTabs(idx, chatId)}
|
||||
onCloseToRight={(chatId) => closeTabsToRight(idx, chatId)}
|
||||
onCloseAll={() => closeAllTabs(idx)}
|
||||
onNewTab={isCoder ? () => void createCoderTab(idx) : () => void createChat(idx)}
|
||||
onNewTab={(kind) => void createTab(idx, kind)}
|
||||
onSplitPane={(kind) => onAddPane(kind)}
|
||||
onReopenPane={hasClosedPanes ? reopenPane : undefined}
|
||||
onShowHistory={() => openSessionHistory(idx)}
|
||||
onRename={renameChat}
|
||||
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
|
||||
// iOS-safe terminal paste (real click is a user gesture), shown
|
||||
// only while the active tab is a terminal.
|
||||
onTerminalPaste={
|
||||
isTerminal
|
||||
? () => terminalsRegistry.get(activePaneChatId(pane) ?? pane.id)?.paste()
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{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" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{terminalLabels.get(pane.id) ?? 'Terminal'}
|
||||
</span>
|
||||
<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();
|
||||
terminalsRegistry.get(pane.id)?.paste();
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<Clipboard size={12} />
|
||||
</button>
|
||||
<PaneHeaderActions
|
||||
onSplitPane={onAddPane}
|
||||
onReopenPane={hasClosedPanes ? reopenPane : undefined}
|
||||
onShowHistory={() => openSessionHistory(idx)}
|
||||
onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
@@ -260,9 +246,10 @@ export function Workspace({
|
||||
/>
|
||||
) : isTerminal ? (
|
||||
<TerminalPane
|
||||
key={activePaneChatId(pane) ?? pane.id}
|
||||
sessionId={sessionId}
|
||||
paneId={pane.id}
|
||||
label={terminalLabels.get(pane.id) ?? 'Terminal'}
|
||||
paneId={activePaneChatId(pane) ?? pane.id}
|
||||
label={terminalLabels.get(activePaneChatId(pane) ?? pane.id) ?? 'Terminal'}
|
||||
active={idx === activePaneIdx}
|
||||
/>
|
||||
) : pane.kind === 'coder' ? (
|
||||
|
||||
Reference in New Issue
Block a user