Skip to content

Commit 95aff87

Browse files
committed
feat: add browser handoff creation
1 parent 9e15384 commit 95aff87

6 files changed

Lines changed: 93 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,18 @@ Clear the current conversation and start a fresh CLI session. The returned
379379
const { sessionId } = await agent.reset();
380380
```
381381

382+
#### `createBrowserHandoff(params?): Promise<BrowserHandoffCreateResult>`
383+
384+
Create a ten-minute, one-time handoff for the active session. Pass an extension
385+
ID to receive a Chrome extension URL, or an install URL for a web handoff.
386+
387+
```typescript
388+
const handoff = await agent.createBrowserHandoff({
389+
extensionId: 'your-extension-id',
390+
});
391+
console.log(handoff.url);
392+
```
393+
382394
#### `getState(): Promise<GetStateResult>`
383395

384396
Get the current agent state.

src/__tests__/session-control.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,37 @@ describe('session control RPCs', () => {
2929
const agent = Agent.fromSDK(sdk);
3030
await expect(agent.reset()).resolves.toEqual({ sessionId: 'session-reset' });
3131
});
32+
33+
it('creates a browser handoff through every public layer', async () => {
34+
const client = new RPCClient();
35+
const calls: Array<{ method: string; params?: unknown }> = [];
36+
const handoff = {
37+
token: 'handoff-token',
38+
sessionId: 'session-browser',
39+
workspaceRoot: '/workspace',
40+
createdAt: '2026-07-20T00:00:00.000Z',
41+
expiresAt: '2026-07-20T00:10:00.000Z',
42+
url: 'chrome-extension://extension/sidepanel.html?handoff=handoff-token',
43+
};
44+
getTransport(client).request = async (method, params) => {
45+
calls.push({ method, params });
46+
return handoff;
47+
};
48+
49+
const params = {
50+
extensionId: 'extension',
51+
installUrl: 'https://autohand.ai/chrome/installed',
52+
};
53+
await expect(client.createBrowserHandoff(params)).resolves.toEqual(handoff);
54+
expect(calls).toEqual([{
55+
method: 'autohand.browserHandoff.create',
56+
params,
57+
}]);
58+
59+
const sdk = {
60+
createBrowserHandoff: async () => handoff,
61+
} as unknown as AutohandSDK;
62+
const agent = Agent.fromSDK(sdk);
63+
await expect(agent.createBrowserHandoff(params)).resolves.toEqual(handoff);
64+
});
3265
});

src/rpc/client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import type {
3434
GetStateResult,
3535
GetMessagesParams,
3636
GetMessagesResult,
37+
BrowserHandoffCreateParams,
38+
BrowserHandoffCreateResult,
3739
PermissionDecision,
3840
PermissionResponseParams,
3941
SDKEvent,
@@ -350,6 +352,18 @@ export class RPCClient {
350352
return this.transport.request('autohand.getMessages', params) as Promise<GetMessagesResult>;
351353
}
352354

355+
/**
356+
* Create a one-time browser handoff for the active session.
357+
*/
358+
async createBrowserHandoff(
359+
params: BrowserHandoffCreateParams = {}
360+
): Promise<BrowserHandoffCreateResult> {
361+
return this.transport.request(
362+
'autohand.browserHandoff.create',
363+
params
364+
) as Promise<BrowserHandoffCreateResult>;
365+
}
366+
353367
/**
354368
* Respond to a permission request
355369
*

src/sdk/agent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import type {
3232
AutoresearchPruneResult,
3333
GetSkillsRegistryParams,
3434
GetSkillsRegistryResult,
35+
BrowserHandoffCreateParams,
36+
BrowserHandoffCreateResult,
3537
InstallSkillParams,
3638
InstallSkillResult,
3739
McpListServersResult,
@@ -553,6 +555,12 @@ export class Agent {
553555
return this.sdk.reset();
554556
}
555557

558+
async createBrowserHandoff(
559+
params: BrowserHandoffCreateParams = {}
560+
): Promise<BrowserHandoffCreateResult> {
561+
return this.sdk.createBrowserHandoff(params);
562+
}
563+
556564
async setPlanMode(enabled: boolean): Promise<void> {
557565
await this.sdk.setPlanMode(enabled);
558566
}

src/sdk/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import type {
3434
GetStateResult,
3535
GetMessagesParams,
3636
GetMessagesResult,
37+
BrowserHandoffCreateParams,
38+
BrowserHandoffCreateResult,
3739
ResetResult,
3840
PermissionDecisionScope,
3941
PermissionResponseParams,
@@ -1409,6 +1411,16 @@ export class AutohandSDK {
14091411
return this.client.getMessages(params);
14101412
}
14111413

1414+
/**
1415+
* Create a browser handoff URL for the active session.
1416+
*/
1417+
async createBrowserHandoff(
1418+
params: BrowserHandoffCreateParams = {}
1419+
): Promise<BrowserHandoffCreateResult> {
1420+
await this.ensureStarted();
1421+
return this.client.createBrowserHandoff(params);
1422+
}
1423+
14121424
// ============================================================================
14131425
// Permission Handling
14141426
// ============================================================================

src/types/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,6 +1848,11 @@ export interface GetMessagesParams {
18481848
limit?: number;
18491849
}
18501850

1851+
export interface BrowserHandoffCreateParams {
1852+
extensionId?: string;
1853+
installUrl?: string;
1854+
}
1855+
18511856
export interface PermissionResponseParams {
18521857
requestId: string;
18531858
decision?: PermissionDecision | PermissionDecisionAlias;
@@ -1876,6 +1881,15 @@ export interface ResetResult {
18761881
sessionId: string;
18771882
}
18781883

1884+
export interface BrowserHandoffCreateResult {
1885+
token: string;
1886+
sessionId: string;
1887+
workspaceRoot: string;
1888+
createdAt: string;
1889+
expiresAt: string;
1890+
url: string;
1891+
}
1892+
18791893
export interface GetStateResult {
18801894
status: 'idle' | 'processing' | 'waiting_permission';
18811895
sessionId: string | null;

0 commit comments

Comments
 (0)