fix: keep floating tabs local with active runtime

Keep the synthetic floating workspace local while a remote runtime is active, including terminal/browser creation, activation, close, and remote snapshot handling.

Maintainer follow-ups:
- require worktreeId for runtime-session terminal create payloads
- add renderer-backed terminal create reply sender regression coverage
- merge current main and keep the WSL readDir breadcrumb test aligned with main's Windows-only handler coverage
This commit is contained in:
Wolfie
2026-06-30 12:48:26 -07:00
committed by GitHub
parent 01070198fa
commit 62ab2d470e
22 changed files with 535 additions and 268 deletions
+11 -2
View File
@@ -151,6 +151,7 @@ describe('RuntimeBrowserCommands browser screencast', () => {
it('creates the first explicit-worktree browser tab without waiting for an existing registration', async () => {
const { RuntimeBrowserCommands } = await import('./orca-runtime-browser')
const webContents = { send: vi.fn() }
const send = vi.fn((channel: string, data: { requestId: string }) => {
expect(channel).toBe('browser:requestTabCreate')
const handler = ipcMainOnMock.mock.calls.find(
@@ -161,8 +162,16 @@ describe('RuntimeBrowserCommands browser screencast', () => {
reply: { requestId: string; browserPageId?: string; error?: string }
) => void)
| undefined
handler?.({} as never, { requestId: data.requestId, browserPageId: 'page-new' })
handler?.({ sender: { send: vi.fn() } } as never, {
requestId: data.requestId,
error: 'spoofed renderer reply'
})
handler?.({ sender: webContents } as never, {
requestId: data.requestId,
browserPageId: 'page-new'
})
})
webContents.send = send
const bridge = {
getRegisteredTabs: vi.fn(() => new Map([['page-new', 101]])),
getActivePageId: vi.fn(() => 'page-new'),
@@ -173,7 +182,7 @@ describe('RuntimeBrowserCommands browser screencast', () => {
createHost({
getAgentBrowserBridge: () => bridge,
getAvailableAuthoritativeWindow: vi.fn(() => ({}) as never),
getAuthoritativeWindow: vi.fn(() => ({ webContents: { send } }) as never)
getAuthoritativeWindow: vi.fn(() => ({ webContents }) as never)
})
)
+2 -2
View File
@@ -1795,10 +1795,10 @@ export class RuntimeBrowserCommands {
}, 10_000)
const handler = (
_event: Electron.IpcMainEvent,
event: Electron.IpcMainEvent,
reply: { requestId: string; browserPageId?: string; error?: string }
): void => {
if (reply.requestId !== requestId) {
if (event.sender !== win.webContents || reply.requestId !== requestId) {
return
}
clearTimeout(timer)
+99 -11
View File
@@ -7305,6 +7305,78 @@ describe('OrcaRuntimeService', () => {
)
})
it('accepts renderer-backed terminal create replies only from the target renderer', async () => {
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string }) => {
ipcMain.emit(
'terminal:tabCreateReply',
{ sender: { send: vi.fn() } },
{ requestId: payload.requestId, error: 'spoofed renderer reply' }
)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-renderer',
worktreeId: TEST_WORKTREE_ID,
title: 'Renderer Terminal',
activeLeafId: 'pane:1',
layout: null
}
],
leaves: [
{
tabId: 'tab-renderer',
worktreeId: TEST_WORKTREE_ID,
leafId: 'pane:1',
paneRuntimeId: 1,
ptyId: 'pty-renderer',
paneTitle: null
}
]
})
ipcMain.emit(
'terminal:tabCreateReply',
{ sender: webContents },
{ requestId: payload.requestId, tabId: 'tab-renderer', title: 'Renderer Terminal' }
)
})
webContents.send = send
const runtime = new OrcaRuntimeService(store)
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents
})
await expect(
runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
command: 'codex',
rendererBacked: true,
title: 'Renderer Terminal'
})
).resolves.toMatchObject({
handle: expect.stringMatching(/^term_/),
tabId: 'tab-renderer',
title: 'Renderer Terminal',
worktreeId: TEST_WORKTREE_ID,
surface: 'visible'
})
expect(send).toHaveBeenCalledWith(
'terminal:requestTabCreate',
expect.objectContaining({
requestId: expect.any(String),
worktreeId: TEST_WORKTREE_ID,
command: 'codex',
title: 'Renderer Terminal'
})
)
expect(electronMocks.ipcMain.removeListener).toHaveBeenCalledWith(
'terminal:tabCreateReply',
expect.any(Function)
)
})
it('splits visible pty-backed terminal sessions through the parent renderer tab', async () => {
const spawn = vi
.fn()
@@ -15888,7 +15960,13 @@ describe('OrcaRuntimeService', () => {
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string; activate?: boolean }) => {
ipcMain.emit(
'terminal:tabCreateReply',
{ sender: { send: vi.fn() } },
{ requestId: payload.requestId, error: 'spoofed renderer reply' }
)
runtime.syncWindowGraph(1, {
tabs: [],
leaves: [
@@ -15924,7 +16002,7 @@ describe('OrcaRuntimeService', () => {
})
ipcMain.emit(
'terminal:tabCreateReply',
{},
{ sender: webContents },
{
requestId: payload.requestId,
tabId: 'tab-renderer',
@@ -15932,11 +16010,12 @@ describe('OrcaRuntimeService', () => {
}
)
})
webContents.send = send
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents: { send }
webContents
})
const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
@@ -15947,7 +16026,8 @@ describe('OrcaRuntimeService', () => {
'terminal:requestTabCreate',
expect.objectContaining({
worktreeId: TEST_WORKTREE_ID,
activate: false
activate: false,
source: 'runtime-session'
})
)
expect(focusTerminal).not.toHaveBeenCalled()
@@ -15971,6 +16051,7 @@ describe('OrcaRuntimeService', () => {
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string }) => {
runtime.syncWindowGraph(1, {
tabs: [],
@@ -16007,15 +16088,16 @@ describe('OrcaRuntimeService', () => {
})
ipcMain.emit(
'terminal:tabCreateReply',
{},
{ sender: webContents },
{ requestId: payload.requestId, tabId: 'tab-renderer', title: 'Terminal' }
)
})
webContents.send = send
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents: { send }
webContents
})
const [first, second] = await Promise.all([
@@ -16072,20 +16154,22 @@ describe('OrcaRuntimeService', () => {
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string; worktreeId: string }) => {
const parentTabId =
payload.worktreeId === TEST_WORKTREE_ID ? 'tab-renderer-a' : 'tab-renderer-b'
ipcMain.emit(
'terminal:tabCreateReply',
{},
{ sender: webContents },
{ requestId: payload.requestId, tabId: parentTabId, title: 'Terminal' }
)
})
webContents.send = send
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents: { send }
webContents
})
const firstCreate = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
@@ -16200,18 +16284,20 @@ describe('OrcaRuntimeService', () => {
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string }) => {
ipcMain.emit(
'terminal:tabCreateReply',
{},
{ sender: webContents },
{ requestId: payload.requestId, tabId: 'tab-pending', title: 'Terminal' }
)
})
webContents.send = send
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents: { send }
webContents
})
const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
@@ -16309,18 +16395,20 @@ describe('OrcaRuntimeService', () => {
})
// Why: reply with a tabId but never sync a matching surface graph, so
// waitForMobileTerminalSurface times out and the rollback path runs.
const webContents = { send: vi.fn() }
const send = vi.fn((_channel: string, payload: { requestId: string }) => {
ipcMain.emit(
'terminal:tabCreateReply',
{},
{ sender: webContents },
{ requestId: payload.requestId, tabId: 'tab-ghost', title: 'Terminal' }
)
})
webContents.send = send
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
electronMocks.BrowserWindow.fromId.mockReturnValue({
isDestroyed: () => false,
webContents: { send }
webContents
})
const pending = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
+5 -4
View File
@@ -15373,10 +15373,10 @@ export class OrcaRuntimeService {
}, 10_000)
const handler = (
_event: Electron.IpcMainEvent,
event: Electron.IpcMainEvent,
r: { requestId: string; tabId?: string; title?: string; error?: string }
): void => {
if (r.requestId !== requestId) {
if (event.sender !== win.webContents || r.requestId !== requestId) {
return
}
clearTimeout(timer)
@@ -15535,10 +15535,10 @@ export class OrcaRuntimeService {
}, 10_000)
const handler = (
_event: Electron.IpcMainEvent,
event: Electron.IpcMainEvent,
r: { requestId: string; tabId?: string; title?: string; error?: string }
): void => {
if (r.requestId !== requestId) {
if (event.sender !== win.webContents || r.requestId !== requestId) {
return
}
clearTimeout(timer)
@@ -15560,6 +15560,7 @@ export class OrcaRuntimeService {
...(startupCommand.launchConfig ? { launchConfig: startupCommand.launchConfig } : {}),
...(startupCommand.launchAgent ? { launchAgent: startupCommand.launchAgent } : {}),
startupCommandDelivery: startupCommand.startupCommandDelivery,
source: 'runtime-session',
activate: opts.activate
})
})
@@ -603,4 +603,43 @@ describe('attachMainWindowServices', () => {
sendMock.mock.invocationCallOrder[0]
)
})
it('accepts terminal reveal replies only from the main window renderer', async () => {
const sendMock = vi.fn()
const mainWindow = createMainWindow({ send: sendMock })
const runtime = createRuntime()
attachMainWindowServices(mainWindow as never, createStore(), runtime as never)
const notifier = runtime.setNotifier.mock.calls[0][0] as {
revealTerminalSession: (
worktreeId: string,
opts: { ptyId: string; title?: string; activate?: boolean }
) => Promise<{ tabId: string; title?: string }>
}
const revealPromise = notifier.revealTerminalSession('wt-1', {
ptyId: 'pty-1',
title: 'SSH tmux'
})
const sentPayload = sendMock.mock.calls.find(
([channel]) => channel === 'ui:createTerminal'
)?.[1]
const handler = onMock.mock.calls.find(
([channel]) => channel === 'terminal:tabCreateReply'
)?.[1]
handler?.(
{ sender: { send: vi.fn() } },
{ requestId: sentPayload.requestId, error: 'spoofed renderer reply' }
)
expect(removeListenerMock).not.toHaveBeenCalledWith('terminal:tabCreateReply', handler)
handler?.(
{ sender: mainWindow.webContents },
{ requestId: sentPayload.requestId, tabId: 'tab-1', title: 'SSH tmux' }
)
await expect(revealPromise).resolves.toEqual({ tabId: 'tab-1', title: 'SSH tmux' })
expect(removeListenerMock).toHaveBeenCalledWith('terminal:tabCreateReply', handler)
})
})
@@ -264,10 +264,12 @@ function registerRuntimeWindowLifecycle(
reject(new Error('Terminal reveal timed out'))
}, 10_000)
const handler = (
_event: Electron.IpcMainEvent,
event: Electron.IpcMainEvent,
reply: { requestId: string; tabId?: string; title?: string; error?: string }
): void => {
if (reply.requestId !== requestId) {
// Why: requestId is renderer-supplied; only the targeted main window
// may satisfy the reveal and provide the tab handle.
if (event.sender !== mainWindow.webContents || reply.requestId !== requestId) {
return
}
clearTimeout(timer)
+2 -15
View File
@@ -268,6 +268,7 @@ import type {
RuntimeStatus,
RuntimeSyncWindowGraphResult,
RuntimeSyncWindowGraph,
RuntimeTerminalCreateRequestPayload,
RuntimeTerminalDriverState,
RuntimeTerminalPresentation
} from '../shared/runtime-types'
@@ -2591,21 +2592,7 @@ export type PreloadApi = {
}) => void
) => () => void
onRequestTerminalCreate: (
callback: (data: {
requestId: string
worktreeId?: string
afterTabId?: string
targetGroupId?: string
command?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
}) => void
callback: (data: RuntimeTerminalCreateRequestPayload) => void
) => () => void
replyTerminalCreate: (reply: {
requestId: string
+3 -30
View File
@@ -65,6 +65,7 @@ import type {
RuntimeStatus,
RuntimeSyncWindowGraphResult,
RuntimeSyncWindowGraph,
RuntimeTerminalCreateRequestPayload,
RuntimeTerminalDriverState,
RuntimeTerminalPresentation
} from '../shared/runtime-types'
@@ -3228,39 +3229,11 @@ const api = {
return () => ipcRenderer.removeListener('ui:createTerminal', listener)
},
onRequestTerminalCreate: (
callback: (data: {
requestId: string
worktreeId?: string
afterTabId?: string
targetGroupId?: string
command?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
}) => void
callback: (data: RuntimeTerminalCreateRequestPayload) => void
): (() => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
data: {
requestId: string
worktreeId?: string
afterTabId?: string
targetGroupId?: string
command?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
}
data: RuntimeTerminalCreateRequestPayload
) => callback(data)
ipcRenderer.on('terminal:requestTabCreate', listener)
return () => ipcRenderer.removeListener('terminal:requestTabCreate', listener)
@@ -1225,6 +1225,59 @@ describe('FloatingTerminalPanel close behavior', () => {
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab')
})
it('keeps floating browser create and duplicate local during active web runtime sessions', async () => {
setFloatingTabs([makeTab({ id: 'tab-1' })])
;(storeBox.state as FloatingPanelStoreState).settings.activeRuntimeEnvironmentId = 'runtime-1'
;(storeBox.state as FloatingPanelStoreState).browserTabsByWorktree = {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: 'browser-1',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
url: 'https://example.com',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
sessionProfileId: 'profile-1',
createdAt: 1
}
]
}
mocks.isWebRuntimeSessionActive.mockReturnValue(true)
mocks.createWebRuntimeSessionBrowserTab.mockResolvedValue(true)
const element = await renderPanel(true)
const tabBar = findByTypeName(element, 'TabBar')
;(tabBar.props.onNewBrowserTab as () => void)()
;(tabBar.props.onDuplicateBrowserTab as (browserTabId: string) => void)('browser-1')
expect(mocks.createWebRuntimeSessionBrowserTab).not.toHaveBeenCalled()
expect(mocks.createBrowserTab).toHaveBeenNthCalledWith(
1,
FLOATING_TERMINAL_WORKTREE_ID,
'about:blank',
{
title: 'New Browser Tab',
focusAddressBar: true,
targetGroupId: 'floating-group',
browserRuntimeEnvironmentId: null
}
)
expect(mocks.createBrowserTab).toHaveBeenNthCalledWith(
2,
FLOATING_TERMINAL_WORKTREE_ID,
'https://example.com',
{
title: 'Example',
sessionProfileId: 'profile-1',
targetGroupId: 'floating-group',
browserRuntimeEnvironmentId: null
}
)
})
it('hides the active terminal pane from the renderer while the panel is closed', async () => {
setFloatingTabs([makeTab({ id: 'tab-1' })])
@@ -2251,7 +2304,7 @@ describe('FloatingTerminalPanel close behavior', () => {
expect(mocks.closeUnifiedTab).not.toHaveBeenCalledWith(simulatorTab.id)
})
it('routes floating terminal create and close through active web runtime sessions', async () => {
it('keeps floating terminal create and close local during active web runtime sessions', async () => {
const onOpenChange = vi.fn()
setFloatingTabs([makeTab({ id: 'tab-1' })])
;(storeBox.state as FloatingPanelStoreState).settings.activeRuntimeEnvironmentId = 'runtime-1'
@@ -2263,22 +2316,19 @@ describe('FloatingTerminalPanel close behavior', () => {
;(tabBar.props.onNewTerminalTab as () => void)()
await flushAsyncWork()
expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
targetGroupId: 'floating-group',
command: undefined,
activate: true,
selectWorktree: false
})
expect(mocks.createTab).not.toHaveBeenCalled()
expect(mocks.createWebRuntimeSessionTerminal).not.toHaveBeenCalled()
expect(mocks.createTab).toHaveBeenCalledWith(
FLOATING_TERMINAL_WORKTREE_ID,
'floating-group',
undefined,
{ activate: false }
)
expect(mocks.activateTab).toHaveBeenCalledWith('created-tab')
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab')
;(tabBar.props.onClose as (tabId: string) => void)('tab-1')
expect(mocks.closeWebRuntimeSessionTab).toHaveBeenCalledWith({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: 'tab-1',
environmentId: 'runtime-1'
})
expect(mocks.closeTab).not.toHaveBeenCalled()
expect(mocks.closeWebRuntimeSessionTab).not.toHaveBeenCalled()
expect(mocks.closeTab).toHaveBeenCalledWith('tab-1')
expect(onOpenChange).not.toHaveBeenCalled()
})
@@ -47,13 +47,6 @@ import {
import { useAppStore } from '@/store'
import type { OpenFile } from '@/store/slices/editor'
import { destroyWorkspaceWebviews } from '@/store/slices/browser-webview-cleanup'
import {
activateWebRuntimeSessionTab,
closeWebRuntimeSessionTab,
createWebRuntimeSessionBrowserTab,
createWebRuntimeSessionTerminal,
isWebRuntimeSessionActive
} from '@/runtime/web-runtime-session'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import {
keybindingMatchesAction,
@@ -658,27 +651,10 @@ export function FloatingTerminalPanel({
return
}
activateTab(item.id)
const runtimeEnvironmentId = useAppStore
.getState()
.settings?.activeRuntimeEnvironmentId?.trim()
if (item.contentType === 'terminal') {
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
void activateWebRuntimeSessionTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: item.entityId,
environmentId: runtimeEnvironmentId
})
}
setActiveTab(item.entityId)
focusTerminalTabSurface(item.entityId)
} else if (item.contentType === 'browser') {
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
void activateWebRuntimeSessionTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: item.id,
environmentId: runtimeEnvironmentId
})
}
const workspace = useAppStore
.getState()
.browserTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.find(
@@ -694,50 +670,28 @@ export function FloatingTerminalPanel({
const createFloatingTerminalTab = useCallback(
(shellOverride?: string) => {
void (async () => {
if (
await createWebRuntimeSessionTerminal({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
targetGroupId: activeGroup?.id,
command: shellOverride,
activate: true,
selectWorktree: false
})
) {
return
}
const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride, {
activate: false
})
activateTab(tab.id)
focusTerminalTabSurface(tab.id)
})()
// Why: the floating workspace is a local scratchpad; a focused remote
// runtime must not own tabs users keep there for manual SSH/tmux work.
const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride, {
activate: false
})
activateTab(tab.id)
focusTerminalTabSurface(tab.id)
},
[activateTab, activeGroup, createTab]
)
const createFloatingBrowserTab = useCallback(() => {
void (async () => {
const url = browserDefaultUrl ?? 'about:blank'
if (
await createWebRuntimeSessionBrowserTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
url,
targetGroupId: activeGroup?.id,
selectWorktree: false
})
) {
return
}
createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, url, {
title: translate(
'auto.components.floating.terminal.FloatingTerminalPanel.8b14ba6c17',
'New Browser Tab'
),
focusAddressBar: true,
targetGroupId: activeGroup?.id
})
})()
const url = browserDefaultUrl ?? 'about:blank'
createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, url, {
title: translate(
'auto.components.floating.terminal.FloatingTerminalPanel.8b14ba6c17',
'New Browser Tab'
),
focusAddressBar: true,
targetGroupId: activeGroup?.id,
browserRuntimeEnvironmentId: null
})
}, [activeGroup, browserDefaultUrl, createBrowserTab])
const createFloatingMarkdownTab = useCallback(() => {
@@ -809,21 +763,7 @@ export function FloatingTerminalPanel({
return
}
const dirtyEditorFileIds: string[] = []
const runtimeEnvironmentId = state.settings?.activeRuntimeEnvironmentId?.trim()
for (const item of items) {
if (
(item.contentType === 'terminal' || item.contentType === 'browser') &&
isWebRuntimeSessionActive(runtimeEnvironmentId)
) {
// Why: paired web clients mirror host-owned tabs; ask the runtime to
// close the host tab instead of deleting the local mirror directly.
void closeWebRuntimeSessionTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: item.contentType === 'browser' ? item.id : item.entityId,
environmentId: runtimeEnvironmentId
})
continue
}
if (item.contentType === 'terminal') {
closeTab(item.entityId)
} else if (item.contentType === 'browser') {
@@ -1545,28 +1485,16 @@ export function FloatingTerminalPanel({
onActivateBrowserTab={activateFloatingItem}
onCloseBrowserTab={closeFloatingItem}
onDuplicateBrowserTab={(browserTabId) => {
void (async () => {
const source = browserTabs.find((tab) => tab.id === browserTabId)
if (!source) {
return
}
if (
await createWebRuntimeSessionBrowserTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
url: source.url,
profileId: source.sessionProfileId,
targetGroupId: activeGroup?.id,
selectWorktree: false
})
) {
return
}
createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, source.url, {
title: source.title,
sessionProfileId: source.sessionProfileId,
targetGroupId: activeGroup?.id
})
})()
const source = browserTabs.find((tab) => tab.id === browserTabId)
if (!source) {
return
}
createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, source.url, {
title: source.title,
sessionProfileId: source.sessionProfileId,
targetGroupId: activeGroup?.id,
browserRuntimeEnvironmentId: null
})
}}
onCloseAllFiles={closeAllFiles}
onMakePreviewFilePermanent={makePreviewFilePermanent}
+3 -3
View File
@@ -710,9 +710,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
)
const selectedRepo = eligibleRepos.find((repo) => repo.id === repoId)
const selectedRepoIsGit = selectedRepo ? isGitRepoKind(selectedRepo) : false
const [ephemeralVmRecipes, setEphemeralVmRecipes] = useState<NonNullable<OrcaHooks['environmentRecipes']>>(
[]
)
const [ephemeralVmRecipes, setEphemeralVmRecipes] = useState<
NonNullable<OrcaHooks['environmentRecipes']>
>([])
const [selectedEphemeralVmRecipeId, setSelectedEphemeralVmRecipeId] = useState<string | null>(
null
)
+39 -1
View File
@@ -1698,7 +1698,8 @@ describe('useIpcEvents updater integration', () => {
settings: {
terminalFontSize: 13,
experimentalNativeChat: false,
openAgentTabsInChatByDefault: false
openAgentTabsInChatByDefault: false,
activeRuntimeEnvironmentId: undefined as string | undefined
}
}
const createTerminalListenerRef: {
@@ -1739,6 +1740,7 @@ describe('useIpcEvents updater integration', () => {
title?: string
activate?: boolean
presentation?: 'background' | 'focused'
source?: 'runtime-session'
}) => void)
| null
} = { current: null }
@@ -2184,6 +2186,42 @@ describe('useIpcEvents updater integration', () => {
title: 'Codex'
})
createTab.mockClear()
replyTerminalCreate.mockClear()
storeState.settings.activeRuntimeEnvironmentId = 'focused-runtime'
requestTerminalCreateListenerRef.current({
requestId: 'req-runtime-session',
worktreeId: 'wt-2',
targetGroupId: 'group-left',
title: 'Runtime Terminal',
command: 'codex',
activate: true,
source: 'runtime-session'
})
expect(createTab).toHaveBeenCalledWith('wt-2', 'group-left', undefined, undefined)
expect(replyTerminalCreate).toHaveBeenCalledWith({
requestId: 'req-runtime-session',
tabId: 'tab-new',
title: 'Runtime Terminal'
})
createTab.mockClear()
replyTerminalCreate.mockClear()
storeState.settings.activeRuntimeEnvironmentId = 'focused-runtime'
requestTerminalCreateListenerRef.current({
requestId: 'req-runtime-blocked',
worktreeId: 'wt-2',
title: 'Blocked Local Terminal'
})
expect(createTab).not.toHaveBeenCalled()
expect(replyTerminalCreate).toHaveBeenCalledWith({
requestId: 'req-runtime-blocked',
error: 'Local terminal creation is unavailable while a remote runtime is active'
})
storeState.settings.activeRuntimeEnvironmentId = undefined
if (typeof focusTerminalListenerRef.current !== 'function') {
throw new Error('Expected focus-terminal listener to be registered')
}
+3 -1
View File
@@ -1556,7 +1556,9 @@ export function useIpcEvents(): void {
unsubs.push(
window.api.ui.onRequestTerminalCreate((data) => {
try {
if (isRuntimeEnvironmentActive()) {
// Why: runtime-session requests are host-owned tabs materialized by this
// renderer, not ordinary local creates that bypass remote runtime mode.
if (isRuntimeEnvironmentActive() && data.source !== 'runtime-session') {
window.api.ui.replyTerminalCreate({
requestId: data.requestId,
error: translate(
@@ -4,21 +4,17 @@ import { createUntitledMarkdownFileWithTemplateSelection } from './create-untitl
import { getConnectionId } from './connection-context'
import { detectLanguage } from './language-detect'
import type { AppState } from '@/store/types'
import {
createWebRuntimeSessionBrowserTab,
createWebRuntimeSessionTerminal
} from '@/runtime/web-runtime-session'
import { focusTerminalTabSurface } from './focus-terminal-tab-surface'
import { translate } from '@/i18n/i18n'
type FloatingWorkspaceTerminalStore = Pick<
AppState,
'activeGroupIdByWorktree' | 'createTab' | 'activateTab' | 'settings'
'activeGroupIdByWorktree' | 'createTab' | 'activateTab'
>
type FloatingWorkspaceBrowserStore = Pick<
AppState,
'activeGroupIdByWorktree' | 'browserDefaultUrl' | 'createBrowserTab' | 'settings'
'activeGroupIdByWorktree' | 'browserDefaultUrl' | 'createBrowserTab'
>
type FloatingWorkspaceMarkdownStore = Pick<AppState, 'activeGroupIdByWorktree' | 'openFile'>
@@ -28,20 +24,9 @@ export async function createFloatingWorkspaceTerminalTab(
shellOverride?: string
): Promise<TerminalTab | null> {
const targetGroupId = store.activeGroupIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]
const runtimeEnvironmentId = store.settings?.activeRuntimeEnvironmentId?.trim()
if (
await createWebRuntimeSessionTerminal({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
environmentId: runtimeEnvironmentId,
targetGroupId,
command: shellOverride,
activate: true,
selectWorktree: false
})
) {
return null
}
// Why: the floating workspace is a local scratchpad; a focused remote runtime
// must not own its SSH/tmux terminals or prune them via session snapshots.
const tab = store.createTab(FLOATING_TERMINAL_WORKTREE_ID, targetGroupId, shellOverride, {
activate: false
})
@@ -54,24 +39,15 @@ export async function createFloatingWorkspaceBrowserTab(
store: FloatingWorkspaceBrowserStore
): Promise<BrowserTab | null> {
const targetGroupId = store.activeGroupIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]
const runtimeEnvironmentId = store.settings?.activeRuntimeEnvironmentId?.trim()
const url = store.browserDefaultUrl ?? 'about:blank'
if (
await createWebRuntimeSessionBrowserTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
environmentId: runtimeEnvironmentId,
url,
targetGroupId,
selectWorktree: false
})
) {
return null
}
// Why: browser tabs in the floating workspace share the same local-only
// ownership rule as floating terminals.
return store.createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, url, {
title: translate('auto.lib.floating.workspace.tab.creation.f3785eddc2', 'New Browser Tab'),
focusAddressBar: true,
targetGroupId
targetGroupId,
browserRuntimeEnvironmentId: null
})
}
@@ -354,14 +354,7 @@ describe('createFloatingWorkspaceTerminalTab', () => {
await expect(createFloatingWorkspaceTerminalTab(store as never)).resolves.toBe(tab)
expect(createWebRuntimeSessionTerminalMock).toHaveBeenCalledWith({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
environmentId: undefined,
targetGroupId: 'floating-group',
command: undefined,
activate: true,
selectWorktree: false
})
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
expect(store.createTab).toHaveBeenCalledWith(
FLOATING_TERMINAL_WORKTREE_ID,
'floating-group',
@@ -372,28 +365,27 @@ describe('createFloatingWorkspaceTerminalTab', () => {
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-1')
})
it('leaves local tabs untouched when the web runtime accepts the floating terminal', async () => {
it('ignores the active runtime and keeps floating workspace terminals local', async () => {
const tab = makeTab('floating-tab-runtime')
const store = {
activeGroupIdByWorktree: {},
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
settings: { activeRuntimeEnvironmentId: 'env-1' },
createTab: vi.fn(),
createTab: vi.fn().mockReturnValue(tab),
activateTab: vi.fn()
}
createWebRuntimeSessionTerminalMock.mockResolvedValue(true)
await expect(createFloatingWorkspaceTerminalTab(store as never, 'pwsh')).resolves.toBeNull()
await expect(createFloatingWorkspaceTerminalTab(store as never, 'pwsh')).resolves.toBe(tab)
expect(createWebRuntimeSessionTerminalMock).toHaveBeenCalledWith(
expect.objectContaining({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
environmentId: 'env-1',
command: 'pwsh',
selectWorktree: false
})
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
expect(store.createTab).toHaveBeenCalledWith(
FLOATING_TERMINAL_WORKTREE_ID,
'floating-group',
'pwsh',
{ activate: false }
)
expect(store.createTab).not.toHaveBeenCalled()
expect(store.activateTab).not.toHaveBeenCalled()
expect(focusTerminalTabSurfaceMock).not.toHaveBeenCalled()
expect(store.activateTab).toHaveBeenCalledWith('floating-tab-runtime')
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-runtime')
})
})
@@ -414,20 +406,15 @@ describe('createFloatingWorkspaceBrowserTab', () => {
await expect(createFloatingWorkspaceBrowserTab(store as never)).resolves.toBe(browserTab)
expect(createWebRuntimeSessionBrowserTabMock).toHaveBeenCalledWith({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
environmentId: undefined,
url: 'about:blank',
targetGroupId: 'floating-group',
selectWorktree: false
})
expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled()
expect(store.createBrowserTab).toHaveBeenCalledWith(
FLOATING_TERMINAL_WORKTREE_ID,
'about:blank',
{
title: 'New Browser Tab',
focusAddressBar: true,
targetGroupId: 'floating-group'
targetGroupId: 'floating-group',
browserRuntimeEnvironmentId: null
}
)
})
@@ -514,7 +501,6 @@ describe('switchFloatingWorkspaceTab', () => {
},
openFiles: [],
setActiveTab: vi.fn(),
settings: { activeRuntimeEnvironmentId: null },
tabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [makeTab('tab-1'), makeTab('tab-2')]
},
@@ -533,6 +519,70 @@ describe('switchFloatingWorkspaceTab', () => {
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('tab-2')
expect(activateWebRuntimeSessionTabMock).not.toHaveBeenCalled()
})
it('cycles browser tabs locally while a web runtime is active', () => {
const notifyActiveTabChanged = vi.fn()
vi.stubGlobal('window', { api: { browser: { notifyActiveTabChanged } } })
isWebRuntimeSessionActiveMock.mockReturnValue(true)
const browserTab = {
id: 'browser-2',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
url: 'https://example.com',
title: 'Browser',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 0,
activePageId: 'page-2'
}
const store = {
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
activateTab: vi.fn(),
browserPagesByWorkspace: {},
browserTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [browserTab] },
groupsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: 'floating-group',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeTabId: 'tab-1',
tabOrder: ['tab-1', 'tab-browser-2'],
recentTabIds: ['tab-1']
}
]
},
openFiles: [],
setActiveTab: vi.fn(),
tabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [makeTab('tab-1')]
},
unifiedTabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
makeUnifiedTerminalTab('tab-1'),
{
id: 'tab-browser-2',
entityId: 'browser-2',
groupId: 'floating-group',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
contentType: 'browser',
label: 'Browser',
customLabel: null,
color: null,
sortOrder: 1,
createdAt: 1
} satisfies Tab
]
}
}
expect(switchFloatingWorkspaceTab(store as never, 1, 'all-types')).toBe(true)
expect(store.activateTab).toHaveBeenCalledWith('tab-browser-2')
expect(activateWebRuntimeSessionTabMock).not.toHaveBeenCalled()
expect(notifyActiveTabChanged).toHaveBeenCalledWith({ browserPageId: 'page-2' })
})
})
describe('shouldMinimizeFloatingWorkspacePanelOnCloseShortcut', () => {
@@ -9,10 +9,6 @@ import {
} from '@/components/terminal/tab-type-cycle'
import type { AppState } from '@/store/types'
import { TOGGLE_FLOATING_TERMINAL_EVENT } from './floating-terminal'
import {
activateWebRuntimeSessionTab,
isWebRuntimeSessionActive
} from '@/runtime/web-runtime-session'
import { focusTerminalTabSurface } from './focus-terminal-tab-surface'
import { keybindingMatchesAction, type KeybindingOverrides } from '../../../shared/keybindings'
export {
@@ -35,7 +31,6 @@ type FloatingWorkspaceTabSwitchStore = Pick<
| 'groupsByWorktree'
| 'openFiles'
| 'setActiveTab'
| 'settings'
| 'tabsByWorktree'
| 'unifiedTabsByWorktree'
>
@@ -136,28 +131,13 @@ function activateFloatingWorkspaceCyclableTab(
store.activateTab(next.tabId)
}
const runtimeEnvironmentId = store.settings?.activeRuntimeEnvironmentId?.trim()
if (next.type === 'terminal') {
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
void activateWebRuntimeSessionTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: next.id,
environmentId: runtimeEnvironmentId
})
}
store.setActiveTab(next.id)
focusTerminalTabSurface(next.id)
return
}
if (next.type === 'browser') {
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
void activateWebRuntimeSessionTab({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabId: next.tabId ?? next.id,
environmentId: runtimeEnvironmentId
})
}
const workspace = getFloatingWorkspaceBrowserTab(store, next.id)
if (workspace?.activePageId && typeof window !== 'undefined' && window.api?.browser) {
void window.api.browser.notifyActiveTabChanged({ browserPageId: workspace.activePageId })
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import {
getExplicitRuntimeEnvironmentIdForWorktree,
getExecutionHostIdForWorktree,
@@ -47,6 +48,13 @@ describe('getSettingsForWorktreeRuntimeOwner', () => {
})
})
it('keeps the synthetic floating workspace local while a runtime is focused', () => {
expect(getSettingsForWorktreeRuntimeOwner(state, FLOATING_TERMINAL_WORKTREE_ID)).toEqual({
activeRuntimeEnvironmentId: null
})
expect(getExecutionHostIdForWorktree(state, FLOATING_TERMINAL_WORKTREE_ID)).toBe('local')
})
it('routes folder workspaces to their project group runtime owner', () => {
expect(getSettingsForWorktreeRuntimeOwner(state, 'folder:runtime-folder')).toEqual({
activeRuntimeEnvironmentId: 'folder-env'
@@ -12,6 +12,7 @@ import type {
Worktree
} from '../../../shared/types'
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers'
export type WorktreeRuntimeOwnerState = {
@@ -130,6 +131,9 @@ export function getRuntimeEnvironmentIdForWorktree(
if (!worktreeId) {
return null
}
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
return null
}
const workspaceScope = parseWorkspaceKey(worktreeId)
if (workspaceScope?.type === 'folder') {
return getRuntimeEnvironmentIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId)
@@ -215,6 +219,9 @@ export function getExecutionHostIdForWorktree(
if (!worktreeId) {
return 'local'
}
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
return 'local'
}
const workspaceScope = parseWorkspaceKey(worktreeId)
if (workspaceScope?.type === 'folder') {
return getExecutionHostIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId)
@@ -5,6 +5,7 @@ import { posix as pathPosix } from 'node:path'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
import { makePaneKey } from '../../../shared/stable-pane-id'
import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import {
recordWebSessionFocusIntent,
resetWebSessionFocusIntentForTests
@@ -144,6 +145,65 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(shouldApplyWebSessionTabsSnapshot(sameEpochOlder, ENV)).toBe(false)
})
it('ignores remote snapshots for the local floating workspace', () => {
const floatingTab: TerminalTab = {
id: 'floating-tab-1',
ptyId: 'pty-floating-1',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
title: 'VPS tmux',
defaultTitle: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: NOW
}
const floatingUnifiedTab: Tab = {
id: floatingTab.id,
entityId: floatingTab.id,
groupId: 'floating-group',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
contentType: 'terminal',
label: floatingTab.title,
customLabel: null,
color: null,
sortOrder: 0,
createdAt: NOW,
isPreview: false
}
const state = makeState({
activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [floatingTab] },
ptyIdsByTabId: { [floatingTab.id]: ['pty-floating-1'] },
unifiedTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [floatingUnifiedTab] },
groupsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: 'floating-group',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeTabId: floatingTab.id,
tabOrder: [floatingTab.id],
recentTabIds: [floatingTab.id]
}
]
},
activeTabId: floatingTab.id,
activeTabIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: floatingTab.id }
})
const patch = applyWebSessionTabsSnapshot(
state,
makeSnapshot([], {
worktree: FLOATING_TERMINAL_WORKTREE_ID,
activeTabId: null,
activeTabType: null
}),
ENV,
NOW
)
expect(patch).toBe(state)
})
it('suppresses a tab the client is closing until the host confirms removal (no close flash)', () => {
const surface = {
type: 'terminal' as const,
@@ -5,6 +5,7 @@ import { useEffect } from 'react'
import type { AppState } from '../store'
import { useAppStore } from '../store'
import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import {
AGENT_STATUS_STALE_AFTER_MS,
type AgentStatusEntry
@@ -183,6 +184,13 @@ export function shouldApplyWebSessionTabsSnapshot(
clearWebSessionTabsTrackingForWorktree(environmentId, snapshot.worktree)
return true
}
if (snapshot.worktree === FLOATING_TERMINAL_WORKTREE_ID) {
// Why: the floating workspace is a local synthetic terminal. A focused
// remote runtime can publish an empty same-id snapshot while the user has a
// local ssh/tmux tab open; treating that as authoritative deletes the local
// floating tabs.
return false
}
rememberHostTerminalTabCount(environmentId, snapshot)
const current = latestSessionTabsSnapshotByWorktree.get(key)
// Why: snapshotVersion is monotonic only WITHIN one host generation; it resets
@@ -1589,6 +1597,9 @@ export function applyWebSessionTabsSnapshot(
now = Date.now()
): WebSessionTabsSyncState | Partial<WebSessionTabsSyncState> {
const worktreeId = rawSnapshot.worktree
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
return state
}
// Why: a remote close prunes the local mirror immediately, but an in-flight
// pre-close snapshot can still list the tab and flash it back. Drop any tab
// the client is closing until the host confirms removal; reconcile the intents
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import type { AppState } from '../types'
import { createTestStore, makeWorktree, seedStore, TEST_REPO } from './store-test-helpers'
@@ -97,6 +98,37 @@ describe('Cmd+J lifted creation actions', () => {
expect(store.getState().browserTabsByWorktree['wt-1'] ?? []).toHaveLength(1)
})
it('creates local browser and terminal tabs for the synthetic floating workspace while a runtime is focused', async () => {
createWebRuntimeSessionBrowserTabMock.mockResolvedValue(false)
createWebRuntimeSessionTerminalMock.mockResolvedValue(false)
const store = createTestStore()
seedStore(store, {
activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID,
settings: { activeRuntimeEnvironmentId: 'runtime-1' } as AppState['settings'],
groupsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: 'group-1',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeTabId: null,
tabOrder: []
}
]
},
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'group-1' }
})
await store.getState().openNewBrowserTabInActiveWorkspace('group-1')
await store.getState().openNewTerminalTabInActiveWorkspace('group-1')
expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled()
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
expect(
store.getState().browserTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
).toHaveLength(1)
expect(store.getState().tabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []).toHaveLength(1)
})
it('does not fall back to a local terminal tab when paired-web creation fails', async () => {
createWebRuntimeSessionTerminalMock.mockResolvedValue(false)
const store = createTestStore()
+26
View File
@@ -29,6 +29,8 @@ import type {
} from './mobile-markdown-document'
import type { RuntimeCapability } from './protocol-version'
import type { RemoteRuntimeSharedConnectionDiagnostics } from './remote-runtime-shared-control-types'
import type { SleepingAgentLaunchConfig } from './agent-session-resume'
import type { StartupCommandDelivery } from './codex-startup-delivery'
export type { RuntimeMarkdownReadTabResult, RuntimeMarkdownSaveTabResult }
@@ -455,6 +457,30 @@ export type RuntimeTerminalAgentStatus = {
}
export type RuntimeTerminalPresentation = 'background' | 'focused'
type RuntimeTerminalCreateBaseRequestPayload = {
requestId: string
worktreeId?: string
afterTabId?: string
targetGroupId?: string
command?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
}
export type RuntimeTerminalCreateRequestPayload =
| (RuntimeTerminalCreateBaseRequestPayload & { source?: undefined })
| (RuntimeTerminalCreateBaseRequestPayload & {
worktreeId: string
// Why: only the host-owned runtime-session bridge may bypass the renderer's
// active-runtime local terminal guard; ordinary UI requests must omit this.
source: 'runtime-session'
})
export type RuntimeTerminalCreate = {
handle: string