Skip to content

Commit dff1309

Browse files
committed
Merge remote-tracking branch 'origin/feat/plan-discovery-hillclimb' into feat/plan-discovery-hillclimb
2 parents 6e8138e + 657e1d4 commit dff1309

4 files changed

Lines changed: 107 additions & 81 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/settings/components/desktop/computer-use.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ function ComputerUseSettingsControls() {
9090
<span className='text-[var(--text-muted)] text-sm'>Allowed</span>
9191
) : (
9292
<Chip
93+
aria-label={`Open System Settings for ${permission === 'accessibility' ? 'Accessibility' : 'Screen Recording'}`}
9394
disabled={pending || !status?.enabled}
9495
onClick={() =>
9596
void update(async () => setStatus(await bridge.requestPermission(permission)))

‎apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts‎

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,38 +1062,47 @@ describe('executeBrowserToolOnClient', () => {
10621062
await flush()
10631063
})
10641064

1065-
it('compacts a large known result before unload-safe delivery', async () => {
1066-
mockExecuteBrowserTool.mockResolvedValue({
1067-
dataUrl: `data:image/jpeg;base64,${'A'.repeat(64 * 1024)}`,
1068-
url: 'https://example.com',
1069-
})
1070-
let finishReport: () => void = () => {}
1071-
mockReportCompletion.mockImplementation(
1072-
() =>
1073-
new Promise<void>((resolve) => {
1074-
finishReport = resolve
1075-
})
1076-
)
1077-
const sendBeacon = vi.mocked(navigator.sendBeacon)
1078-
const toolCallId = nextToolCallId()
1065+
it.each([false, true])(
1066+
'compacts a large result without overstating outcome certainty (unknown=%s)',
1067+
async (outcomeUnknown) => {
1068+
mockExecuteBrowserTool.mockResolvedValue({
1069+
dataUrl: `data:image/jpeg;base64,${'A'.repeat(64 * 1024)}`,
1070+
url: 'https://example.com',
1071+
outcomeUnknown,
1072+
doNotRetry: outcomeUnknown,
1073+
})
1074+
let finishReport: () => void = () => {}
1075+
mockReportCompletion.mockImplementation(
1076+
() =>
1077+
new Promise<void>((resolve) => {
1078+
finishReport = resolve
1079+
})
1080+
)
1081+
const sendBeacon = vi.mocked(navigator.sendBeacon)
1082+
const toolCallId = nextToolCallId()
10791083

1080-
executeBrowserToolOnClient(toolCallId, 'browser_screenshot', {})
1081-
await flush()
1082-
window.dispatchEvent(new Event('pagehide'))
1083-
await flush()
1084+
executeBrowserToolOnClient(toolCallId, 'browser_screenshot', {})
1085+
await flush()
1086+
window.dispatchEvent(new Event('pagehide'))
1087+
await flush()
10841088

1085-
const beaconPayload = sendBeacon.mock.calls[0]?.[1] as NodeBlob
1086-
const payload = JSON.parse(await beaconPayload.text())
1087-
expect(beaconPayload.size).toBeLessThanOrEqual(48 * 1024)
1088-
expect(payload).toMatchObject({
1089-
toolCallId,
1090-
status: 'success',
1091-
data: { resultOmittedDuringPageExit: true },
1092-
})
1093-
expect(payload.data.attachment).toBeUndefined()
1094-
finishReport()
1095-
await flush()
1096-
})
1089+
const beaconPayload = sendBeacon.mock.calls[0]?.[1] as NodeBlob
1090+
const payload = JSON.parse(await beaconPayload.text())
1091+
expect(beaconPayload.size).toBeLessThanOrEqual(48 * 1024)
1092+
expect(payload).toMatchObject({
1093+
toolCallId,
1094+
status: outcomeUnknown ? 'error' : 'success',
1095+
data: {
1096+
resultOmittedDuringPageExit: true,
1097+
...(outcomeUnknown ? { outcomeUnknown: true, doNotRetry: true } : {}),
1098+
},
1099+
})
1100+
expect(payload.data.attachment).toBeUndefined()
1101+
if (outcomeUnknown) expect(payload.data.note).not.toContain('known terminal state')
1102+
finishReport()
1103+
await flush()
1104+
}
1105+
)
10971106

10981107
it.each([['not accepted', () => false]])(
10991108
'uses keepalive fallback for known success when the page-exit beacon %s',

‎apps/sim/lib/mothership/tools/client/computer-tool-unload.test.ts‎

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -164,64 +164,80 @@ describe('native computer completion during page unload', () => {
164164
}
165165
})
166166

167-
it('retains confirmed zero-dispatch failure after all delivery paths fail and releases it after replay acknowledgment', async () => {
168-
native.execute.mockRejectedValueOnce(
169-
new ComputerUseError({
170-
code: 'activation_required',
171-
message: 'Activate first.',
172-
dispatchState: 'not_started',
167+
it.each([false, true])(
168+
'retains native input uncertainty through delivery failure and replay (unknown=%s)',
169+
async (outcomeUnknown) => {
170+
native.execute.mockRejectedValueOnce(
171+
new ComputerUseError({
172+
code: outcomeUnknown ? 'focus_changed' : 'activation_required',
173+
message: outcomeUnknown
174+
? 'The exact editor is no longer focused; remaining input was not dispatched. Observe again.'
175+
: 'Activate first.',
176+
...(outcomeUnknown ? {} : { dispatchState: 'not_started' as const }),
177+
})
178+
)
179+
const fetch = vi
180+
.fn<typeof globalThis.fetch>()
181+
.mockResolvedValue(new Response(null, { status: 503 }))
182+
vi.stubGlobal('fetch', fetch)
183+
const bodies: NodeBlob[] = []
184+
Object.defineProperty(navigator, 'sendBeacon', {
185+
configurable: true,
186+
value: (_url: string, body: NodeBlob) => {
187+
bodies.push(body)
188+
return false
189+
},
173190
})
174-
)
175-
const fetch = vi
176-
.fn<typeof globalThis.fetch>()
177-
.mockResolvedValue(new Response(null, { status: 503 }))
178-
vi.stubGlobal('fetch', fetch)
179-
const bodies: NodeBlob[] = []
180-
Object.defineProperty(navigator, 'sendBeacon', {
181-
configurable: true,
182-
value: (_url: string, body: NodeBlob) => {
183-
bodies.push(body)
184-
return false
185-
},
186-
})
187-
const id = nextId()
188-
const timestamp = new Date().toISOString()
189-
const execution = executeComputerToolOnClient(id, { action: 'list_apps' }, timestamp)
190-
await vi.runAllTimersAsync()
191-
await execution
192-
try {
191+
const id = nextId()
192+
const timestamp = new Date().toISOString()
193+
const input = {
194+
action: 'type_text' as const,
195+
bundleId: 'com.example.Fixture',
196+
snapshotId: 'before',
197+
elementId: 'editor',
198+
text: 'prefix then interruption',
199+
}
200+
const execution = executeComputerToolOnClient(id, input, timestamp)
201+
await vi.runAllTimersAsync()
202+
await execution
203+
try {
204+
window.dispatchEvent(new Event('pagehide'))
205+
await flushMicrotasks()
206+
expect(bodies).toHaveLength(1)
207+
expect(copilotConfirmBodySchema.parse(JSON.parse(await bodies[0].text()))).toMatchObject({
208+
toolCallId: id,
209+
status: 'error',
210+
data: {
211+
code: outcomeUnknown ? 'focus_changed' : 'activation_required',
212+
...(outcomeUnknown ? {} : { dispatchState: 'not_started' }),
213+
doNotRetry: outcomeUnknown,
214+
outcomeUnknown,
215+
},
216+
})
217+
const fallback = fetch.mock.calls.find(([, init]) => init?.keepalive)
218+
expect(fallback?.[1]?.body).toBe(await bodies[0].text())
219+
expect(native.cancel).not.toHaveBeenCalled()
220+
} finally {
221+
fetch.mockResolvedValue(new Response(null, { status: 200 }))
222+
await executeComputerToolOnClient(id, input, timestamp)
223+
}
224+
const count = bodies.length
193225
window.dispatchEvent(new Event('pagehide'))
194-
await flushMicrotasks()
195-
expect(bodies).toHaveLength(1)
196-
expect(copilotConfirmBodySchema.parse(JSON.parse(await bodies[0].text()))).toMatchObject({
226+
expect(bodies).toHaveLength(count)
227+
expect(
228+
copilotConfirmBodySchema.parse(JSON.parse(String(fetch.mock.lastCall?.[1]?.body)))
229+
).toMatchObject({
197230
toolCallId: id,
198231
status: 'error',
199232
data: {
200-
code: 'activation_required',
201-
dispatchState: 'not_started',
202-
doNotRetry: false,
203-
outcomeUnknown: false,
233+
...(outcomeUnknown ? {} : { dispatchState: 'not_started' }),
234+
outcomeUnknown,
235+
doNotRetry: outcomeUnknown,
204236
},
205237
})
206-
const fallback = fetch.mock.calls.find(([, init]) => init?.keepalive)
207-
expect(fallback?.[1]?.body).toBe(await bodies[0].text())
208-
expect(native.cancel).not.toHaveBeenCalled()
209-
} finally {
210-
fetch.mockResolvedValue(new Response(null, { status: 200 }))
211-
await executeComputerToolOnClient(id, { action: 'list_apps' }, timestamp)
238+
expect(native.execute).toHaveBeenCalledOnce()
212239
}
213-
const count = bodies.length
214-
window.dispatchEvent(new Event('pagehide'))
215-
expect(bodies).toHaveLength(count)
216-
expect(
217-
copilotConfirmBodySchema.parse(JSON.parse(String(fetch.mock.lastCall?.[1]?.body)))
218-
).toMatchObject({
219-
toolCallId: id,
220-
status: 'error',
221-
data: { dispatchState: 'not_started', outcomeUnknown: false },
222-
})
223-
expect(native.execute).toHaveBeenCalledOnce()
224-
})
240+
)
225241

226242
it('reports an uncertain action and cancels only while native work is still pending', async () => {
227243
const nativeResult = createDeferred<ComputerUseResult>()

‎apps/sim/lib/mothership/tools/client/page-exit-completion.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function compactCompletionForPageExit(
3535
...(data.doNotRetry === true ? { doNotRetry: true } : {}),
3636
...(data.sessionClosed === true ? { sessionClosed: true } : {}),
3737
resultOmittedDuringPageExit: true,
38-
note: 'The action reached a known terminal state, but its full result was too large for unload-safe delivery. Do not repeat a side-effecting action. Take a fresh observation to recover current state.',
38+
note: 'The full result was too large for unload-safe delivery. Do not repeat a side-effecting action. Take a fresh observation to recover current state.',
3939
},
4040
}
4141
}

0 commit comments

Comments
 (0)