Skip to content

Commit 6839966

Browse files
committed
fix(copilot): restore active client panels
1 parent 18fa325 commit 6839966

3 files changed

Lines changed: 86 additions & 34 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
sendBrowserPanelAction,
1818
setBrowserTabPinned,
1919
} from '@/lib/browser-agent/transport'
20+
import { BROWSER_SESSION_RESOURCE_ID } from '@/lib/copilot/resources/types'
21+
import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
2022
import { useBrowserPanelOcclusion } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-panel-occlusion'
2123
import { BrowserTabStrip } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-strip'
2224
import { useBrowserSessionStore } from '@/stores/browser-session/store'
@@ -124,6 +126,25 @@ export function BrowserSession({ visible }: { visible: boolean }) {
124126
const hostRef = useRef<HTMLDivElement>(null)
125127
const urlInputRef = useRef<HTMLInputElement>(null)
126128
const panelOccluded = useBrowserPanelOcclusion(hostRef, visible)
129+
const { removeResource } = useMothershipResources()
130+
131+
// The browser session ending closes the panel, the way the terminal panel
132+
// goes when its last shell does. What it leaves otherwise is a tab whose
133+
// only content explains that there is nothing to show and that starting
134+
// again has to happen from somewhere else — the agent reopens the panel on
135+
// its next browser action anyway. Guarded on having seen a live session so
136+
// that opening the panel while the store still remembers a closed one does
137+
// not immediately close it again.
138+
const wasAlive = useRef(false)
139+
useEffect(() => {
140+
if (sessionAlive) {
141+
wasAlive.current = true
142+
return
143+
}
144+
if (!wasAlive.current) return
145+
wasAlive.current = false
146+
removeResource('browser', BROWSER_SESSION_RESOURCE_ID)
147+
}, [sessionAlive, removeResource])
127148
const pageUrlRef = useRef(pageState?.url ?? '')
128149
pageUrlRef.current = pageState?.url ?? ''
129150
/** Non-null while the user is editing the URL bar; otherwise it mirrors the page. */
@@ -389,13 +410,11 @@ export function BrowserSession({ visible }: { visible: boolean }) {
389410
className='pointer-events-none absolute inset-0 size-full object-fill'
390411
/>
391412
)}
392-
{(!pageState || !sessionAlive) && (
413+
{!pageState && (
393414
<div className='absolute inset-0 flex flex-col items-center justify-center gap-2'>
394415
<Cursor className='size-[18px] text-[var(--text-tertiary)]' />
395416
<p className='text-[var(--text-muted)] text-small'>
396-
{sessionAlive
397-
? 'Waiting for the browser session to start…'
398-
: 'The browser session was closed — ask Sim to navigate again to start a new one.'}
417+
Waiting for the browser session to start…
399418
</p>
400419
</div>
401420
)}

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.test.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import type { StreamBatchEvent } from '@/lib/copilot/request/session/types'
1111
import {
1212
getReplayCompletedWorkflowToolCallIds,
13-
hasExecutingBrowserToolCall,
13+
panelForExecutingClientTool,
1414
reconcileLiveAssistantTurn,
1515
selectReconnectReplayState,
1616
} from '@/app/workspace/[workspaceId]/home/hooks/use-chat'
@@ -220,7 +220,7 @@ describe('getReplayCompletedWorkflowToolCallIds', () => {
220220
})
221221
})
222222

