Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f086c3a
Transfer keyboard focus when a sibling chat pane opens
brennanb2025 Oct 15, 2026
0ed5bb4
fix(native-chat): hand focus to newly opened chat composer
brennanb2025 Oct 15, 2026
6167d52
fix(native-chat): keep tab bar controller within line limit
brennanb2025 Oct 15, 2026
a66078c
fix(chat): split into a fresh conversation beside the current chat
brennanb2025 Oct 15, 2026
7a09e4a
test(chat): declare the split fixture store without an assertion
brennanb2025 Oct 15, 2026
9895b77
fix(chat): route the split to the source conversation host
brennanb2025 Oct 15, 2026
55b97cf
test(chat): keep the split regression focused on the launch contract
brennanb2025 Oct 15, 2026
64f5843
test(chat): give the command palette split fixture its conversation a…
brennanb2025 Oct 15, 2026
c41c2fa
test(chat): seed the legacy command fixture without expanding its ass…
brennanb2025 Oct 15, 2026
4302dc3
test(chat): isolate pane identity commands from the split launch boun…
brennanb2025 Oct 15, 2026
e70e185
Prevent chat splits from starting a terminal on host refusal
brennanb2025 Oct 15, 2026
90d2d89
Cancel structured launches when their destination pane closes
brennanb2025 Oct 15, 2026
c2fcf4b
Model live destination groups in agent launch test fixture
brennanb2025 Oct 15, 2026
d56006f
Capture split chat focus intent at the user action
brennanb2025 Oct 15, 2026
72dfe8b
feat(chat): add the terminal pane hover action cluster
brennanb2025 Oct 15, 2026
2ceba76
fix(chat): keep pane controls above the conversation surface
brennanb2025 Oct 15, 2026
00bf68c
refactor(chat): share the tab resume launch hook and format pane cont…
brennanb2025 Oct 15, 2026
02dbb1e
style(chat): document reuse of the terminal header button styles
brennanb2025 Oct 15, 2026
7ee4392
refactor(ui): share the pane title button variants between terminals …
brennanb2025 Oct 15, 2026
813208f
fix(chat): keep header actions scoped to the displayed chat
brennanb2025 Oct 15, 2026
591e393
fix(chat): abandon Resume when its destination pane closes
brennanb2025 Oct 15, 2026
fe5d91e
test(chat): declare destination fixtures without type assertions
brennanb2025 Oct 15, 2026
d5dfe40
fix(chat): keep Resume visible when its source pane closes
brennanb2025 Oct 15, 2026
53c3dbe
fix(chat): reuse tab menu Resume placement unchanged
brennanb2025 Oct 15, 2026
81242ac
test: fix Resume regression harness setup
brennanb2025 Oct 15, 2026
ca269e1
fix(chat): hand input to the split pane during admission
Oct 15, 2026
8fe8cd3
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 21, 2026
8cb9fd3
fix(chat): Split opens a fresh chat beside the current one
brennanb2025 Oct 21, 2026
46dfe6a
fix(chat): Split palette search words describe a new chat, not a move
brennanb2025 Oct 21, 2026
d3b4c75
fix: remove misleading chat move shortcut hints
brennanb2025 Oct 21, 2026
b904956
fix(chat): a new chat stays in its pane until its host lists it
brennanb2025 Oct 21, 2026
e64c5c3
test(chat): type the faked create replies without an assertion
brennanb2025 Oct 21, 2026
b2c663c
Keep new chat protection tied to accepted host lists
brennanb2025 Oct 21, 2026
8abd33c
Update paired chat close regression to current suppression API
brennanb2025 Oct 21, 2026
e23b07a
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 21, 2026
5808d99
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 21, 2026
267a686
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 21, 2026
e382d82
fix(chat): Portuguese Split palette words describe a new chat
brennanb2025 Oct 21, 2026
3687253
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 22, 2026
9551884
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 22, 2026
7d78d91
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 22, 2026
29731df
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 22, 2026
ba54568
Merge remote-tracking branch 'origin/main' into brennankb5/native-cha…
brennanb2025 Oct 22, 2026
9c3ec3f
Merge current split chat base and reuse its pane actions
brennanb2025 Oct 22, 2026
649d734
test(chat): use the destination group option in split fixture
brennanb2025 Oct 22, 2026
1d25bcc
Avoid History projection in native chat header and currentness checks
brennanb2025 Oct 22, 2026
f0e90c8
fix(native-chat): keep Find below pane controls
brennanb2025 Oct 22, 2026
8391b9f
fix(ui): keep noninteractive tooltip layers pointer-transparent
brennanb2025 Oct 22, 2026
91dcc66
Merge main into native chat pane actions
brennanb2025 Oct 22, 2026
db1c34f
Merge origin/main into brennankb5/native-chat-pane-actions
brennanb2025 Oct 23, 2026
c088804
Mount native chat actions only for shown panes
brennanb2025 Oct 23, 2026
dde77e3
Use checked property access in header instrumentation
brennanb2025 Oct 23, 2026
cee8797
Read Continue eligibility records through workspace selectors
brennanb2025 Oct 23, 2026
2c8e562
Cover native header Find clearance and tooltip click passage
brennanb2025 Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions docs/automated-setup-tab-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ This is PR 2. [PR 1 (#29810)](https://github.com/stablyai/orca/pull/29810) separ

## Evidence and severity

* Confirmed pre-change mechanism: `shouldSeedWorktreePrimaryTerminal` treated split setup as a reason to create a shell even when the caller supplies native chat. Renderer `queueSetupAndIssueCommands`, runtime `provisionWorktreeTerminals`, and `spawnLocalStartupAndSetupTerminals` in `worktree-remote.ts` each branched on setup location. This PR reuses their existing dedicated-tab arms.
* The supplied incident investigation observed two leaves bound to one PTY and an independent equal-count chat-publication failure. This worktree does not establish the original missing-binding cause or two setup executions. Removing placement complexity is the user-requested product policy; it is not proof of fixing every frozen terminal.
* No demonstrated P0. P1 acceptance risks are replaying setup after host provisioning, losing requested startup/setup sequencing, or changing primary selection/background workspace selection. The validation below exercises actual creation routes and the preserved one-shot startup mechanisms.
* P2 tradeoff: automatic side-by-side setup placement disappears; manual splitting remains. Older execution hosts can still return existing split layouts, which updated clients must accept without moving/replaying setup.
* The first-terminal delayed delivery coupling exists in `ensureWorktreeHasInitialTerminal` for runtime-owned fallback commands. Ordinary desktop structured creation passes no backend startup and returns setup through local IPC, then uses host authority `none`, so its null-primary Setup path is already independent. The host native-chat route normally consumes setup before returning. Keep delayed delivery unchanged: no actual request proves the alleged native-chat P1. The post-PR-1 semantic review found no reason to expand this scope. The conditional STA-9417 expected-failure fixture does not establish the original incident's cause.
- Confirmed pre-change mechanism: `shouldSeedWorktreePrimaryTerminal` treated split setup as a reason to create a shell even when the caller supplies native chat. Renderer `queueSetupAndIssueCommands`, runtime `provisionWorktreeTerminals`, and `spawnLocalStartupAndSetupTerminals` in `worktree-remote.ts` each branched on setup location. This PR reuses their existing dedicated-tab arms.
- The supplied incident investigation observed two leaves bound to one PTY and an independent equal-count chat-publication failure. This worktree does not establish the original missing-binding cause or two setup executions. Removing placement complexity is the user-requested product policy; it is not proof of fixing every frozen terminal.
- No demonstrated P0. P1 acceptance risks are replaying setup after host provisioning, losing requested startup/setup sequencing, or changing primary selection/background workspace selection. The validation below exercises actual creation routes and the preserved one-shot startup mechanisms.
- P2 tradeoff: automatic side-by-side setup placement disappears; manual splitting remains. Older execution hosts can still return existing split layouts, which updated clients must accept without moving/replaying setup.
- The first-terminal delayed delivery coupling exists in `ensureWorktreeHasInitialTerminal` for runtime-owned fallback commands. Ordinary desktop structured creation passes no backend startup and returns setup through local IPC, then uses host authority `none`, so its null-primary Setup path is already independent. The host native-chat route normally consumes setup before returning. Keep delayed delivery unchanged: no actual request proves the alleged native-chat P1. The post-PR-1 semantic review found no reason to expand this scope. The conditional STA-9417 expected-failure fixture does not establish the original incident's cause.

## Surfaces and selection

| Intent | Surfaces owed by this create with setup |
| --- | --- |
| Native chat | Requested chat plus Setup, without a split-anchor shell. |
| Terminal agent | Requested agent terminal plus Setup. |
| Plain terminal | Normal primary terminal plus Setup. |
| Setup only | Setup alone; preserve current suppression of default tabs. |
| Intent | Surfaces owed by this create with setup |
| --------------------- | -------------------------------------------------------------------- |
| Native chat | Requested chat plus Setup, without a split-anchor shell. |
| Terminal agent | Requested agent terminal plus Setup. |
| Plain terminal | Normal primary terminal plus Setup. |
| Setup only | Setup alone; preserve current suppression of default tabs. |
| Explicit default tabs | Existing template allocation/commands/title/color policy plus Setup. |
| No setup | Existing behavior. |
| No setup | Existing behavior. |

Issue automation retains its independent split from the normal primary pane, command/env, one-shot behavior, and focus policy. Setup stays unselected when another primary surface exists. Setup-only can select Setup when creation is still in view. Background creates/navigation away preserve workspace/tab selection and never reveal/focus a native window.

Expand Down Expand Up @@ -59,7 +59,6 @@ Rendered checks require rebuilt hidden Electron plus Playwright CDP; inspect tab

After implementation: Astra reviews actual final diff/tests and semantic overlap after PR 1 rebase. No merge until PR 1 confirmation, exact-head full `.github/workflows/pr.yml` successful aggregate `verify` / PR checks passed and all required checks, and parent approval through the mandatory blocking merge-gate ask. Use `--match-head-commit` and confirm the merge.


## Implemented validation record

Reviewed source head: `d96ae2ca6afdca919b4d7f8d85308363abe8b28b`, based on the exact PR 1 merge above. The follow-up documentation commit changes no source behavior. The independent review records no unresolved P0/P1 and explicit P2 tradeoffs.
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,12 @@ html.native-shell .app-layout {
-webkit-app-region: no-drag;
}

/* Modal inline pointer styles must not turn noninteractive labels into hit targets. */
[data-radix-popper-content-wrapper]:has(> [data-slot='tooltip-content'].pointer-events-none),
[data-slot='tooltip-content'].pointer-events-none {
pointer-events: none !important;
}

.titlebar {
height: 36px;
min-height: 36px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function NativeChatFindBar({
// The chat sends clicks on its plain surface to the composer; the bar's are its own.
data-native-chat-typing-redirect-ignore="true"
data-native-chat-find-bar="true"
className="absolute top-2 right-4 z-20 flex w-85 max-w-[calc(100%-2rem)] items-center gap-1 rounded-lg border border-border bg-popover/95 px-2 py-1 text-popover-foreground shadow-floating backdrop-blur-sm"
className="absolute top-[calc(var(--native-chat-find-top-inset,0px)+var(--spacing)*2)] right-4 z-20 flex w-85 max-w-[calc(100%-2rem)] items-center gap-1 rounded-lg border border-border bg-popover/95 px-2 py-1 text-popover-foreground shadow-floating backdrop-blur-sm"
onMouseDown={(event) => {
// A press on the count or padding keeps typing in the find input.
if (!(event.target instanceof Element) || !event.target.closest('button, input')) {
Expand Down
117 changes: 117 additions & 0 deletions src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { useAppStore } from '@/store'
import { createTabsSliceMockApi } from '@/store/slices/tabs-slice-test-harness'
import { makeFolderWorkspace } from '@/store/slices/worktrees-slice-test-fixtures'
import * as historyRequest from '../right-sidebar/ai-vault-panel-session-list-request'

const mocks = vi.hoisted(() => ({ launch: vi.fn(), resume: vi.fn(), close: vi.fn() }))
vi.mock('@/store', async () => {
const { createTestStore } = await import('@/store/slices/store-test-helpers')
return { useAppStore: createTestStore() }
})
vi.mock('@/lib/launch-agent-in-new-tab', () => ({ launchAgentInNewTab: mocks.launch }))
vi.mock('@/lib/workspace-tab-commands', () => ({ dispatchWorkspaceTabCommand: mocks.close }))
vi.mock('../tab-bar/native-chat-cli-fork-run', () => ({
runNativeChatCliFork: mocks.resume,
useNativeChatCliForkPending: () => false
}))

import { NativeChatPaneHeader } from './NativeChatPaneHeader'

const fullRequest = vi.spyOn(historyRequest, 'resolveAiVaultPanelSessionListRequest')
const testWindow = window
const api = createTabsSliceMockApi()
vi.stubGlobal('window', testWindow)
Object.assign(window, { api })
afterEach(cleanup)
beforeEach(() => {
vi.clearAllMocks()
useAppStore.setState(useAppStore.getInitialState(), true)
useAppStore.setState({
folderWorkspaces: [makeFolderWorkspace({ id: 'workspace', folderPath: '/repo' })]
})
})

function renderHeader() {
const source = useAppStore.getState().createUnifiedTab('folder:workspace', 'agent-session', {
agentSessionAgent: 'codex',
executionHostId: 'runtime:remote-host'
})
render(
<TooltipProvider>
<NativeChatPaneHeader
tabId={source.id}
worktreeId={source.worktreeId}
groupId={source.groupId}
nativeChat={{ sessionId: source.entityId, agent: 'codex' }}
focused
colorScheme="dark"
/>
</TooltipProvider>
)
return source
}

it('offers the terminal header action order and targets Resume and Close at the displayed chat', () => {
const source = renderHeader()
expect(screen.getAllByRole('button').map((button) => button.getAttribute('aria-label'))).toEqual([
'Continue with Terminal Agent',
'Split Chat Right',
'Close tab'
])
const folder = useAppStore.getState().folderWorkspaces[0]
act(() => useAppStore.setState({ folderWorkspaces: [{ ...folder, executionHostId: 'ssh:dev' }] }))
expect(screen.queryByRole('button', { name: 'Continue with Terminal Agent' })).toBeNull()
act(() => useAppStore.setState({ folderWorkspaces: [folder] }))
expect(fullRequest).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Continue with Terminal Agent' }))
expect(fullRequest).toHaveBeenCalledTimes(1)
expect(mocks.resume).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
subject: expect.objectContaining({
sessionId: source.entityId,
request: { scopePaths: ['/repo'], executionHostScope: 'local', sessionLimit: 250 }
}),
tab: { id: source.id, worktreeId: source.worktreeId },
agent: 'codex'
})
)
fireEvent.click(screen.getByRole('button', { name: 'Close tab' }))
expect(mocks.close).toHaveBeenCalledExactlyOnceWith({
type: 'close',
target: { kind: 'tab', worktreeId: source.worktreeId, tabId: source.id }
})
})

it('Split from the header keeps the source and opens a fresh chat on the same host', () => {
const source = renderHeader()
mocks.launch.mockImplementation((args: { worktreeId: string; groupId: string }) => {
useAppStore.getState().createUnifiedTab(args.worktreeId, 'agent-session', {
targetGroupId: args.groupId,
agentSessionAgent: 'codex',
executionHostId: 'runtime:remote-host'
})
return { surface: { kind: 'local-chat', tabId: 'fresh-chat' } }
})
fireEvent.click(screen.getByRole('button', { name: 'Split Chat Right' }))
const state = useAppStore.getState()
const destination = state.activeGroupIdByWorktree[source.worktreeId]
expect(destination).not.toBe(source.groupId)
expect(mocks.launch).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
agent: 'codex',
requestedSurface: 'chat',
selectedExecutionHostId: source.executionHostId,
worktreeId: source.worktreeId,
groupId: destination,
focusIntent: expect.objectContaining({ cancel: expect.any(Function) })
})
)
const tabs = state.unifiedTabsByWorktree[source.worktreeId]
expect(tabs.find((tab) => tab.id === source.id)).toEqual(source)
expect(tabs).toHaveLength(2)
expect(tabs.find((tab) => tab.groupId === destination)?.entityId).not.toBe(source.entityId)
})
124 changes: 124 additions & 0 deletions src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { SquareSplitVertical, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import { agentForksConversations } from '../../../../shared/agent-resume-argv'
import type { NativeChatTabSession } from '../tab-bar/tab-session-history-switch'
import { useSystemPrefersDark } from '../terminal-pane/use-system-prefers-dark'
import { useStructuredChatPaneActions } from './use-structured-chat-pane-actions'
import { NativeChatResumeInCliButton } from './NativeChatResumeInCliButton'
import type { NativeChatStructuredViewProps } from './native-chat-view-types'

const HEADER_STYLE: React.CSSProperties & Record<'--orca-pane-title-bg', string> = {
'--orca-pane-title-bg': 'var(--chat-canvas)'
}

export function NativeChatPaneHeader({
tabId,
worktreeId,
groupId,
nativeChat,
focused,
colorScheme
}: {
tabId: string
worktreeId: string
groupId: string
nativeChat: NativeChatTabSession
focused: boolean
colorScheme: React.CSSProperties['colorScheme']
}): React.JSX.Element {
const actions = useStructuredChatPaneActions(tabId, worktreeId, groupId)
const theme = useAppStore((state) => state.settings?.theme)
const systemDark = useSystemPrefersDark()
const light = colorScheme
? colorScheme === 'light'
: theme === 'light' || (theme === 'system' && !systemDark)
const split = translate('auto.components.cmd.j.quick.actions.splitChatRight', 'Split Chat Right')
return (
<div
className="pane-title-bar z-20"
data-chromeless=""
data-active-pane={focused ? '' : undefined}
data-pane-title-surface={light ? 'light' : 'dark'}
style={HEADER_STYLE}
>
<div className="pane-title-actions ml-auto flex shrink-0 items-center gap-0">
{nativeChat.agent && agentForksConversations(nativeChat.agent) ? (
<NativeChatResumeInCliButton
tabId={tabId}
worktreeId={worktreeId}
nativeChat={nativeChat}
/>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="pane-title"
size="icon-xs"
aria-label={split}
onClick={(event) => {
event.stopPropagation()
actions.onSplitRight()
}}
>
<SquareSplitVertical className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{split}
</TooltipContent>
</Tooltip>
{actions.canClosePane ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="pane-title-close"
size="icon-xs"
aria-label={actions.closeLabel}
onClick={(event) => {
event.stopPropagation()
actions.onClosePane()
}}
>
<X className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{actions.closeLabel}
</TooltipContent>
</Tooltip>
) : null}
</div>
</div>
)
}

export function nativeChatPaneHeader(
view: Pick<
NativeChatStructuredViewProps,
| 'tabId'
| 'groupId'
| 'sessionId'
| 'agent'
| 'isVisible'
| 'isFocusedGroup'
| 'contextMenuActions'
>,
worktreeId: string | null,
colorScheme: React.CSSProperties['colorScheme']
): React.JSX.Element | null {
return view.isVisible && worktreeId && view.groupId && !view.contextMenuActions ? (
<NativeChatPaneHeader
tabId={view.tabId}
worktreeId={worktreeId}
groupId={view.groupId}
nativeChat={{ sessionId: view.sessionId, agent: view.agent }}
focused={view.isFocusedGroup}
colorScheme={colorScheme}
/>
) : null
}
Loading
Loading