appilot 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -7
- package/dist/dom/sensitiveFields.d.ts +33 -0
- package/dist/dom/sensitiveFields.js +72 -0
- package/dist/dom/surfaces.d.ts +81 -0
- package/dist/dom/surfaces.js +91 -0
- package/dist/dom/uniqueSelector.d.ts +23 -0
- package/dist/dom/uniqueSelector.js +104 -0
- package/dist/domResponder.d.ts +38 -0
- package/dist/domResponder.js +355 -0
- package/dist/focusedSessions/pageSdk.d.ts +107 -0
- package/dist/focusedSessions/pageSdk.js +238 -0
- package/dist/httpFetch.d.ts +52 -0
- package/dist/httpFetch.js +137 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +24 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +21 -0
- package/dist/sessions/types.d.ts +140 -0
- package/dist/sessions/types.js +12 -0
- package/dist/webmcp/registry.d.ts +75 -0
- package/dist/webmcp/registry.js +152 -0
- package/dist/webmcp/sdk.d.ts +38 -0
- package/dist/webmcp/sdk.js +81 -0
- package/dist/widget/boot.d.ts +91 -0
- package/dist/widget/boot.js +313 -0
- package/package.json +51 -16
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-page DOM responder for the agent runtime.
|
|
3
|
+
*
|
|
4
|
+
* Implements the six Web-MCP-aligned DOM tools (plus the `http_fetch`
|
|
5
|
+
* HTTP-proxy tool) the agent calls via the agent_dom_request SSE round-trip.
|
|
6
|
+
* See docs/agent/dom-tools.md for the wire format.
|
|
7
|
+
*
|
|
8
|
+
* This module is the in-page side of the contract, shared by both surfaces:
|
|
9
|
+
* - Chrome extension: invoked from the content-script message listener
|
|
10
|
+
* (content.ts) when the background relays an agent_dom_request to the
|
|
11
|
+
* active tab.
|
|
12
|
+
* - Embeddable widget: invoked directly by the widget's AgentDomBridge
|
|
13
|
+
* (services/AgentDomBridge.ts), which intercepts agent_dom_request on the
|
|
14
|
+
* widget SSE stream and POSTs the result to /widget/agent/dom-response.
|
|
15
|
+
*
|
|
16
|
+
* Element-id correlation: each call to get_page_outline / find_elements /
|
|
17
|
+
* inspect_element returns ephemeral ids that subsequent calls (inspect /
|
|
18
|
+
* wait_for) can dereference. Ids are kept in `currentTurnElementMap`; the
|
|
19
|
+
* map is cleared on each NEW turn (`resetTurn` on the request, or
|
|
20
|
+
* `resetDomToolTurn()`).
|
|
21
|
+
*
|
|
22
|
+
* Authoring controls bridge: when find_elements / inspect_element hit an
|
|
23
|
+
* element matched by an authored Control's locator, the `matches_authored_control`
|
|
24
|
+
* field returns the Control's semantic_id. The Controls registry is provided
|
|
25
|
+
* per turn (controlsForTurn).
|
|
26
|
+
*/
|
|
27
|
+
import { httpFetch } from './httpFetch.js';
|
|
28
|
+
import { invokeClientTool } from './webmcp/registry.js';
|
|
29
|
+
import { computeUniqueSelector } from './dom/uniqueSelector.js';
|
|
30
|
+
import { isSensitiveField } from './dom/sensitiveFields.js';
|
|
31
|
+
const MAX_HITS = 20;
|
|
32
|
+
const HIT_LIMIT_HARD = 50;
|
|
33
|
+
const MAX_TEXT_CHARS = 2000;
|
|
34
|
+
let nextElementId = 1;
|
|
35
|
+
let currentTurnElementMap = new Map();
|
|
36
|
+
let controlsForTurn = [];
|
|
37
|
+
/** Clear the per-turn element-id map. Call at the start of every new turn. */
|
|
38
|
+
export function resetDomToolTurn() {
|
|
39
|
+
currentTurnElementMap = new Map();
|
|
40
|
+
nextElementId = 1;
|
|
41
|
+
}
|
|
42
|
+
function freshId() {
|
|
43
|
+
return `el-${nextElementId++}`;
|
|
44
|
+
}
|
|
45
|
+
function registerElement(el) {
|
|
46
|
+
for (const [id, existing] of currentTurnElementMap) {
|
|
47
|
+
if (existing === el)
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
const id = freshId();
|
|
51
|
+
currentTurnElementMap.set(id, el);
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
function rectOf(el) {
|
|
55
|
+
const r = el.getBoundingClientRect();
|
|
56
|
+
return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
|
|
57
|
+
}
|
|
58
|
+
function isVisible(el) {
|
|
59
|
+
if (!(el instanceof HTMLElement) && !(el instanceof SVGElement))
|
|
60
|
+
return false;
|
|
61
|
+
const r = el.getBoundingClientRect();
|
|
62
|
+
if (r.width === 0 && r.height === 0)
|
|
63
|
+
return false;
|
|
64
|
+
const style = window.getComputedStyle(el);
|
|
65
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
66
|
+
return false;
|
|
67
|
+
if (parseFloat(style.opacity || '1') === 0)
|
|
68
|
+
return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function accessibleName(el) {
|
|
72
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
73
|
+
if (ariaLabel)
|
|
74
|
+
return ariaLabel.trim();
|
|
75
|
+
const labelledby = el.getAttribute('aria-labelledby');
|
|
76
|
+
if (labelledby) {
|
|
77
|
+
const ref = document.getElementById(labelledby);
|
|
78
|
+
if (ref)
|
|
79
|
+
return (ref.textContent || '').trim();
|
|
80
|
+
}
|
|
81
|
+
const text = (el.textContent || '').trim();
|
|
82
|
+
return text.slice(0, 120);
|
|
83
|
+
}
|
|
84
|
+
function roleOf(el) {
|
|
85
|
+
const explicit = el.getAttribute('role');
|
|
86
|
+
if (explicit)
|
|
87
|
+
return explicit;
|
|
88
|
+
const tag = el.tagName.toLowerCase();
|
|
89
|
+
if (tag === 'button')
|
|
90
|
+
return 'button';
|
|
91
|
+
if (tag === 'a')
|
|
92
|
+
return 'link';
|
|
93
|
+
if (tag === 'input') {
|
|
94
|
+
const type = el.type || 'text';
|
|
95
|
+
if (type === 'checkbox')
|
|
96
|
+
return 'checkbox';
|
|
97
|
+
if (type === 'radio')
|
|
98
|
+
return 'radio';
|
|
99
|
+
return 'textbox';
|
|
100
|
+
}
|
|
101
|
+
if (tag === 'textarea')
|
|
102
|
+
return 'textbox';
|
|
103
|
+
if (tag === 'select')
|
|
104
|
+
return 'combobox';
|
|
105
|
+
return tag;
|
|
106
|
+
}
|
|
107
|
+
function matchesAuthoredControl(el) {
|
|
108
|
+
for (const c of controlsForTurn) {
|
|
109
|
+
try {
|
|
110
|
+
const candidate = document.querySelector(c.locator);
|
|
111
|
+
if (candidate === el)
|
|
112
|
+
return c.semantic_id;
|
|
113
|
+
}
|
|
114
|
+
catch { /* invalid selector */ }
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function elementSummary(el) {
|
|
119
|
+
const id = registerElement(el);
|
|
120
|
+
const disableable = el;
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
tag: el.tagName.toLowerCase(),
|
|
124
|
+
role: roleOf(el),
|
|
125
|
+
name: accessibleName(el),
|
|
126
|
+
text: (el.textContent || '').trim().slice(0, 120),
|
|
127
|
+
visible: isVisible(el),
|
|
128
|
+
disabled: 'disabled' in disableable ? !!disableable.disabled : false,
|
|
129
|
+
rect: rectOf(el),
|
|
130
|
+
selector: computeUniqueSelector(el),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ── Tool implementations ────────────────────────────────────────────────
|
|
134
|
+
function getPageOutline() {
|
|
135
|
+
const landmarks = [];
|
|
136
|
+
for (const el of document.querySelectorAll('[role="navigation"], [role="main"], [role="banner"], [role="contentinfo"], [role="complementary"], header, nav, main, aside, footer')) {
|
|
137
|
+
landmarks.push({
|
|
138
|
+
id: registerElement(el),
|
|
139
|
+
role: roleOf(el),
|
|
140
|
+
name: accessibleName(el),
|
|
141
|
+
rect: rectOf(el),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const headings = [];
|
|
145
|
+
for (const el of document.querySelectorAll('h1, h2, h3, h4')) {
|
|
146
|
+
headings.push({
|
|
147
|
+
id: registerElement(el),
|
|
148
|
+
level: Number(el.tagName.substring(1)),
|
|
149
|
+
text: (el.textContent || '').trim().slice(0, 120),
|
|
150
|
+
rect: rectOf(el),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return { landmarks, headings };
|
|
154
|
+
}
|
|
155
|
+
function findElements(args) {
|
|
156
|
+
const selector = typeof args?.selector === 'string' ? args.selector : undefined;
|
|
157
|
+
const role = typeof args?.role === 'string' ? args.role : undefined;
|
|
158
|
+
const name = typeof args?.name === 'string' ? args.name : undefined;
|
|
159
|
+
const visibleOnly = args?.visible_only !== false;
|
|
160
|
+
const limit = Math.min(Math.max(Number(args?.limit) || MAX_HITS, 1), HIT_LIMIT_HARD);
|
|
161
|
+
if (!selector && !role && !name) {
|
|
162
|
+
return { error: 'invalid_args', detail: 'at least one of selector, role, name required' };
|
|
163
|
+
}
|
|
164
|
+
let candidates = [];
|
|
165
|
+
if (selector) {
|
|
166
|
+
try {
|
|
167
|
+
candidates = Array.from(document.querySelectorAll(selector));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return { error: 'invalid_selector', detail: selector };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
candidates = Array.from(document.querySelectorAll('*'));
|
|
175
|
+
}
|
|
176
|
+
const hits = [];
|
|
177
|
+
const nameLower = name?.toLowerCase();
|
|
178
|
+
for (const el of candidates) {
|
|
179
|
+
if (visibleOnly && !isVisible(el))
|
|
180
|
+
continue;
|
|
181
|
+
if (role && roleOf(el) !== role)
|
|
182
|
+
continue;
|
|
183
|
+
if (nameLower && !accessibleName(el).toLowerCase().includes(nameLower))
|
|
184
|
+
continue;
|
|
185
|
+
hits.push(elementSummary(el));
|
|
186
|
+
if (hits.length >= limit)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
return { hits, truncated: candidates.length > hits.length };
|
|
190
|
+
}
|
|
191
|
+
function inspectElement(args) {
|
|
192
|
+
const id = String(args?.id || '');
|
|
193
|
+
const el = currentTurnElementMap.get(id);
|
|
194
|
+
if (!el)
|
|
195
|
+
return { error: 'unknown_id', detail: id };
|
|
196
|
+
const attributes = {};
|
|
197
|
+
// A server-rendered `value="..."` on a credential field is the same secret by
|
|
198
|
+
// another route, so the attribute dump is filtered the same way.
|
|
199
|
+
const redactValue = isSensitiveField(el);
|
|
200
|
+
for (const attr of Array.from(el.attributes)) {
|
|
201
|
+
attributes[attr.name] = redactValue && attr.name === 'value' ? '' : attr.value;
|
|
202
|
+
}
|
|
203
|
+
const parentChain = [];
|
|
204
|
+
let parent = el.parentElement;
|
|
205
|
+
while (parent && parentChain.length < 8) {
|
|
206
|
+
parentChain.push({ tag: parent.tagName.toLowerCase(), id: registerElement(parent), role: roleOf(parent) });
|
|
207
|
+
parent = parent.parentElement;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...elementSummary(el),
|
|
211
|
+
attributes,
|
|
212
|
+
parent_chain: parentChain,
|
|
213
|
+
matches_authored_control: matchesAuthoredControl(el),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function readFormState(args) {
|
|
217
|
+
let forms = [];
|
|
218
|
+
if (args?.form_id) {
|
|
219
|
+
const el = currentTurnElementMap.get(String(args.form_id));
|
|
220
|
+
if (el)
|
|
221
|
+
forms = [el];
|
|
222
|
+
}
|
|
223
|
+
else if (typeof args?.selector === 'string') {
|
|
224
|
+
try {
|
|
225
|
+
forms = Array.from(document.querySelectorAll(args.selector));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return { error: 'invalid_selector', detail: args.selector };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
forms = Array.from(document.querySelectorAll('form'));
|
|
233
|
+
}
|
|
234
|
+
const result = forms.slice(0, 5).map(form => {
|
|
235
|
+
const fields = [];
|
|
236
|
+
for (const fieldEl of form.querySelectorAll('input, select, textarea, [contenteditable]')) {
|
|
237
|
+
const f = fieldEl;
|
|
238
|
+
// The value is withheld for credential fields, never the field itself:
|
|
239
|
+
// the agent still needs to know the password input EXISTS and whether it
|
|
240
|
+
// is filled, so it can reason about the form without reading the secret.
|
|
241
|
+
const sensitive = isSensitiveField(f);
|
|
242
|
+
fields.push({
|
|
243
|
+
name: f.name || f.id || accessibleName(f),
|
|
244
|
+
label: accessibleName(f),
|
|
245
|
+
value: sensitive ? '' : (f.value ?? f.textContent ?? ''),
|
|
246
|
+
value_withheld: sensitive || undefined,
|
|
247
|
+
filled: sensitive ? !!f.value : undefined,
|
|
248
|
+
visible: isVisible(f),
|
|
249
|
+
disabled: !!f.disabled,
|
|
250
|
+
required: !!f.required,
|
|
251
|
+
validation_error: f.validationMessage || null,
|
|
252
|
+
matches_authored_control: matchesAuthoredControl(f) ?? null,
|
|
253
|
+
selector: computeUniqueSelector(f),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return { form_id: registerElement(form), fields };
|
|
257
|
+
});
|
|
258
|
+
return { forms: result };
|
|
259
|
+
}
|
|
260
|
+
function getVisibleText(args) {
|
|
261
|
+
let region = null;
|
|
262
|
+
if (args?.region_id)
|
|
263
|
+
region = currentTurnElementMap.get(String(args.region_id)) || null;
|
|
264
|
+
if (!region && typeof args?.selector === 'string') {
|
|
265
|
+
try {
|
|
266
|
+
region = document.querySelector(args.selector);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return { error: 'invalid_selector', detail: args.selector };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!region)
|
|
273
|
+
return { error: 'no_target', detail: 'pass region_id or selector' };
|
|
274
|
+
const max = Math.min(Math.max(Number(args?.max_chars) || MAX_TEXT_CHARS, 50), 10_000);
|
|
275
|
+
const text = region.innerText || region.textContent || '';
|
|
276
|
+
const trimmed = text.trim();
|
|
277
|
+
return { text: trimmed.slice(0, max), truncated: trimmed.length > max };
|
|
278
|
+
}
|
|
279
|
+
async function waitFor(args) {
|
|
280
|
+
const event = String(args?.event || '');
|
|
281
|
+
const target = args?.target || {};
|
|
282
|
+
const timeoutMs = Math.min(Math.max(Number(args?.timeout_ms) || 5000, 500), 30_000);
|
|
283
|
+
const t0 = performance.now();
|
|
284
|
+
const check = () => {
|
|
285
|
+
let el = null;
|
|
286
|
+
if (target.id)
|
|
287
|
+
el = currentTurnElementMap.get(String(target.id)) || null;
|
|
288
|
+
if (!el && target.selector) {
|
|
289
|
+
try {
|
|
290
|
+
el = document.querySelector(target.selector);
|
|
291
|
+
}
|
|
292
|
+
catch { /* noop */ }
|
|
293
|
+
}
|
|
294
|
+
if (event === 'element_visible')
|
|
295
|
+
return { matched: !!el && isVisible(el), evidence: el ? { id: registerElement(el) } : undefined };
|
|
296
|
+
if (event === 'element_disappears')
|
|
297
|
+
return { matched: !el || !isVisible(el) };
|
|
298
|
+
if (event === 'text_appears') {
|
|
299
|
+
const text = (target.text_substring || '').toLowerCase();
|
|
300
|
+
if (!text)
|
|
301
|
+
return { matched: false };
|
|
302
|
+
const body = document.body?.innerText?.toLowerCase() || '';
|
|
303
|
+
return { matched: body.includes(text), evidence: { text_excerpt: target.text_substring } };
|
|
304
|
+
}
|
|
305
|
+
if (event === 'text_disappears') {
|
|
306
|
+
const text = (target.text_substring || '').toLowerCase();
|
|
307
|
+
if (!text)
|
|
308
|
+
return { matched: true };
|
|
309
|
+
const body = document.body?.innerText?.toLowerCase() || '';
|
|
310
|
+
return { matched: !body.includes(text) };
|
|
311
|
+
}
|
|
312
|
+
if (event === 'value_changes') {
|
|
313
|
+
// Best-effort: snapshot value first time we're called for this target.
|
|
314
|
+
// A more robust impl would diff against a snapshot taken at args-receive.
|
|
315
|
+
return { matched: false };
|
|
316
|
+
}
|
|
317
|
+
return { matched: false };
|
|
318
|
+
};
|
|
319
|
+
return new Promise(resolve => {
|
|
320
|
+
const tick = () => {
|
|
321
|
+
const r = check();
|
|
322
|
+
const elapsed = performance.now() - t0;
|
|
323
|
+
if (r.matched)
|
|
324
|
+
return resolve({ matched: true, elapsed_ms: Math.round(elapsed), evidence: r.evidence });
|
|
325
|
+
if (elapsed >= timeoutMs)
|
|
326
|
+
return resolve({ matched: false, elapsed_ms: Math.round(elapsed), reason: 'timeout' });
|
|
327
|
+
setTimeout(tick, 150);
|
|
328
|
+
};
|
|
329
|
+
tick();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export async function runDomTool(req) {
|
|
333
|
+
if (req.resetTurn) {
|
|
334
|
+
resetDomToolTurn();
|
|
335
|
+
}
|
|
336
|
+
if (req.controlsForTurn)
|
|
337
|
+
controlsForTurn = req.controlsForTurn;
|
|
338
|
+
switch (req.tool) {
|
|
339
|
+
case 'get_page_outline': return getPageOutline();
|
|
340
|
+
case 'find_elements': return findElements(req.args);
|
|
341
|
+
case 'inspect_element': return inspectElement(req.args);
|
|
342
|
+
case 'read_form_state': return readFormState(req.args);
|
|
343
|
+
case 'get_visible_text': return getVisibleText(req.args);
|
|
344
|
+
case 'wait_for': return waitFor(req.args);
|
|
345
|
+
case 'http_fetch': return httpFetch(req.args);
|
|
346
|
+
case 'invoke_client_action': {
|
|
347
|
+
const action = typeof req.args.action === 'string' ? req.args.action : '';
|
|
348
|
+
const actionArgs = (req.args.args && typeof req.args.args === 'object')
|
|
349
|
+
? req.args.args
|
|
350
|
+
: {};
|
|
351
|
+
return invokeClientTool(action, actionArgs);
|
|
352
|
+
}
|
|
353
|
+
default: return { error: 'unknown_tool', detail: req.tool };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused Sessions page SDK: the surface a host app calls to run an
|
|
3
|
+
* app-initiated, mission-scoped agent conversation and receive its structured
|
|
4
|
+
* outcome (docs/agent/focused-sessions.md).
|
|
5
|
+
*
|
|
6
|
+
* const handle = await startFocusedSession({ template_id: 'role-play', variables: { role: 'Sam' } });
|
|
7
|
+
* handle.onOutcome(outcome => saveEvidence(outcome));
|
|
8
|
+
*
|
|
9
|
+
* Architecture mirrors the WebMCP registry: the SDK itself performs NO HTTP
|
|
10
|
+
* and holds NO credentials. It delegates to a PROVIDER the assistant surface
|
|
11
|
+
* registers on `window.__APPILOT_SESSIONS__`:
|
|
12
|
+
* - the widget registers a direct provider (it shares the page realm and
|
|
13
|
+
* owns the widget-key + user-token REST calls), and
|
|
14
|
+
* - the extension's MAIN-world script registers a postMessage relay to its
|
|
15
|
+
* isolated content script -> background -> JWT REST.
|
|
16
|
+
* A window-anchored global (not a module singleton) because the host app's
|
|
17
|
+
* bundle and the assistant surface's bundle are DIFFERENT module graphs; a
|
|
18
|
+
* module-level variable would give each its own copy.
|
|
19
|
+
*
|
|
20
|
+
* Import from `appilot` (same shipping posture as `registerTool`).
|
|
21
|
+
*/
|
|
22
|
+
import type { FocusedSessionEndedEvent, FocusedSessionEndStatus, FocusedSessionOutcomeEvent, FocusedSessionStartRequest, FocusedSessionStartResult } from '../sessions/types.js';
|
|
23
|
+
/** What the assistant surface (widget / extension bridge) implements. */
|
|
24
|
+
export interface FocusedSessionProvider {
|
|
25
|
+
start(request: FocusedSessionStartRequest): Promise<FocusedSessionStartResult>;
|
|
26
|
+
end(focusedSessionId: string): Promise<void>;
|
|
27
|
+
/** Subscribe to outcome events; returns an unsubscribe function. */
|
|
28
|
+
onOutcome(cb: (event: FocusedSessionOutcomeEvent) => void): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to terminal-WITHOUT-outcome events; returns an unsubscribe
|
|
31
|
+
* function. Optional so older surfaces (the extension MAIN-world bridge)
|
|
32
|
+
* keep type-checking until they wire it; the page handle degrades to a
|
|
33
|
+
* never-firing subscription in that case.
|
|
34
|
+
*/
|
|
35
|
+
onEnded?(cb: (event: FocusedSessionEndedEvent) => void): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Re-attach to a session this identity already started, after the page lost
|
|
38
|
+
* its handle (a full browser reload). Resolves to null when the session is
|
|
39
|
+
* no longer running. Optional so older surfaces keep type-checking; without
|
|
40
|
+
* it `resumeFocusedSession` resolves to null rather than throwing.
|
|
41
|
+
*/
|
|
42
|
+
resume?(focusedSessionId: string): Promise<FocusedSessionStartResult | null>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Called by the assistant surface (widget boot / extension MAIN-world bridge)
|
|
46
|
+
* to expose the session transport to the page. Last registration wins: when
|
|
47
|
+
* both the widget and the extension are present on a page, the most recent
|
|
48
|
+
* surface serves new sessions (in practice hosts embed exactly one).
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerFocusedSessionProvider(provider: FocusedSessionProvider): void;
|
|
51
|
+
/** A running (or finished) focused session, from the host page's side. */
|
|
52
|
+
export interface FocusedSessionPageHandle {
|
|
53
|
+
focusedSessionId: string;
|
|
54
|
+
conversationId: number;
|
|
55
|
+
/** Resolved display name (the session header the assistant surface shows). */
|
|
56
|
+
name: string;
|
|
57
|
+
/**
|
|
58
|
+
* Subscribe to THIS session's outcome. The outcome is AI output: run it
|
|
59
|
+
* through your app's own review/validation model before treating it as
|
|
60
|
+
* truth. Returns an unsubscribe function.
|
|
61
|
+
*/
|
|
62
|
+
onOutcome(cb: (outcome: Record<string, unknown>) => void): () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Subscribe to THIS session ending WITHOUT an outcome: abandoned by the
|
|
65
|
+
* server (turn budget exhausted), expired, failed, or ended by the page
|
|
66
|
+
* (`end()`). Fires exactly once per session and is mutually exclusive with
|
|
67
|
+
* `onOutcome`, a completed session fires only `onOutcome`. Use it to stop
|
|
68
|
+
* waiting: without it a page that waits for the outcome hangs forever when
|
|
69
|
+
* the session dies outcome-less. Returns an unsubscribe function.
|
|
70
|
+
*/
|
|
71
|
+
onEnded(cb: (event: {
|
|
72
|
+
focusedSessionId: string;
|
|
73
|
+
status: FocusedSessionEndStatus;
|
|
74
|
+
}) => void): () => void;
|
|
75
|
+
/** Abandon the session (no outcome will be produced). */
|
|
76
|
+
end(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Start a focused session. Resolves once the assistant surface has created
|
|
80
|
+
* the session server-side (mission resolved + conversation bound); rejects
|
|
81
|
+
* with the surface's typed error message when the template is unknown, a
|
|
82
|
+
* required variable is missing, or no assistant surface is present.
|
|
83
|
+
*/
|
|
84
|
+
export declare function startFocusedSession(request: FocusedSessionStartRequest): Promise<FocusedSessionPageHandle>;
|
|
85
|
+
/**
|
|
86
|
+
* The id of the session this tab last started, if it has not reached a terminal
|
|
87
|
+
* state. Useful for deciding whether to offer a "continue" affordance before
|
|
88
|
+
* paying for the round-trip that `resumeFocusedSession` makes.
|
|
89
|
+
*/
|
|
90
|
+
export declare function getResumableFocusedSessionId(): string | null;
|
|
91
|
+
/** Forget the remembered session without ending it server-side. */
|
|
92
|
+
export declare function forgetFocusedSession(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Re-attach to a session this tab started before a reload.
|
|
95
|
+
*
|
|
96
|
+
* Resolves to a handle when the session is still running and belongs to the
|
|
97
|
+
* current identity, and to `null` otherwise: nothing remembered, the assistant
|
|
98
|
+
* surface cannot resume, the session already ended, or it is not this user's.
|
|
99
|
+
* Ownership is checked server-side; possessing an id proves nothing.
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* const handle = await resumeFocusedSession();
|
|
103
|
+
* if (handle) handle.onOutcome(saveEvidence);
|
|
104
|
+
* else showActivityFinishedState();
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function resumeFocusedSession(focusedSessionId?: string): Promise<FocusedSessionPageHandle | null>;
|