223-
describe('hasExecutingBrowserToolCall', () => {
223+
describe('panelForExecutingClientTool', () => {
224224
function toolCallMessage(id: string, name: string, status: ToolCallStatus): ChatMessage {
225225
return {
226226
id,
@@ -236,16 +236,42 @@ describe('hasExecutingBrowserToolCall', () => {
236236
toolCallMessage('m2', 'browser_navigate', 'executing'),
237237
]
238238

239-
expect(hasExecutingBrowserToolCall(messages)).toBe(true)
239+
expect(panelForExecutingClientTool(messages)).toBe('browser')
240240
})
241241

242-
it('ignores completed browser tool calls and executing non-browser tools', () => {
242+
it('detects a terminal tool call that is still executing', () => {
243+
const messages = [
244+
toolCallMessage('m1', 'terminal', 'success'),
245+
toolCallMessage('m2', 'terminal', 'executing'),
246+
]
247+
248+
expect(panelForExecutingClientTool(messages)).toBe('terminal')
249+
})
250+
251+
it('ignores completed calls and executing tools that own no panel', () => {
243252
const messages = [
244253
toolCallMessage('m1', 'browser_click', 'success'),
245-
toolCallMessage('m2', 'run_workflow', 'executing'),
246-
{ id: 'm3', role: 'assistant' as const, content: 'no blocks' },
254+
toolCallMessage('m2', 'terminal', 'success'),
255+
toolCallMessage('m3', 'run_workflow', 'executing'),
256+
{ id: 'm4', role: 'assistant' as const, content: 'no blocks' },
257+
]
258+
259+
expect(panelForExecutingClientTool(messages)).toBe(null)
260+
})
261+
262+
// Both panels can be in flight at once; the later call is the one the user
263+
// was watching when they navigated away.
264+
it('picks the later panel when both are mid-action', () => {
265+
const browserFirst = [
266+
toolCallMessage('m1', 'browser_navigate', 'executing'),
267+
toolCallMessage('m2', 'terminal', 'executing'),
268+
]
269+
const terminalFirst = [
270+
toolCallMessage('m1', 'terminal', 'executing'),
271+
toolCallMessage('m2', 'browser_navigate', 'executing'),
247272
]
248273

249-
expect(hasExecutingBrowserToolCall(messages)).toBe(false)
274+
expect(panelForExecutingClientTool(browserFirst)).toBe('terminal')
275+
expect(panelForExecutingClientTool(terminalFirst)).toBe('browser')
250276
})
251277
})

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -956,21 +956,28 @@ export function getReplayCompletedWorkflowToolCallIds(events: StreamBatchEvent[]
956956
}
957957

958958
/**
959-
* True when the transcript holds a browser tool call that is still executing —
960-
* i.e. the live turn is mid browser-action. Used on reconnect to restore the
961-
* Browser resource panel the way workflow-run recovery restores workflows:
962-
* completed browser calls are suppressed on replay (exactly-once) and so never
963-
* re-surface the panel themselves.
959+
* Which live panel the transcript is mid-action on, or null for neither.
960+
*
961+
* Used on reconnect to restore that panel, the way workflow-run recovery
962+
* restores workflows. A completed browser or terminal call is suppressed on
963+
* replay, so it never re-opens its own panel — without this, returning to a
964+
* chat mid-turn lands on whichever resource happened to be persisted last
965+
* while the agent is driving a different one. When calls against both are in
966+
* flight the later one wins, being the one the user was watching.
964967
*/
965-
export function hasExecutingBrowserToolCall(messages: ChatMessage[]): boolean {
966-
return messages.some((message) =>
967-
(message.contentBlocks ?? []).some(
968-
(block) =>
969-
block.toolCall !== undefined &&
970-
isBrowserToolName(block.toolCall.name) &&
971-
block.toolCall.status === 'executing'
972-
)
973-
)
968+
export function panelForExecutingClientTool(
969+
messages: ChatMessage[]
970+
): 'browser' | 'terminal' | null {
971+
let panel: 'browser' | 'terminal' | null = null
972+
for (const message of messages) {
973+
for (const block of message.contentBlocks ?? []) {
974+
const call = block.toolCall
975+
if (call === undefined || call.status !== 'executing') continue
976+
if (isBrowserToolName(call.name)) panel = 'browser'
977+
else if (isTerminalToolName(call.name)) panel = 'terminal'
978+
}
979+
}
980+
return panel
974981
}
975982

976983
function buildRecoverySubjectKey(
@@ -1952,15 +1959,14 @@ export function useChat(
19521959
setActiveResourceId(null)
19531960
}
19541961

1955-
// Browser counterpart of the workflow-run recovery above: returning to a
1956-
// chat whose live turn is mid browser-action re-focuses the Browser tab
1957-
// and re-expands a collapsed panel. Runs after the resource hydration so
1958-
// it wins over the "last resource" active fallback. Completed browser
1959-
// calls are suppressed on replay (exactly-once) and never re-fire
1960-
// `startClientBrowserTool`, so without this the panel restores to
1961-
// whichever resource happened to be persisted last.
1962-
if (shouldReconnectActiveStream && hasExecutingBrowserToolCall(mappedMessages)) {
1963-
openBrowserResource()
1962+
// Live-panel counterpart of the workflow-run recovery above: returning to
1963+
// a chat whose turn is mid browser-action or mid-command re-focuses that
1964+
// tab and re-expands a collapsed panel. Runs after the resource hydration
1965+
// so it wins over the "last resource" active fallback.
1966+
if (shouldReconnectActiveStream) {
1967+
const panel = panelForExecutingClientTool(mappedMessages)
1968+
if (panel === 'browser') openBrowserResource()
1969+
else if (panel === 'terminal') openTerminalResource()
19641970
}
19651971

19661972
const snapshotPreviewSessions = Array.isArray(chatHistory.streamSnapshot?.previewSessions)
@@ -2029,6 +2035,7 @@ export function useChat(
20292035
cancelActiveStreamRecovery,
20302036
flushPendingResources,
20312037
openBrowserResource,
2038+
openTerminalResource,
20322039
recoverPendingClientWorkflowTools,
20332040
seedPreviewSessions,
20342041
setTransportIdle,

0 commit comments

Comments
 (0)