Prevent mobile worktree selection from focusing desktop (#6461)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
ba88e27cdc
commit
5fd9f0c62c
|
|
@ -710,9 +710,12 @@ export function HostScreen({
|
|||
// Highlight the row immediately; the next worktree.ps poll confirms it.
|
||||
setOptimisticActiveWorktreeId(item.worktreeId)
|
||||
if (client && connState === 'connected') {
|
||||
// Why: opening a mobile session should hydrate host-owned tabs without
|
||||
// pulling other paired clients, especially desktop, into this worktree.
|
||||
void client
|
||||
.sendRequest('worktree.activate', {
|
||||
worktree: `id:${item.worktreeId}`
|
||||
worktree: `id:${item.worktreeId}`,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2624,11 +2624,12 @@ export default function SessionScreen() {
|
|||
}
|
||||
void (async () => {
|
||||
if (client && created !== '1') {
|
||||
// Why: desktop reveal can be slow on cold/busy hosts, but mobile
|
||||
// session tabs are addressed by worktree id and can load immediately.
|
||||
// Why: mobile needs host-owned tabs hydrated for this route, but should
|
||||
// not pull other paired clients, especially desktop, into this worktree.
|
||||
void client
|
||||
.sendRequest('worktree.activate', {
|
||||
worktree: `id:${worktreeId}`
|
||||
worktree: `id:${worktreeId}`,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => null)
|
||||
}
|
||||
|
|
@ -2653,7 +2654,8 @@ export default function SessionScreen() {
|
|||
void (async () => {
|
||||
await client
|
||||
.sendRequest('worktree.activate', {
|
||||
worktree: `id:${worktreeId}`
|
||||
worktree: `id:${worktreeId}`,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => null)
|
||||
if (disposed) {
|
||||
|
|
@ -2802,7 +2804,8 @@ export default function SessionScreen() {
|
|||
void client
|
||||
.sendRequest('session.tabs.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
tabId: matchingTab.id
|
||||
tabId: matchingTab.id,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
|
@ -2834,7 +2837,8 @@ export default function SessionScreen() {
|
|||
void client
|
||||
.sendRequest('session.tabs.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
tabId: tab.id
|
||||
tabId: tab.id,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
|
@ -2857,7 +2861,8 @@ export default function SessionScreen() {
|
|||
void client
|
||||
.sendRequest('session.tabs.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
tabId: tab.id
|
||||
tabId: tab.id,
|
||||
notifyClients: false
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
|
@ -4137,7 +4142,8 @@ export default function SessionScreen() {
|
|||
.sendRequest('session.tabs.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
tabId: activePendingTerminalTab.id,
|
||||
leafId: activePendingTerminalTab.leafId
|
||||
leafId: activePendingTerminalTab.leafId,
|
||||
notifyClients: false
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ describe('mobile session startup', () => {
|
|||
)
|
||||
|
||||
expect(startupEffect).toContain("void client\n .sendRequest('worktree.activate'")
|
||||
expect(startupEffect).toContain('notifyClients: false')
|
||||
expect(startupEffect).not.toContain("await client\n .sendRequest('worktree.activate'")
|
||||
expect(startupEffect.indexOf("sendRequest('worktree.activate'")).toBeLessThan(
|
||||
startupEffect.indexOf('await fetchSessionTabs()')
|
||||
|
|
@ -58,12 +59,22 @@ describe('mobile session startup', () => {
|
|||
expect(pendingActivationEffect).toContain("sendRequest('session.tabs.activate'")
|
||||
expect(pendingActivationEffect).toContain('tabId: activePendingTerminalTab.id')
|
||||
expect(pendingActivationEffect).toContain('leafId: activePendingTerminalTab.leafId')
|
||||
expect(pendingActivationEffect).toContain('notifyClients: false')
|
||||
expect(pendingActivationEffect).toContain(
|
||||
'applySessionTabs((response as RpcSuccess).result as SessionTabsResult)'
|
||||
)
|
||||
expect(pendingActivationEffect).toContain('scheduleDelayedAction(() => void fetchSessionTabs()')
|
||||
})
|
||||
|
||||
it('keeps mobile session tab activation local to the phone', () => {
|
||||
const activationRequests = source.split("sendRequest('session.tabs.activate'").slice(1)
|
||||
|
||||
expect(activationRequests).toHaveLength(4)
|
||||
for (const request of activationRequests) {
|
||||
expect(request.slice(0, request.indexOf('})'))).toContain('notifyClients: false')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps dynamic agent rows above fixed New Tab actions', () => {
|
||||
const newTabActions = sliceBetween('title="New Tab"', 'onClose={() => setShowCreateTabDrawer')
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(new URL('../../app/h/[hostId]/index.tsx', import.meta.url), 'utf8')
|
||||
|
||||
function sliceBetween(startPattern: string, endPattern: string): string {
|
||||
const start = source.indexOf(startPattern)
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
const end = source.indexOf(endPattern, start)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
return source.slice(start, end)
|
||||
}
|
||||
|
||||
describe('mobile worktree activation', () => {
|
||||
it('opens mobile sessions without foregrounding other paired clients', () => {
|
||||
const openSession = sliceBetween(
|
||||
'const openWorktreeSession = useCallback(',
|
||||
'const handleSortChange = useCallback'
|
||||
)
|
||||
|
||||
expect(openSession).toContain("sendRequest('worktree.activate'")
|
||||
expect(openSession).toContain('notifyClients: false')
|
||||
})
|
||||
})
|
||||
|
|
@ -11775,6 +11775,77 @@ describe('OrcaRuntimeService', () => {
|
|||
expect(focusTerminal).toHaveBeenCalledWith('tab-1', TEST_WORKTREE_ID, 'pane:2')
|
||||
})
|
||||
|
||||
it('activates mobile session tabs without focusing desktop clients when requested', async () => {
|
||||
const focusTerminal = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setNotifier({
|
||||
worktreesChanged: vi.fn(),
|
||||
reposChanged: vi.fn(),
|
||||
activateWorktree: vi.fn(),
|
||||
createTerminal: vi.fn(),
|
||||
revealTerminalSession: vi.fn(),
|
||||
splitTerminal: vi.fn(),
|
||||
renameTerminal: vi.fn(),
|
||||
focusTerminal,
|
||||
closeTerminal: vi.fn(),
|
||||
sleepWorktree: vi.fn(),
|
||||
terminalFitOverrideChanged: vi.fn(),
|
||||
terminalDriverChanged: vi.fn()
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [],
|
||||
leaves: [],
|
||||
mobileSessionTabs: [
|
||||
{
|
||||
worktree: TEST_WORKTREE_ID,
|
||||
publicationEpoch: 'epoch-1',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: 'group-1',
|
||||
activeTabId: 'tab-1::pane:2',
|
||||
activeTabType: 'terminal',
|
||||
tabGroups: [{ id: 'group-1', activeTabId: 'tab-1', tabOrder: ['tab-1'] }],
|
||||
tabs: [
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'tab-1::pane:1',
|
||||
parentTabId: 'tab-1',
|
||||
leafId: 'pane:1',
|
||||
title: 'left',
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'tab-1::pane:2',
|
||||
parentTabId: 'tab-1',
|
||||
leafId: 'pane:2',
|
||||
title: 'right',
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const activated = await runtime.activateMobileSessionTab(
|
||||
`id:${TEST_WORKTREE_ID}`,
|
||||
'tab-1::pane:1',
|
||||
undefined,
|
||||
{ notifyClients: false }
|
||||
)
|
||||
|
||||
expect(focusTerminal).not.toHaveBeenCalled()
|
||||
expect(activated).toMatchObject({
|
||||
activeTabId: 'tab-1::pane:1',
|
||||
activeTabType: 'terminal',
|
||||
tabGroups: [expect.objectContaining({ id: 'group-1', activeTabId: 'tab-1' })]
|
||||
})
|
||||
expect(activated.tabs).toEqual([
|
||||
expect.objectContaining({ id: 'tab-1::pane:1', isActive: true }),
|
||||
expect.objectContaining({ id: 'tab-1::pane:2', isActive: false })
|
||||
])
|
||||
})
|
||||
|
||||
it('closes browser mobile session tabs when addressed by browser workspace id', async () => {
|
||||
const closeSessionTab = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
|
|
|
|||
|
|
@ -3658,7 +3658,8 @@ export class OrcaRuntimeService {
|
|||
async activateMobileSessionTab(
|
||||
worktreeSelector: string,
|
||||
tabId: string,
|
||||
leafId?: string
|
||||
leafId?: string,
|
||||
opts: { notifyClients?: boolean } = {}
|
||||
): Promise<RuntimeMobileSessionTabsResult> {
|
||||
const explicitWorktreeId = getExplicitWorktreeIdSelector(worktreeSelector)
|
||||
const worktreeId =
|
||||
|
|
@ -3733,6 +3734,10 @@ export class OrcaRuntimeService {
|
|||
candidate.isActive
|
||||
)
|
||||
const targetTab = activeSibling ?? tab
|
||||
if (opts.notifyClients === false) {
|
||||
this.activateMobileSessionTabForRemoteClient(worktreeId, snapshot!, targetTab)
|
||||
return this.getMobileSessionTabsForWorktree(worktreeId)
|
||||
}
|
||||
if (!this.notifier?.focusTerminal) {
|
||||
if (
|
||||
!targetTab.isActive &&
|
||||
|
|
@ -3744,15 +3749,57 @@ export class OrcaRuntimeService {
|
|||
}
|
||||
this.notifier?.focusTerminal(targetTab.parentTabId, worktreeId, targetTab.leafId)
|
||||
} else if (tab.type === 'browser') {
|
||||
if (opts.notifyClients === false) {
|
||||
this.activateMobileSessionTabForRemoteClient(worktreeId, snapshot!, tab)
|
||||
return this.getMobileSessionTabsForWorktree(worktreeId)
|
||||
}
|
||||
// Why: browser mobile tabs are renderer-owned unified tabs; focusing the
|
||||
// session tab keeps desktop tab order/group state authoritative.
|
||||
this.notifier?.focusEditorTab?.(tab.id, worktreeId)
|
||||
} else {
|
||||
if (opts.notifyClients === false) {
|
||||
this.activateMobileSessionTabForRemoteClient(worktreeId, snapshot!, tab)
|
||||
return this.getMobileSessionTabsForWorktree(worktreeId)
|
||||
}
|
||||
this.notifier?.focusEditorTab?.(tab.id, worktreeId)
|
||||
}
|
||||
return this.getMobileSessionTabsForWorktree(worktreeId)
|
||||
}
|
||||
|
||||
private activateMobileSessionTabForRemoteClient(
|
||||
worktreeId: string,
|
||||
snapshot: RuntimeMobileSessionTabsSnapshot,
|
||||
activeTab: RuntimeMobileSessionSnapshotTab
|
||||
): void {
|
||||
// Why: phone tab selection should update the mobile snapshot without
|
||||
// asking desktop renderers to focus the phone's background worktree.
|
||||
const activeTopLevelId = activeTab.type === 'terminal' ? activeTab.parentTabId : activeTab.id
|
||||
const tabs = snapshot.tabs.map((tab) => ({
|
||||
...tab,
|
||||
isActive: tab.id === activeTab.id
|
||||
}))
|
||||
const tabGroups = snapshot.tabGroups?.map((group) =>
|
||||
group.tabOrder.includes(activeTopLevelId)
|
||||
? { ...group, activeTabId: activeTopLevelId }
|
||||
: group
|
||||
)
|
||||
const activeGroupId =
|
||||
tabGroups?.find((group) => group.tabOrder.includes(activeTopLevelId))?.id ??
|
||||
snapshot.activeGroupId
|
||||
const nextSnapshot: RuntimeMobileSessionTabsSnapshot = {
|
||||
...snapshot,
|
||||
publicationEpoch: `mobile-local:${Date.now().toString(36)}`,
|
||||
snapshotVersion: snapshot.snapshotVersion + 1,
|
||||
activeGroupId,
|
||||
activeTabId: activeTab.id,
|
||||
activeTabType: activeTab.type,
|
||||
...(tabGroups ? { tabGroups } : {}),
|
||||
tabs
|
||||
}
|
||||
this.mobileSessionTabsByWorktree.set(worktreeId, nextSnapshot)
|
||||
this.emitMobileSessionTabsSnapshot(nextSnapshot)
|
||||
}
|
||||
|
||||
private shouldMaterializeHeadlessMobileSessionTab(
|
||||
snapshot: RuntimeMobileSessionTabsSnapshot,
|
||||
tab: RuntimeMobileSessionTerminalTab
|
||||
|
|
@ -11533,7 +11580,10 @@ export class OrcaRuntimeService {
|
|||
return { worktreeId: worktree.id }
|
||||
}
|
||||
|
||||
async activateManagedWorktree(worktreeSelector: string): Promise<{
|
||||
async activateManagedWorktree(
|
||||
worktreeSelector: string,
|
||||
opts: { notifyClients?: boolean } = {}
|
||||
): Promise<{
|
||||
repoId: string
|
||||
worktreeId: string
|
||||
activated: boolean
|
||||
|
|
@ -11545,9 +11595,19 @@ export class OrcaRuntimeService {
|
|||
throw new Error('repo_not_found')
|
||||
}
|
||||
|
||||
// Why: inactive worktree terminal panes are renderer-owned and may not have
|
||||
// live PTYs until the desktop activates the worktree and mounts them.
|
||||
this.notifyActivateWorktree(repo.id, worktree.id)
|
||||
if (opts.notifyClients !== false) {
|
||||
// Why: inactive worktree terminal panes are renderer-owned and may not have
|
||||
// live PTYs until the desktop activates the worktree and mounts them.
|
||||
this.notifyActivateWorktree(repo.id, worktree.id)
|
||||
} else {
|
||||
// Why: mobile/web selection needs fresh session surfaces without forcing
|
||||
// every attached desktop renderer to navigate to the phone's workspace.
|
||||
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktree.id, {
|
||||
allowAttachedWindow: true
|
||||
})
|
||||
await this.refreshMobileSessionPtyRecords()
|
||||
this.notifyMobileSessionTabsChanged(worktree.id)
|
||||
}
|
||||
return { repoId: repo.id, worktreeId: worktree.id, activated: true }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from 'zod'
|
|||
import { isTuiAgent } from '../../../../shared/tui-agent-config'
|
||||
import type { TuiAgent } from '../../../../shared/types'
|
||||
import { sleepingAgentLaunchConfigSchema } from '../../../../shared/workspace-session-sleeping-agents'
|
||||
import { OptionalBoolean } from '../schemas'
|
||||
|
||||
export const WorktreeTabSelector = z.object({
|
||||
worktree: z
|
||||
|
|
@ -19,7 +20,8 @@ export const ActivateTab = WorktreeTabSelector.extend({
|
|||
.unknown()
|
||||
.transform((v) => (typeof v === 'string' ? v : ''))
|
||||
.pipe(z.string().min(1, 'Missing tab id')),
|
||||
leafId: z.string().max(128).optional()
|
||||
leafId: z.string().max(128).optional(),
|
||||
notifyClients: OptionalBoolean
|
||||
})
|
||||
|
||||
export type TerminalPaneLayoutNodeInput =
|
||||
|
|
|
|||
|
|
@ -9,6 +9,36 @@ function makeRequest(method: string, params?: unknown): RpcRequest {
|
|||
}
|
||||
|
||||
describe('session tab RPC methods', () => {
|
||||
it('routes mobile-only activation without notifying desktop clients', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
activateMobileSessionTab: vi.fn().mockResolvedValue({
|
||||
worktree: 'wt-1',
|
||||
publicationEpoch: 'epoch-1',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: null,
|
||||
activeTabId: 'tab-1',
|
||||
activeTabType: 'terminal',
|
||||
tabs: []
|
||||
})
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS })
|
||||
|
||||
const response = await dispatcher.dispatch(
|
||||
makeRequest('session.tabs.activate', {
|
||||
worktree: 'id:wt-1',
|
||||
tabId: 'tab-1',
|
||||
leafId: 'leaf-1',
|
||||
notifyClients: false
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.ok).toBe(true)
|
||||
expect(runtime.activateMobileSessionTab).toHaveBeenCalledWith('id:wt-1', 'tab-1', 'leaf-1', {
|
||||
notifyClients: false
|
||||
})
|
||||
})
|
||||
|
||||
it('dispatches tab moves through the runtime', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ export const SESSION_TAB_METHODS: RpcAnyMethod[] = [
|
|||
name: 'session.tabs.activate',
|
||||
params: ActivateTab,
|
||||
handler: async (params, { runtime }) =>
|
||||
runtime.activateMobileSessionTab(params.worktree, params.tabId, params.leafId)
|
||||
runtime.activateMobileSessionTab(params.worktree, params.tabId, params.leafId, {
|
||||
notifyClients: params.notifyClients !== false
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'session.tabs.close',
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ export const WorktreeSelector = z.object({
|
|||
.pipe(z.string().min(1, 'Missing worktree selector'))
|
||||
})
|
||||
|
||||
export const WorktreeActivate = WorktreeSelector.extend({
|
||||
notifyClients: OptionalBoolean
|
||||
})
|
||||
|
||||
export const WorktreeCreate = z
|
||||
.object({
|
||||
repo: z
|
||||
|
|
|
|||
|
|
@ -20,6 +20,28 @@ function makeRequest(method: string, params?: unknown): RpcRequest {
|
|||
}
|
||||
|
||||
describe('worktree RPC methods', () => {
|
||||
it('routes mobile session-only activation without notifying desktop clients', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
activateManagedWorktree: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ repoId: 'repo-1', worktreeId: 'wt-1', activated: true })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
|
||||
const response = await dispatcher.dispatch(
|
||||
makeRequest('worktree.activate', {
|
||||
worktree: 'id:wt-1',
|
||||
notifyClients: false
|
||||
})
|
||||
)
|
||||
|
||||
expect(response).toMatchObject({ ok: true })
|
||||
expect(runtime.activateManagedWorktree).toHaveBeenCalledWith('id:wt-1', {
|
||||
notifyClients: false
|
||||
})
|
||||
})
|
||||
|
||||
it('routes create options to the runtime server', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { defineMethod, type RpcMethod } from '../core'
|
|||
import {
|
||||
WorktreeCreate,
|
||||
WorktreeDetectedListParams,
|
||||
WorktreeActivate,
|
||||
WorktreeForceDeleteBranch,
|
||||
WorktreeListParams,
|
||||
WorktreePrefetchCreateBase,
|
||||
|
|
@ -57,8 +58,11 @@ export const WORKTREE_METHODS: RpcMethod[] = [
|
|||
}),
|
||||
defineMethod({
|
||||
name: 'worktree.activate',
|
||||
params: WorktreeSelector,
|
||||
handler: async (params, { runtime }) => runtime.activateManagedWorktree(params.worktree)
|
||||
params: WorktreeActivate,
|
||||
handler: async (params, { runtime }) =>
|
||||
runtime.activateManagedWorktree(params.worktree, {
|
||||
notifyClients: params.notifyClients !== false
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'worktree.create',
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ describe('activateAndRevealWorktree created agent reopen', () => {
|
|||
expect(callRuntimeEnvironment).toHaveBeenCalledWith({
|
||||
selector: 'web-runtime-1',
|
||||
method: 'worktree.activate',
|
||||
params: { worktree: `id:${worktree.id}` },
|
||||
params: { worktree: `id:${worktree.id}`, notifyClients: false },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -306,11 +306,12 @@ export function activateAndRevealWorktree(
|
|||
const ownerRuntimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(postActivationState, wt.id)
|
||||
if (opts?.notifyHostRuntime !== false && isWebRuntimeSessionActive(ownerRuntimeEnvironmentId)) {
|
||||
// Why: paired web clients own only local selection state. The desktop host
|
||||
// must also activate the worktree so hidden renderer-owned terminal panes
|
||||
// mount and publish session surfaces back to the web client.
|
||||
// should publish session surfaces for the phone without treating that as a
|
||||
// desktop navigation command.
|
||||
void activateWebRuntimeSessionWorktree({
|
||||
worktreeId,
|
||||
environmentId: ownerRuntimeEnvironmentId
|
||||
environmentId: ownerRuntimeEnvironmentId,
|
||||
notifyDesktop: (globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ !== true
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
|
||||
import {
|
||||
activateWebRuntimeSessionWorktree,
|
||||
activateWebRuntimeSessionTab,
|
||||
closeWebRuntimeTerminal,
|
||||
closeWebRuntimeSessionTab,
|
||||
|
|
@ -62,6 +63,55 @@ function makeSnapshot(): RuntimeMobileSessionTabsResult {
|
|||
}
|
||||
}
|
||||
|
||||
describe('activateWebRuntimeSessionWorktree', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
|
||||
mocks.getState.mockReturnValue({
|
||||
settings: {
|
||||
activeRuntimeEnvironmentId: ENVIRONMENT_ID
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('can ask the host to activate session surfaces without notifying desktop clients', async () => {
|
||||
const runtimeCall = vi.fn().mockResolvedValueOnce({
|
||||
id: 'activate',
|
||||
ok: true,
|
||||
result: { repoId: 'repo', worktreeId: WORKTREE_ID, activated: true }
|
||||
})
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
runtimeEnvironments: {
|
||||
call: runtimeCall
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
activateWebRuntimeSessionWorktree({
|
||||
worktreeId: WORKTREE_ID,
|
||||
notifyDesktop: false
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: ENVIRONMENT_ID,
|
||||
method: 'worktree.activate',
|
||||
params: {
|
||||
worktree: `id:${WORKTREE_ID}`,
|
||||
notifyClients: false
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createWebRuntimeSessionBrowserTab', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@ async function refreshWebRuntimeSessionTabsSnapshot(
|
|||
export async function activateWebRuntimeSessionWorktree(args: {
|
||||
worktreeId: string
|
||||
environmentId?: string | null
|
||||
notifyDesktop?: boolean
|
||||
}): Promise<boolean> {
|
||||
const environmentId =
|
||||
args.environmentId?.trim() ??
|
||||
|
|
@ -296,7 +297,8 @@ export async function activateWebRuntimeSessionWorktree(args: {
|
|||
selector: environmentId,
|
||||
method: 'worktree.activate',
|
||||
params: {
|
||||
worktree: toRuntimeWorktreeSelector(args.worktreeId)
|
||||
worktree: toRuntimeWorktreeSelector(args.worktreeId),
|
||||
notifyClients: args.notifyDesktop !== false
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue