mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(chat): match terminal pane split and close menus (#28969)
* Transfer keyboard focus when a sibling chat pane opens * fix(native-chat): hand focus to newly opened chat composer * fix(native-chat): keep tab bar controller within line limit * fix(chat): split into a fresh conversation beside the current chat * test(chat): declare the split fixture store without an assertion * fix(chat): route the split to the source conversation host * test(chat): keep the split regression focused on the launch contract * test(chat): give the command palette split fixture its conversation agent * test(chat): seed the legacy command fixture without expanding its asserted shape * test(chat): isolate pane identity commands from the split launch boundary * Prevent chat splits from starting a terminal on host refusal * Cancel structured launches when their destination pane closes * Model live destination groups in agent launch test fixture * Capture split chat focus intent at the user action * feat(chat): add the terminal pane hover action cluster * fix(chat): keep pane controls above the conversation surface * refactor(chat): share the tab resume launch hook and format pane controls * style(chat): document reuse of the terminal header button styles * refactor(ui): share the pane title button variants between terminals and chats * fix(chat): keep header actions scoped to the displayed chat * fix(chat): abandon Resume when its destination pane closes * test(chat): declare destination fixtures without type assertions * fix(chat): keep Resume visible when its source pane closes * fix(chat): reuse tab menu Resume placement unchanged * test: fix Resume regression harness setup * feat(chat): match terminal pane split and close context actions * style(chat): format the pane context menu * style(chat): format the structured pane command fixture * test(chat): remove the obsolete tab-movement menu stubs * Match terminal close-pane menu styling * fix(chat): share the pane action close label in the body menu * test(chat): match structured pane action fixture returns * fix(chat): hand input to the split pane during admission * fix(chat): Split opens a fresh chat beside the current one Split Right/Down on a native chat used to move the chat into a new pane. It now opens an empty pane beside the chat and launches a new chat of the same agent on the same host into it through the New Chat launch, so the new composer takes focus at once. A launch that never opens a tab closes the empty pane. Drops the earlier admission and focus-transfer changes that main now covers. * fix(chat): Split palette search words describe a new chat, not a move * fix: remove misleading chat move shortcut hints * fix(chat): a new chat stays in its pane until its host lists it A host tab list built before a chat's create landed can arrive after the create reply. Once the reply settled the launch, nothing protected the new tab, so that older list dropped it: Split's new pane collapsed and focus moved to another chat, and the next list re-added the chat elsewhere. Keep a small in-memory record for a chat this client created that no host list has shown yet. While it exists, a list that omits the chat keeps its tab. The record ends when an accepted list from that host includes the chat, when the tab closes, on workspace teardown, or 30 seconds after the reply, when the worktree is re-read from its host (including an unchanged version) so a chat the host really closed is dropped then. The launch lifecycle, status, persistence and settlement are unchanged. * test(chat): type the faked create replies without an assertion * Keep new chat protection tied to accepted host lists * Update paired chat close regression to current suppression API * fix(chat): Portuguese Split palette words describe a new chat * test(chat): use the destination group option in split fixture * Avoid History projection in native chat header and currentness checks * fix(native-chat): keep Find below pane controls * fix(ui): keep noninteractive tooltip layers pointer-transparent * fix: derive chat pane menu state inside mounted content * test: use complete settings fixture for chat menu contract * ci: refresh chat menu checks after base correction * ci: refresh chat menu checks after base type fixes * test(chat): focus retained menu lifetime contract --------- Co-authored-by: Orca Worker <orca-worker@localhost>
This commit is contained in:
co-authored by
Orca Worker
parent
f04b831a61
commit
a75cdc8287
@@ -146,8 +146,6 @@ export function NativeChatStructuredSession(
|
||||
startFailures,
|
||||
agentName: agentLabel
|
||||
})
|
||||
// Nothing reads an unread history, so its pane stays blank beside the Retry line.
|
||||
const loadingPane = historyPhase === 'unread' ? null : <NativeChatLoadingCue />
|
||||
// A lost contact is the host notice's to say; the read adds only a refusal the host sent.
|
||||
const readFailure =
|
||||
controller.status === 'error' && !(hostOutage && !controller.readRefusal)
|
||||
@@ -170,6 +168,7 @@ export function NativeChatStructuredSession(
|
||||
const fileLinkProps = { onLinkClick, allowFileUriLinks: onLinkClick !== undefined }
|
||||
const paneCommands = useStructuredNativeChatPaneCommands({
|
||||
tabId: props.tabId,
|
||||
ownerWorktreeId,
|
||||
groupId: props.groupId,
|
||||
isVisible: props.isVisible,
|
||||
rootRef,
|
||||
@@ -294,7 +293,7 @@ export function NativeChatStructuredSession(
|
||||
<NativeChatContextClearFade clearing={controller.contextClear.clearing}>
|
||||
{find.isOpen ? <NativeChatFindBar find={find} isVisible={props.isVisible} /> : null}
|
||||
{viewState.kind === 'loading' || (viewState.kind === 'error' && !readFailure) ? (
|
||||
loadingPane
|
||||
historyPhase !== 'unread' && <NativeChatLoadingCue />
|
||||
) : viewState.kind === 'error' ? (
|
||||
<NativeChatEmptyState
|
||||
kind="error"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createRef } from 'react'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { makeGroup, makeUnifiedTab } from '../worktree-jump-palette-test-fixtures'
|
||||
import { useStructuredNativeChatPaneCommands } from './use-structured-native-chat-pane-commands'
|
||||
import { emptyNativeChatContextMenuActions } from './use-native-chat-context-menu'
|
||||
import { runNativeChatSplitTarget } from './native-chat-layout-actions'
|
||||
|
||||
vi.mock('./native-chat-layout-actions', () => ({ runNativeChatSplitTarget: vi.fn(() => true) }))
|
||||
vi.mock('./NativeChatSelectionQuote', () => ({ NativeChatSelectionQuote: () => null }))
|
||||
const initial = useAppStore.getState()
|
||||
let reads = 0
|
||||
let renders = 0
|
||||
let revision = 0
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
useAppStore.setState(initial, true)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
function Host({ index }: { index: number }) {
|
||||
renders++
|
||||
const rootRef = createRef<HTMLDivElement>()
|
||||
const { menu, ...commands } = useStructuredNativeChatPaneCommands({
|
||||
tabId: `chat-${index}`,
|
||||
groupId: 'group-owner',
|
||||
ownerWorktreeId: 'owner',
|
||||
isVisible: index % 3 !== 1,
|
||||
terminalPaneActions: index % 3 === 2 ? emptyNativeChatContextMenuActions : undefined,
|
||||
rootRef,
|
||||
composerRef: createRef(),
|
||||
questionAnswerInputRef: createRef(),
|
||||
sessionId: '4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37',
|
||||
target: { kind: 'local' }
|
||||
})
|
||||
return (
|
||||
<div ref={rootRef} data-testid={`host-${index}`} {...commands}>
|
||||
{menu}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function measure(write: () => void) {
|
||||
for (const tab of Object.values(useAppStore.getState().unifiedTabsByWorktree).flat()) {
|
||||
for (const key of ['id', 'groupId'] as const) {
|
||||
const value = tab[key]
|
||||
Object.defineProperty(tab, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => (reads++, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
reads = 0
|
||||
act(write)
|
||||
return reads
|
||||
}
|
||||
|
||||
it.each([6, 60])('bounds retained menu work at %i chats', async (size) => {
|
||||
const seed = () => {
|
||||
const tabs = Array.from({ length: size }, (_, index) => ({
|
||||
...makeUnifiedTab(`chat-${index}`, 'owner', `session-${index}`, 'Chat'),
|
||||
contentType: 'agent-session' as const,
|
||||
agentSessionAgent: 'codex' as const
|
||||
}))
|
||||
const ids = tabs.map((tab) => tab.id)
|
||||
const other = { ...tabs[0], id: 'other', worktreeId: 'other', groupId: 'group-other' }
|
||||
useAppStore.setState({
|
||||
keybindings: initial.keybindings,
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: { owner: tabs, other: [other] },
|
||||
groupsByWorktree: {
|
||||
owner: [makeGroup('owner', ids)],
|
||||
other: [makeGroup('other', ['other'])]
|
||||
}
|
||||
})
|
||||
}
|
||||
const writes = [
|
||||
() => useAppStore.getState().setTabLabel('chat-1', `Title ${++revision}`),
|
||||
() => useAppStore.getState().activateTab('chat-1'),
|
||||
() => useAppStore.getState().setTabLabel('other', `Other ${++revision}`),
|
||||
() => useAppStore.getState().activateTab('other'),
|
||||
() => useAppStore.setState({ activeWorktreeId: 'unrelated' })
|
||||
]
|
||||
seed()
|
||||
const baseline = writes.map(measure)
|
||||
const idle = () => {
|
||||
act(seed)
|
||||
const before = renders
|
||||
expect(writes.map(measure)).toEqual(baseline)
|
||||
expect(renders).toBe(before)
|
||||
}
|
||||
const hosts = Array.from({ length: size }, (_, index) => <Host key={index} index={index} />)
|
||||
render(hosts, { wrapper: TooltipProvider })
|
||||
idle()
|
||||
fireEvent.contextMenu(screen.getByTestId('host-0'))
|
||||
await screen.findByRole('menu')
|
||||
expect(screen.getAllByRole('menuitem').map((item) => item.textContent)).toEqual([
|
||||
expect.stringContaining('Split Chat Right'),
|
||||
expect.stringContaining('Split Chat Down'),
|
||||
'Copy Orca Session ID'
|
||||
])
|
||||
const opened = renders
|
||||
const extra = measure(writes[0]) - baseline[0]
|
||||
expect(extra).toBeGreaterThanOrEqual(size - 1)
|
||||
expect(extra).toBeLessThanOrEqual(size * 2)
|
||||
expect(renders).toBe(opened)
|
||||
await new Promise((resolve) => setTimeout(resolve, 110))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Split Chat Right/ }))
|
||||
expect(runNativeChatSplitTarget).toHaveBeenLastCalledWith(
|
||||
{ kind: 'workspace-tab', unifiedTabId: 'chat-0', groupId: 'group-owner' },
|
||||
'right'
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
||||
idle()
|
||||
})
|
||||
@@ -70,10 +70,6 @@ vi.mock('@/lib/image-blob-png', async (importOriginal) => ({
|
||||
|
||||
vi.mock('./NativeChatSelectionQuote', () => ({ NativeChatSelectionQuote: () => null }))
|
||||
|
||||
vi.mock('@/components/tab-bar/TabWorkspaceLayoutMenuSection', () => ({
|
||||
TabWorkspaceLayoutMenuSection: () => 'Move Tab to Split'
|
||||
}))
|
||||
|
||||
function childrenText(children: ReactNode): string {
|
||||
return React.Children.toArray(children)
|
||||
.map((child) => {
|
||||
@@ -102,7 +98,6 @@ function Harness({
|
||||
composerRef: createRef(),
|
||||
onSwitchToTerminal,
|
||||
showTerminalPaneActions: !structured,
|
||||
workspaceLayout: structured ? { unifiedTabId: 'chat-tab', groupId: 'group-1' } : undefined,
|
||||
resolveOrcaSessionId: orcaSessionId === undefined ? undefined : async () => orcaSessionId,
|
||||
actions: {
|
||||
...emptyNativeChatContextMenuActions,
|
||||
@@ -424,14 +419,6 @@ describe('useNativeChatContextMenu', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('reuses workspace layout actions without terminal-only pane commands', () => {
|
||||
const markup = renderToStaticMarkup(<Harness structured />)
|
||||
|
||||
expect(markup).toContain('Move Tab to Split')
|
||||
expect(markup).not.toContain('Split Terminal Right')
|
||||
expect(markup).not.toContain('Fork Agent Session')
|
||||
})
|
||||
|
||||
describe('Copy Orca Session ID', () => {
|
||||
const orcaSessionId = 'orca_session_id:4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37'
|
||||
const writeClipboardText = vi.fn()
|
||||
|
||||
@@ -7,7 +7,16 @@ import {
|
||||
type PointerEventHandler,
|
||||
type RefObject
|
||||
} from 'react'
|
||||
import { Clipboard, Copy, GitFork, Image as ImageIcon, SquareTerminal } from 'lucide-react'
|
||||
import {
|
||||
Clipboard,
|
||||
Copy,
|
||||
GitFork,
|
||||
Image as ImageIcon,
|
||||
PanelBottomClose,
|
||||
PanelRightClose,
|
||||
SquareTerminal,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -23,15 +32,14 @@ import {
|
||||
type NativeChatCopyImage
|
||||
} from './native-chat-image-copy'
|
||||
import { isMacPlatform, nativeChatToggleShortcutLabel } from './native-chat-shortcut'
|
||||
import { TabWorkspaceLayoutMenuSection } from '@/components/tab-bar/TabWorkspaceLayoutMenuSection'
|
||||
import { canMoveTabToNewPaneColumn } from '@/components/tab-bar/tab-move-to-pane-column'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useStructuredChatPaneActions } from './use-structured-chat-pane-actions'
|
||||
import { isEditableTarget } from '@/lib/editable-target'
|
||||
import { findEditablePrimarySelectionPasteTarget } from '@/lib/primary-selection-paste'
|
||||
import { NativeChatCopyOrcaSessionIdMenuItem } from './NativeChatCopyOrcaSessionIdMenuItem'
|
||||
import { NativeChatSelectionQuote } from './NativeChatSelectionQuote'
|
||||
import { NativeChatPaneMenuItems } from './NativeChatPaneMenuItems'
|
||||
import type { NativeChatComposerHandle } from './native-chat-composer-types'
|
||||
import type { TabSplitDirection } from '@/store/slices/tabs'
|
||||
import { AgentSessionContinuationMenuItem } from '@/components/terminal-pane/AgentSessionContinuationMenuItem'
|
||||
import { AgentSessionContextCopyMenuItem } from '@/components/terminal-pane/AgentSessionContextCopyMenuItem'
|
||||
import {
|
||||
@@ -67,11 +75,7 @@ type UseNativeChatContextMenuArgs = {
|
||||
actions: NativeChatContextMenuActions
|
||||
showTerminalPaneActions?: boolean
|
||||
splitShortcutLabels?: { right: string; down: string }
|
||||
workspaceLayout?: {
|
||||
unifiedTabId: string
|
||||
groupId: string
|
||||
shortcutLabels?: Partial<Record<TabSplitDirection, string>>
|
||||
}
|
||||
chatPane?: { tabId: string; worktreeId: string; groupId: string }
|
||||
/** A structured chat tab's Orca session ID; a chat in a terminal pane is that terminal's agent. */
|
||||
resolveOrcaSessionId?: () => Promise<string | null>
|
||||
} & NativeChatContextMenuTargetArgs
|
||||
@@ -142,7 +146,7 @@ export function useNativeChatContextMenu({
|
||||
actions,
|
||||
showTerminalPaneActions = true,
|
||||
splitShortcutLabels,
|
||||
workspaceLayout,
|
||||
chatPane,
|
||||
resolveOrcaSessionId,
|
||||
messageListRef,
|
||||
fileLinkContext = null,
|
||||
@@ -173,11 +177,8 @@ export function useNativeChatContextMenu({
|
||||
Boolean(target.quoteText ?? target.link ?? target.codeText ?? target.messageText) ||
|
||||
(target.rewindItemId !== null && rewind !== undefined)
|
||||
const hasSessionItems = onFind !== undefined || showTerminalPaneActions
|
||||
const showWorkspaceLayout =
|
||||
workspaceLayout !== undefined &&
|
||||
canMoveTabToNewPaneColumn(workspaceLayout.unifiedTabId, workspaceLayout.groupId)
|
||||
const hasItems =
|
||||
hasEditItems || hasSessionItems || showWorkspaceLayout || resolveOrcaSessionId !== undefined
|
||||
hasEditItems || hasSessionItems || chatPane !== undefined || resolveOrcaSessionId !== undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
@@ -317,21 +318,19 @@ export function useNativeChatContextMenu({
|
||||
<AgentSessionContextCopyMenuItem onSelect={actions.onCopyAgentSessionContext} />
|
||||
</>
|
||||
) : null}
|
||||
{workspaceLayout ? (
|
||||
<TabWorkspaceLayoutMenuSection
|
||||
unifiedTabId={workspaceLayout.unifiedTabId}
|
||||
groupId={workspaceLayout.groupId}
|
||||
{chatPane ? (
|
||||
<StructuredChatPaneMenuItems
|
||||
pane={chatPane}
|
||||
leadingSeparator={hasEditItems || hasSessionItems}
|
||||
shortcutLabels={workspaceLayout.shortcutLabels}
|
||||
splitShortcutLabels={splitShortcutLabels}
|
||||
resolveOrcaSessionId={resolveOrcaSessionId}
|
||||
/>
|
||||
) : null}
|
||||
{showTerminalPaneActions ? (
|
||||
<NativeChatPaneMenuItems actions={actions} splitShortcutLabels={splitShortcutLabels} />
|
||||
) : resolveOrcaSessionId ? (
|
||||
) : !chatPane && resolveOrcaSessionId ? (
|
||||
<>
|
||||
{hasEditItems || hasSessionItems || showWorkspaceLayout ? (
|
||||
<DropdownMenuSeparator />
|
||||
) : null}
|
||||
{hasEditItems || hasSessionItems ? <DropdownMenuSeparator /> : null}
|
||||
<NativeChatCopyOrcaSessionIdMenuItem resolveOrcaSessionId={resolveOrcaSessionId} />
|
||||
</>
|
||||
) : null}
|
||||
@@ -341,6 +340,51 @@ export function useNativeChatContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
function StructuredChatPaneMenuItems({
|
||||
pane,
|
||||
leadingSeparator,
|
||||
splitShortcutLabels,
|
||||
resolveOrcaSessionId
|
||||
}: Pick<UseNativeChatContextMenuArgs, 'splitShortcutLabels' | 'resolveOrcaSessionId'> & {
|
||||
pane: NonNullable<UseNativeChatContextMenuArgs['chatPane']>
|
||||
leadingSeparator: boolean
|
||||
}) {
|
||||
const actions = useStructuredChatPaneActions(pane.tabId, pane.worktreeId, pane.groupId)
|
||||
const hasSplitPanes = useAppStore(
|
||||
(state) => (state.groupsByWorktree[pane.worktreeId]?.length ?? 0) > 1
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{leadingSeparator ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem onSelect={actions.onSplitRight}>
|
||||
<PanelRightClose />
|
||||
{translate('auto.components.cmd.j.quick.actions.splitChatRight', 'Split Chat Right')}
|
||||
<DropdownMenuShortcut>{splitShortcutLabels?.right}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={actions.onSplitDown}>
|
||||
<PanelBottomClose />
|
||||
{translate('auto.components.cmd.j.quick.actions.splitChatDown', 'Split Chat Down')}
|
||||
<DropdownMenuShortcut>{splitShortcutLabels?.down}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
{resolveOrcaSessionId ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<NativeChatCopyOrcaSessionIdMenuItem resolveOrcaSessionId={resolveOrcaSessionId} />
|
||||
</>
|
||||
) : null}
|
||||
{hasSplitPanes && actions.canClosePane ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onSelect={actions.onClosePane}>
|
||||
<X />
|
||||
{actions.closeLabel}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function keepSelectionThroughMenuPress(event: React.PointerEvent<HTMLElement>): void {
|
||||
// Why: left alone, the press that opens the menu collapses the selection before contextmenu fires.
|
||||
if (
|
||||
|
||||
+45
-17
@@ -7,12 +7,22 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as I18n from '@/i18n/i18n'
|
||||
import type { NativeChatComposerHandle } from './NativeChatComposer'
|
||||
import { useStructuredNativeChatPaneCommands } from './use-structured-native-chat-pane-commands'
|
||||
import { moveTabToNewPaneColumn } from '@/components/tab-bar/tab-move-to-pane-column'
|
||||
|
||||
type ItemProps = { onSelect?: () => void; children?: ReactNode }
|
||||
|
||||
const items = vi.hoisted(() => ({ list: [] as ItemProps[] }))
|
||||
const callRuntimeRpc = vi.hoisted(() => vi.fn())
|
||||
const paneActions = vi.hoisted(() => ({
|
||||
onSplitRight: vi.fn(() => true),
|
||||
onSplitDown: vi.fn(() => true),
|
||||
canClosePane: true,
|
||||
closeLabel: 'Close Pane',
|
||||
onClosePane: vi.fn()
|
||||
}))
|
||||
const store = vi.hoisted(() => ({
|
||||
keybindings: {},
|
||||
groupsByWorktree: { 'worktree-1': [{ id: 'group-1' }] }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dropdown-menu', () => {
|
||||
const Pass = ({ children }: { children?: ReactNode }) => children
|
||||
@@ -41,12 +51,11 @@ vi.mock('@/components/ui/tooltip', () => {
|
||||
const Pass = ({ children }: { children?: ReactNode }) => children
|
||||
return { Tooltip: Pass, TooltipTrigger: Pass, TooltipContent: () => null }
|
||||
})
|
||||
vi.mock('@/components/tab-bar/tab-move-to-pane-column', () => ({
|
||||
canMoveTabToNewPaneColumn: () => true,
|
||||
moveTabToNewPaneColumn: vi.fn()
|
||||
vi.mock('./use-structured-chat-pane-actions', () => ({
|
||||
useStructuredChatPaneActions: () => paneActions
|
||||
}))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (select: (state: { keybindings: object }) => unknown) => select({ keybindings: {} })
|
||||
useAppStore: (select: (state: typeof store) => unknown) => select(store)
|
||||
}))
|
||||
vi.mock('../../runtime/runtime-rpc-client', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
@@ -68,9 +77,10 @@ function childrenText(children: ReactNode): string {
|
||||
.join('')
|
||||
}
|
||||
|
||||
function StructuredChatTab(): ReactNode {
|
||||
function StructuredChatTab({ owner = 'worktree-1' }: { owner?: string | null }): ReactNode {
|
||||
const { menu } = useStructuredNativeChatPaneCommands({
|
||||
tabId: 'chat-tab',
|
||||
ownerWorktreeId: owner,
|
||||
groupId: 'group-1',
|
||||
isVisible: true,
|
||||
rootRef: createRef<HTMLDivElement>(),
|
||||
@@ -85,23 +95,41 @@ function StructuredChatTab(): ReactNode {
|
||||
describe('a structured chat tab', () => {
|
||||
beforeEach(() => {
|
||||
items.list = []
|
||||
vi.clearAllMocks()
|
||||
store.groupsByWorktree['worktree-1'] = [{ id: 'group-1' }]
|
||||
callRuntimeRpc.mockReset().mockResolvedValue({ orcaSessionId: `orca_session_id:${SESSION}` })
|
||||
Object.assign(window, {
|
||||
api: { ui: { writeClipboardText: vi.fn().mockResolvedValue(undefined) } }
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps Move Tab to Split without fresh-chat shortcut hints', () => {
|
||||
renderToStaticMarkup(<StructuredChatTab />)
|
||||
const right = items.list.find((item) => childrenText(item.children) === 'Right')
|
||||
expect(right).toBeDefined()
|
||||
expect(items.list.some((item) => childrenText(item.children) === 'Down')).toBe(true)
|
||||
right?.onSelect?.()
|
||||
expect(moveTabToNewPaneColumn).toHaveBeenCalledWith({
|
||||
unifiedTabId: 'chat-tab',
|
||||
groupId: 'group-1',
|
||||
direction: 'right'
|
||||
})
|
||||
it.each([false, true])('uses the header commands with split panes: %s', (split) => {
|
||||
if (split) {
|
||||
store.groupsByWorktree['worktree-1'].push({ id: 'group-2' })
|
||||
}
|
||||
const markup = renderToStaticMarkup(<StructuredChatTab />)
|
||||
expect(markup).not.toContain('Move Tab to Split')
|
||||
expect(markup).not.toContain('Resume in New CLI')
|
||||
expect(markup).not.toContain('Continue with Terminal Agent')
|
||||
for (const [label, command] of [
|
||||
['Split Chat Right', paneActions.onSplitRight],
|
||||
['Split Chat Down', paneActions.onSplitDown]
|
||||
] as const) {
|
||||
const item = items.list.find((entry) => childrenText(entry.children).startsWith(label))
|
||||
expect(item).toBeDefined()
|
||||
item?.onSelect?.()
|
||||
expect(command).toHaveBeenCalledOnce()
|
||||
}
|
||||
const close = items.list.find((entry) => childrenText(entry.children) === 'Close Pane')
|
||||
expect(Boolean(close)).toBe(split)
|
||||
close?.onSelect?.()
|
||||
expect(paneActions.onClosePane).toHaveBeenCalledTimes(split ? 1 : 0)
|
||||
})
|
||||
|
||||
it.each([null, ''])('keeps the session-ID fallback without a known owner: %s', (owner) => {
|
||||
const markup = renderToStaticMarkup(<StructuredChatTab owner={owner} />)
|
||||
expect(markup).not.toContain('Split Chat')
|
||||
expect(markup).toContain('Copy Orca Session ID')
|
||||
})
|
||||
|
||||
it("offers Copy Orca Session ID, asking the tab's host for this session's Orca session ID", async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { runNativeChatSplitTarget } from './native-chat-layout-actions'
|
||||
|
||||
export function useStructuredNativeChatPaneCommands({
|
||||
tabId,
|
||||
ownerWorktreeId,
|
||||
groupId,
|
||||
isVisible,
|
||||
rootRef,
|
||||
@@ -29,6 +30,7 @@ export function useStructuredNativeChatPaneCommands({
|
||||
...menuTargets
|
||||
}: {
|
||||
tabId: string
|
||||
ownerWorktreeId: string | null
|
||||
groupId?: string
|
||||
isVisible: boolean
|
||||
rootRef: RefObject<HTMLDivElement | null>
|
||||
@@ -67,12 +69,9 @@ export function useStructuredNativeChatPaneCommands({
|
||||
right: formatShortcutLabel('terminal.splitRight', keybindings),
|
||||
down: formatShortcutLabel('terminal.splitDown', keybindings)
|
||||
},
|
||||
workspaceLayout:
|
||||
groupId && terminalPaneActions === undefined
|
||||
? {
|
||||
unifiedTabId: tabId,
|
||||
groupId
|
||||
}
|
||||
chatPane:
|
||||
ownerWorktreeId && groupId && terminalPaneActions === undefined
|
||||
? { tabId, worktreeId: ownerWorktreeId, groupId }
|
||||
: undefined
|
||||
})
|
||||
const onKeyDownCapture = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
||||
|
||||
Reference in New Issue
Block a user