Skip to content

Commit 1f3977b

Browse files
committed
Always focus the resource the agent is working on, and its browser tab
The resource panel had a carve-out: an already-open browser session declined to replace another selection and only got an attention marker, so agent browser work happened off-screen. The panel now follows the agent to whatever it touches — browser included — and an event can still opt out explicitly. The browser panel also follows the agent BETWEEN tabs: the store already tracked automationTabId (and the strip marked it), but the visible tab never changed. It now switches when the agent's target tab changes, so watching the agent never means hunting for the tab it moved to. Keyed on the target changing rather than on it being set, so a user who browses elsewhere mid-run is only pulled along when the agent itself moves.
1 parent c4f4ec9 commit 1f3977b

3 files changed

Lines changed: 49 additions & 45 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,24 @@ export function BrowserSession({
608608

609609
useEffect(() => onBrowserOmniboxFocus(focusOmnibox, scopeId), [focusOmnibox, scopeId])
610610

611+
// Follow the agent's tab. The panel already marks the automated tab in the
612+
// strip; this makes it the VISIBLE one, so watching the agent never means
613+
// hunting for which tab it moved to. Keyed on the automation target
614+
// CHANGING, not on it merely being set — the user can still browse a
615+
// different tab mid-run and is only pulled along when the agent itself
616+
// moves to another tab.
617+
const followedAutomationTabRef = useRef<string | null>(null)
618+
useEffect(() => {
619+
if (!automationActive || !automationTabId) {
620+
if (!automationActive) followedAutomationTabRef.current = null
621+
return
622+
}
623+
if (followedAutomationTabRef.current === automationTabId) return
624+
followedAutomationTabRef.current = automationTabId
625+
if (automationTabId === activeTabId) return
626+
sendBrowserPanelAction('switch-tab', { tabId: automationTabId }, scopeId)
627+
}, [activeTabId, automationActive, automationTabId, scopeId])
628+
611629
// Sim owns keyboard events while its renderer has focus. Claim Cmd+L here
612630
// before the workspace's global "Go to Logs" command can navigate away.
613631
useEffect(() => {

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,24 @@ describe('selectDeletedWorkflowResources', () => {
6161
})
6262

6363
describe('shouldActivateResourceEvent', () => {
64-
it('keeps background browser activity from replacing another selected resource', () => {
65-
expect(shouldActivateResourceEvent('file-1', 'browser-session')).toBe(false)
64+
it('surfaces browser work even when another resource is selected', () => {
65+
expect(shouldActivateResourceEvent('file-1', 'browser-session')).toBe(true)
6666
})
6767

68-
it('allows an explicit user action to surface the browser over another selection', () => {
69-
expect(
70-
shouldActivateResourceEvent('file-1', 'browser-session', {
71-
activate: true,
72-
})
73-
).toBe(true)
68+
it('surfaces every other resource the agent touches', () => {
69+
expect(shouldActivateResourceEvent('file-1', 'workflow-1')).toBe(true)
70+
expect(shouldActivateResourceEvent('browser-session', 'terminal-session')).toBe(true)
71+
expect(shouldActivateResourceEvent(null, 'browser-session')).toBe(true)
7472
})
7573

76-
it('activates the browser when nothing else is selected', () => {
77-
expect(shouldActivateResourceEvent(null, 'browser-session')).toBe(true)
78-
expect(shouldActivateResourceEvent('browser-session', 'browser-session')).toBe(true)
74+
it('honors an explicit request to activate', () => {
75+
expect(shouldActivateResourceEvent('file-1', 'browser-session', { activate: true })).toBe(true)
7976
})
8077

81-
it('activates a non-browser resource even when another resource is selected', () => {
82-
expect(shouldActivateResourceEvent('file-1', 'workflow-1')).toBe(true)
83-
expect(shouldActivateResourceEvent('browser-session', 'terminal-session')).toBe(true)
78+
it('lets an event opt out of stealing focus', () => {
79+
expect(shouldActivateResourceEvent('file-1', 'browser-session', { activate: false })).toBe(
80+
false
81+
)
8482
})
8583
})
8684

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

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,22 +1215,18 @@ export interface ResourceEventOptions {
12151215
export type ResourceEventHandler = (resourceId: string, options?: ResourceEventOptions) => void
12161216

12171217
/**
1218-
* Whether a streamed resource event should activate its tab. Resources switch
1219-
* into view as the agent creates or edits them; only an already-open browser
1220-
* session declines to replace an existing selection (it gets an attention
1221-
* marker instead), unless the event explicitly requests activation — which
1222-
* `openBrowserResource` does when it newly opens the browser tab, so agent
1223-
* browser work surfaces on first open and stays put once the user has
1224-
* deliberately switched away.
1218+
* Whether a streamed resource event should activate its tab. The panel always
1219+
* follows the agent: whatever it is creating, editing, or driving becomes the
1220+
* visible resource, browser sessions included. The parameters are retained so
1221+
* callers stay explicit about the resource in play, and so a future opt-out
1222+
* (an event that deliberately declines focus) has a place to live.
12251223
*/
12261224
export function shouldActivateResourceEvent(
1227-
activeResourceId: string | null,
1228-
resourceId: string,
1225+
_activeResourceId: string | null,
1226+
_resourceId: string,
12291227
options?: ResourceEventOptions
12301228
): boolean {
1231-
if (options?.activate === true) return true
1232-
if (resourceId !== BROWSER_SESSION_RESOURCE_ID) return true
1233-
return !activeResourceId || activeResourceId === resourceId
1229+
return options?.activate !== false
12341230
}
12351231

12361232
/**
@@ -2022,24 +2018,16 @@ export function useChat(
20222018
[workspaceId]
20232019
)
20242020

2025-
const openBrowserResource = useCallback(
2026-
(activate = false) => {
2027-
// A newly opened browser tab surfaces like any other agent-created
2028-
// resource. Only an ALREADY-open browser tab stays in the background
2029-
// behind another selection — the user saw it and switched away, so
2030-
// ongoing agent activity earns an attention marker, not a tab switch.
2031-
const newlyOpened = addResource({
2032-
type: 'browser',
2033-
id: BROWSER_SESSION_RESOURCE_ID,
2034-
title: 'Browser',
2035-
})
2036-
onResourceEventRef.current?.(
2037-
BROWSER_SESSION_RESOURCE_ID,
2038-
activate || newlyOpened ? { activate: true } : undefined
2039-
)
2040-
},
2041-
[addResource]
2042-
)
2021+
const openBrowserResource = useCallback(() => {
2022+
// Browser work surfaces like any other agent activity: the panel follows
2023+
// the agent to the browser whether or not the session was already open.
2024+
addResource({
2025+
type: 'browser',
2026+
id: BROWSER_SESSION_RESOURCE_ID,
2027+
title: 'Browser',
2028+
})
2029+
onResourceEventRef.current?.(BROWSER_SESSION_RESOURCE_ID, { activate: true })
2030+
}, [addResource])
20432031

20442032
const getResourceActivityTracker = useCallback(
20452033
(generation: number, targetChatId?: string) => {
@@ -2155,7 +2143,7 @@ export function useChat(
21552143
// (message components dispatch the request; this hook owns the resource).
21562144
useEffect(() => {
21572145
return onOpenInBrowserPanel((url) => {
2158-
openBrowserResource(true)
2146+
openBrowserResource()
21592147
void openUrlInNewBrowserTab(url, desktopScopeIdRef.current).catch((error) => {
21602148
logger.warn('Failed to open chat link in a new browser tab', {
21612149
error: getErrorMessage(error),

0 commit comments

Comments
 (0)