@botiverse/porthole 0.1.2 → 0.1.4
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/dist/annotate/types.d.ts +120 -0
- package/dist/annotate/types.js +42 -0
- package/dist/annotate/validate.d.ts +11 -0
- package/dist/annotate/validate.js +156 -0
- package/dist/bridge/bridge.d.ts +4 -0
- package/dist/bridge/bridge.js +12 -1
- package/dist/compile/policy.d.ts +10 -3
- package/dist/compile/policy.js +16 -5
- package/dist/config/capabilities.d.ts +37 -4
- package/dist/config/capabilities.js +20 -0
- package/dist/config/schema.d.ts +11 -0
- package/dist/config/validate.js +12 -0
- package/dist/conformance/clipboard.d.ts +2 -0
- package/dist/conformance/clipboard.js +167 -0
- package/dist/conformance/kit.d.ts +1 -1
- package/dist/conformance/probes.js +2 -0
- package/dist/conformance/served.d.ts +8 -1
- package/dist/conformance/served.js +12 -8
- package/dist/guide/guide.js +11 -5
- package/dist/host/clipboard.d.ts +11 -0
- package/dist/host/clipboard.js +43 -0
- package/dist/host/create-host.js +165 -7
- package/dist/host/types.d.ts +68 -2
- package/dist/index.d.ts +8 -5
- package/dist/index.js +7 -3
- package/dist/mcp/adapter.js +10 -1
- package/dist/presets/presets.d.ts +3 -1
- package/dist/presets/presets.js +1 -0
- package/dist/protocol/mcp-apps.d.ts +5 -0
- package/dist/protocol/mcp-apps.js +5 -0
- package/dist/protocol/shell-adapter.js +6 -2
- package/dist/react/dialog.js +2 -0
- package/dist/react/frame.d.ts +50 -2
- package/dist/react/frame.js +153 -11
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/toast.d.ts +33 -0
- package/dist/react/toast.js +42 -0
- package/dist/server/naming.d.ts +6 -0
- package/dist/server/naming.js +38 -0
- package/dist/server/pages.js +2 -0
- package/dist/server/server.js +49 -17
- package/dist/server/types.d.ts +6 -3
- package/dist/shell/annotate.d.ts +20 -0
- package/dist/shell/annotate.js +700 -0
- package/dist/shell/document.d.ts +6 -0
- package/dist/shell/document.js +12 -3
- package/dist/shell/plan.d.ts +2 -0
- package/dist/shell/plan.js +2 -1
- package/dist/shell/runtime.d.ts +4 -1
- package/dist/shell/runtime.js +71 -3
- package/package.json +1 -1
- package/src/annotate/types.ts +126 -0
- package/src/annotate/validate.ts +186 -0
- package/src/bridge/bridge.ts +13 -0
- package/src/compile/policy.ts +17 -5
- package/src/config/capabilities.ts +33 -3
- package/src/config/schema.ts +12 -0
- package/src/config/validate.ts +15 -0
- package/src/conformance/clipboard.ts +184 -0
- package/src/conformance/kit.ts +1 -1
- package/src/conformance/probes.ts +2 -0
- package/src/conformance/served.ts +24 -9
- package/src/guide/guide.ts +22 -5
- package/src/host/clipboard.ts +41 -0
- package/src/host/create-host.ts +181 -7
- package/src/host/types.ts +64 -2
- package/src/index.ts +24 -0
- package/src/mcp/adapter.ts +10 -1
- package/src/presets/presets.ts +4 -0
- package/src/protocol/mcp-apps.ts +6 -0
- package/src/protocol/shell-adapter.ts +11 -2
- package/src/react/dialog.tsx +2 -0
- package/src/react/frame.tsx +231 -13
- package/src/react/index.ts +9 -1
- package/src/react/toast.tsx +96 -0
- package/src/server/naming.ts +41 -0
- package/src/server/pages.ts +2 -0
- package/src/server/server.ts +61 -20
- package/src/server/types.ts +6 -3
- package/src/shell/annotate.ts +700 -0
- package/src/shell/document.ts +21 -3
- package/src/shell/plan.ts +4 -1
- package/src/shell/runtime.ts +71 -3
package/src/host/create-host.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANNOTATE_METHODS,
|
|
3
|
+
type AnnotateMode,
|
|
4
|
+
type AnnotationMark,
|
|
5
|
+
type AnnotationReport,
|
|
6
|
+
} from "../annotate/types.ts";
|
|
7
|
+
import { annotateMode, checkMarks, parseReport } from "../annotate/validate.ts";
|
|
1
8
|
import { createBridge, type BridgeEvent } from "../bridge/bridge.ts";
|
|
9
|
+
import type { HostFunctionBridge } from "../bridge/types.ts";
|
|
2
10
|
import { compile } from "../compile/compile.ts";
|
|
3
11
|
import { declaredCapabilities } from "../compile/policy.ts";
|
|
4
12
|
import { capabilitySpec } from "../config/capabilities.ts";
|
|
@@ -17,6 +25,8 @@ import {
|
|
|
17
25
|
import { shellAdapter } from "../protocol/shell-adapter.ts";
|
|
18
26
|
import { resolveTheme } from "../theme/variables.ts";
|
|
19
27
|
import type {
|
|
28
|
+
AnnotationControls,
|
|
29
|
+
HostErrorKind,
|
|
20
30
|
DisplayEvents,
|
|
21
31
|
DisplayHandle,
|
|
22
32
|
HostOptions,
|
|
@@ -31,6 +41,17 @@ const DEFAULT_LOAD_TIMEOUT_MS = 15_000;
|
|
|
31
41
|
const MAX_RESTARTS = 3;
|
|
32
42
|
|
|
33
43
|
const BASE_KEYS = new Set(["hostContextChanged", "teardown", "close"]);
|
|
44
|
+
|
|
45
|
+
/** The frame's annotation reports, by method. */
|
|
46
|
+
const REPORTS: Readonly<Record<string, AnnotationReport["type"]>> = {
|
|
47
|
+
[ANNOTATE_METHODS.selected]: "selected",
|
|
48
|
+
[ANNOTATE_METHODS.unselected]: "unselected",
|
|
49
|
+
[ANNOTATE_METHODS.cancelled]: "cancelled",
|
|
50
|
+
[ANNOTATE_METHODS.anchored]: "anchored",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
54
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
55
|
const STANDARD = new Set(MCP_APPS_STYLE_VARIABLES);
|
|
35
56
|
|
|
36
57
|
/**
|
|
@@ -76,6 +97,16 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
76
97
|
(listener as (detail: DisplayEvents[E]) => void)(detail);
|
|
77
98
|
}
|
|
78
99
|
};
|
|
100
|
+
// What the content did wrong (the agent's to hear and fix), and what failed on the host's
|
|
101
|
+
// side (its own to handle), each on its own event and on `error` with its source.
|
|
102
|
+
const contentError = (message: string) => {
|
|
103
|
+
emit("contentError", { message });
|
|
104
|
+
emit("error", { message, source: "content" });
|
|
105
|
+
};
|
|
106
|
+
const hostError = (message: string, kind: HostErrorKind) => {
|
|
107
|
+
emit("hostError", { message, kind });
|
|
108
|
+
emit("error", { message, source: "host" });
|
|
109
|
+
};
|
|
79
110
|
|
|
80
111
|
let scheme: ColorScheme = mount.scheme;
|
|
81
112
|
let height = mount.initialHeightPx ?? 0;
|
|
@@ -90,6 +121,129 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
90
121
|
const view = container.ownerDocument.defaultView;
|
|
91
122
|
// The adapter's own operations, the last call of each, replayed into every new frame.
|
|
92
123
|
const replay = new Map<string, unknown[]>();
|
|
124
|
+
// Annotations: the host's state, given to each frame once it finished its handshake.
|
|
125
|
+
const annotates = config.capabilities.annotate !== undefined;
|
|
126
|
+
let annotating: Required<AnnotateMode> | null = null;
|
|
127
|
+
let marks: AnnotationMark[] = [];
|
|
128
|
+
let activeMark: string | null = null;
|
|
129
|
+
let handshaken = false;
|
|
130
|
+
const toFrame = (method: string, params: Record<string, unknown>) => {
|
|
131
|
+
if (handshaken && surface !== null) {
|
|
132
|
+
surface.transport.send({ jsonrpc: "2.0", method, params });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const giveAnnotations = () => {
|
|
136
|
+
if (marks.length > 0) {
|
|
137
|
+
toFrame(ANNOTATE_METHODS.show, {
|
|
138
|
+
marks,
|
|
139
|
+
...(activeMark === null ? {} : { active: activeMark }),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (annotating !== null) {
|
|
143
|
+
toFrame(ANNOTATE_METHODS.start, { ...annotating });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* A report from the frame: checked, taken only while the host asked for it (a selection
|
|
148
|
+
* only in select mode, results only for its own marks), then through the bridge's gates.
|
|
149
|
+
*/
|
|
150
|
+
const onAnnotation = (raw: unknown, bridge: HostFunctionBridge) => {
|
|
151
|
+
if (
|
|
152
|
+
!isRecord(raw) ||
|
|
153
|
+
raw.jsonrpc !== "2.0" ||
|
|
154
|
+
"id" in raw ||
|
|
155
|
+
typeof raw.method !== "string"
|
|
156
|
+
) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const type = REPORTS[raw.method];
|
|
160
|
+
if (type === undefined) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let report = parseReport({ ...(isRecord(raw.params) ? raw.params : {}), type });
|
|
164
|
+
if (report === null) {
|
|
165
|
+
void bridge.call("annotate", null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
switch (report.type) {
|
|
169
|
+
case "selected": {
|
|
170
|
+
const kind = report.anchor.kind;
|
|
171
|
+
if (
|
|
172
|
+
annotating === null ||
|
|
173
|
+
!(kind === "element" ? annotating.elements : annotating.text)
|
|
174
|
+
) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (kind === "element") {
|
|
178
|
+
annotating = annotating.text ? { elements: false, text: true } : null;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "unselected":
|
|
183
|
+
if (annotating?.text !== true) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case "cancelled":
|
|
188
|
+
if (annotating?.elements !== true) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
annotating = annotating.text ? { elements: false, text: true } : null;
|
|
192
|
+
break;
|
|
193
|
+
case "anchored": {
|
|
194
|
+
const known = new Set(marks.map((mark) => mark.id));
|
|
195
|
+
const results = report.results.filter((result) => known.has(result.id));
|
|
196
|
+
if (results.length === 0) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
report = { type: "anchored", results };
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
void bridge.call("annotate", report);
|
|
204
|
+
};
|
|
205
|
+
const needsAnnotate = (operation: string) => {
|
|
206
|
+
if (!annotates) {
|
|
207
|
+
throw new TypeError(`annotations.${operation}: the config doesn't declare annotate`);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const annotations: AnnotationControls = {
|
|
211
|
+
start(mode) {
|
|
212
|
+
needsAnnotate("start");
|
|
213
|
+
annotating = annotateMode(mode);
|
|
214
|
+
toFrame(ANNOTATE_METHODS.start, { ...annotating });
|
|
215
|
+
},
|
|
216
|
+
stop() {
|
|
217
|
+
needsAnnotate("stop");
|
|
218
|
+
annotating = null;
|
|
219
|
+
toFrame(ANNOTATE_METHODS.stop, {});
|
|
220
|
+
},
|
|
221
|
+
show(next) {
|
|
222
|
+
needsAnnotate("show");
|
|
223
|
+
marks = checkMarks(next);
|
|
224
|
+
if (activeMark !== null && !marks.some((mark) => mark.id === activeMark)) {
|
|
225
|
+
activeMark = null;
|
|
226
|
+
}
|
|
227
|
+
toFrame(ANNOTATE_METHODS.show, {
|
|
228
|
+
marks,
|
|
229
|
+
...(activeMark === null ? {} : { active: activeMark }),
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
focus(id) {
|
|
233
|
+
needsAnnotate("focus");
|
|
234
|
+
if (!marks.some((mark) => mark.id === id)) {
|
|
235
|
+
throw new TypeError(`annotations.focus: no mark ${JSON.stringify(id)} is shown`);
|
|
236
|
+
}
|
|
237
|
+
activeMark = id;
|
|
238
|
+
toFrame(ANNOTATE_METHODS.focus, { id });
|
|
239
|
+
},
|
|
240
|
+
clear() {
|
|
241
|
+
needsAnnotate("clear");
|
|
242
|
+
marks = [];
|
|
243
|
+
activeMark = null;
|
|
244
|
+
toFrame(ANNOTATE_METHODS.clear, {});
|
|
245
|
+
},
|
|
246
|
+
};
|
|
93
247
|
|
|
94
248
|
const hostContext = (): HostContext => {
|
|
95
249
|
const width = container.getBoundingClientRect().width;
|
|
@@ -134,7 +288,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
134
288
|
return;
|
|
135
289
|
}
|
|
136
290
|
case "error":
|
|
137
|
-
|
|
291
|
+
contentError(event.message);
|
|
138
292
|
return;
|
|
139
293
|
case "refused":
|
|
140
294
|
emit("refused", { capability: event.capability, refusal: event.refusal });
|
|
@@ -142,10 +296,14 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
142
296
|
case "muted":
|
|
143
297
|
emit("muted", undefined);
|
|
144
298
|
return;
|
|
299
|
+
case "annotation":
|
|
300
|
+
emit("annotation", event.report);
|
|
301
|
+
return;
|
|
145
302
|
}
|
|
146
303
|
};
|
|
147
304
|
|
|
148
305
|
const stop = () => {
|
|
306
|
+
handshaken = false;
|
|
149
307
|
abort?.abort();
|
|
150
308
|
abort = null;
|
|
151
309
|
outcomeWatch?.stop();
|
|
@@ -164,8 +322,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
164
322
|
allow: compiled.allow,
|
|
165
323
|
referrerPolicy: compiled.referrerPolicy,
|
|
166
324
|
colorScheme: scheme,
|
|
167
|
-
// An app reports no size: its frame fills the host's container, at least the minimum
|
|
168
|
-
|
|
325
|
+
// An app reports no size: its frame fills the host's container, at least the minimum;
|
|
326
|
+
// so does any display the host mounts to fill (its content scrolls inside).
|
|
327
|
+
...(speaks && mount.fill !== true
|
|
169
328
|
? { initialHeightPx: height }
|
|
170
329
|
: { fill: true, initialHeightPx: config.limits.minHeightPx }),
|
|
171
330
|
});
|
|
@@ -197,6 +356,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
197
356
|
userActivation: () => created.userActivation(),
|
|
198
357
|
onEvent: onBridge,
|
|
199
358
|
});
|
|
359
|
+
if (annotates) {
|
|
360
|
+
created.transport.listen((raw) => onAnnotation(raw, bridge));
|
|
361
|
+
}
|
|
200
362
|
const opened = adapter.connect({
|
|
201
363
|
display: { id: mount.displayId, title: mount.title },
|
|
202
364
|
compiled,
|
|
@@ -207,6 +369,10 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
207
369
|
emit: (event) => {
|
|
208
370
|
if (event.type === "initialized") {
|
|
209
371
|
watch.initialized();
|
|
372
|
+
if (!handshaken) {
|
|
373
|
+
handshaken = true;
|
|
374
|
+
giveAnnotations();
|
|
375
|
+
}
|
|
210
376
|
emit("initialized", undefined);
|
|
211
377
|
}
|
|
212
378
|
},
|
|
@@ -223,9 +389,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
223
389
|
run();
|
|
224
390
|
} else {
|
|
225
391
|
stop();
|
|
226
|
-
|
|
227
|
-
message: "The content kept navigating its frame away; it was stopped.",
|
|
228
|
-
});
|
|
392
|
+
contentError("The content kept navigating its frame away; it was stopped.");
|
|
229
393
|
}
|
|
230
394
|
});
|
|
231
395
|
};
|
|
@@ -242,6 +406,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
242
406
|
html: compiled.shellDocument({
|
|
243
407
|
origins: [container.ownerDocument.defaultView?.location.origin ?? ""],
|
|
244
408
|
title: mount.title,
|
|
409
|
+
...nonceOf(options.nonce),
|
|
245
410
|
...(mount.document === undefined ? {} : { document: mount.document }),
|
|
246
411
|
}),
|
|
247
412
|
});
|
|
@@ -264,7 +429,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
264
429
|
},
|
|
265
430
|
(error: unknown) => {
|
|
266
431
|
if (mine === generation && !controller.signal.aborted) {
|
|
267
|
-
|
|
432
|
+
// The host's own failure (its backend down, access gone): never the content's.
|
|
433
|
+
hostError(error instanceof Error ? error.message : String(error), "locate");
|
|
434
|
+
emit("outcome", { state: "blocked", reason: "locate" });
|
|
268
435
|
}
|
|
269
436
|
},
|
|
270
437
|
);
|
|
@@ -274,6 +441,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
274
441
|
|
|
275
442
|
const controls = {
|
|
276
443
|
displayId: mount.displayId,
|
|
444
|
+
annotations,
|
|
277
445
|
setTheme(next: ColorScheme) {
|
|
278
446
|
scheme = next;
|
|
279
447
|
surface?.setColorScheme(next);
|
|
@@ -333,3 +501,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
|
|
|
333
501
|
},
|
|
334
502
|
};
|
|
335
503
|
}
|
|
504
|
+
|
|
505
|
+
/** The host's nonce option, read now (`HostOptions.nonce`). */
|
|
506
|
+
function nonceOf(nonce: HostOptions["nonce"]): { nonce?: string } {
|
|
507
|
+
const value = typeof nonce === "function" ? nonce() : nonce;
|
|
508
|
+
return value === undefined || value === "" ? {} : { nonce: value };
|
|
509
|
+
}
|
package/src/host/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AnnotateMode, AnnotationMark, AnnotationReport } from "../annotate/types.ts";
|
|
1
2
|
import type { Confirm, CapabilityHandlers, Refusal } from "../bridge/types.ts";
|
|
2
3
|
import type { CompiledConfig } from "../compile/compile.ts";
|
|
3
4
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
@@ -33,6 +34,13 @@ export interface HostOptions<C extends AdapterConnection = ShellConnection> {
|
|
|
33
34
|
readonly platform?: PlatformHost;
|
|
34
35
|
/** Required for domain and external isolation: where an instance's frame loads from. */
|
|
35
36
|
readonly locator?: InstanceLocator;
|
|
37
|
+
/**
|
|
38
|
+
* The embedding page's CSP nonce, where its `script-src` is nonce-based (P054): an opaque frame
|
|
39
|
+
* (`srcdoc`) inherits the page's policy, so the shell and the content's scripts carry it. A
|
|
40
|
+
* function is read each time a frame starts (a page whose nonce changes). Served frames inherit
|
|
41
|
+
* nothing and need none.
|
|
42
|
+
*/
|
|
43
|
+
readonly nonce?: string | (() => string | undefined);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export interface MountOptions {
|
|
@@ -49,8 +57,23 @@ export interface MountOptions {
|
|
|
49
57
|
readonly document?: string;
|
|
50
58
|
/** The height to start at, before the content reports its own. */
|
|
51
59
|
readonly initialHeightPx?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Fill the container (give it a height) instead of taking the content's height: the content
|
|
62
|
+
* scrolls inside its frame (an artifact viewer's page). The content's sizes are still reported
|
|
63
|
+
* (`resize`). A url-mode app always fills.
|
|
64
|
+
*/
|
|
65
|
+
readonly fill?: boolean;
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
/** What failed on the host's side (`hostError`). */
|
|
69
|
+
export type HostErrorKind =
|
|
70
|
+
/** The locator rejected: the host's backend didn't give a location. */
|
|
71
|
+
| "locate"
|
|
72
|
+
/** A display couldn't be made (the React binding: a bad config, a missing handler). */
|
|
73
|
+
| "mount"
|
|
74
|
+
/** An annotation call was refused (an invalid mark, an unknown id). */
|
|
75
|
+
| "annotations";
|
|
76
|
+
|
|
54
77
|
/** What a display reports, by event name. */
|
|
55
78
|
/** Why a display's frame showed nothing of its content (`FrameOutcome`). */
|
|
56
79
|
export type FrameBlock =
|
|
@@ -64,6 +87,8 @@ export type FrameBlock =
|
|
|
64
87
|
| "upstream"
|
|
65
88
|
/** The host's own check refused it (revoked, no longer a member): `code` says which. */
|
|
66
89
|
| "denied"
|
|
90
|
+
/** The host's locator failed (its backend down, access gone): see the `hostError` event. */
|
|
91
|
+
| "locate"
|
|
67
92
|
| "not-found"
|
|
68
93
|
| "error";
|
|
69
94
|
|
|
@@ -92,14 +117,49 @@ export interface DisplayEvents {
|
|
|
92
117
|
readonly outcome: FrameOutcome;
|
|
93
118
|
/** The content's size, after the config's limits. */
|
|
94
119
|
readonly resize: { readonly height: number; readonly width?: number };
|
|
95
|
-
/**
|
|
96
|
-
|
|
120
|
+
/**
|
|
121
|
+
* What the content did wrong: what it threw, what the sandbox blocked (its own words:
|
|
122
|
+
* untrusted), or that it kept navigating its frame away. The agent's to hear and fix.
|
|
123
|
+
*/
|
|
124
|
+
readonly contentError: { readonly message: string };
|
|
125
|
+
/**
|
|
126
|
+
* What failed on the host's side (`kind`): its locator, for now. The host's to handle; never
|
|
127
|
+
* the agent's. The display's `outcome` says `blocked: locate` too.
|
|
128
|
+
*/
|
|
129
|
+
readonly hostError: { readonly message: string; readonly kind: HostErrorKind };
|
|
130
|
+
/**
|
|
131
|
+
* Both of the above, with their `source`: for a host that listened here before they were
|
|
132
|
+
* split (0.1.3). Prefer `contentError` and `hostError`.
|
|
133
|
+
*/
|
|
134
|
+
readonly error: { readonly message: string; readonly source: "content" | "host" };
|
|
97
135
|
/** The content navigated its frame away; the frame was started again from its content. */
|
|
98
136
|
readonly navigated: undefined;
|
|
99
137
|
/** A call didn't run. */
|
|
100
138
|
readonly refused: { readonly capability: CapabilityName; readonly refusal: Refusal };
|
|
101
139
|
/** Too many declines: the display stops asking until it runs again. */
|
|
102
140
|
readonly muted: undefined;
|
|
141
|
+
/**
|
|
142
|
+
* What the frame reported about annotations (`annotate`, P053), checked and taken only while
|
|
143
|
+
* the host asked for it: a selection, its clearing, a cancelled pick, where marks are. The
|
|
144
|
+
* content's word: UI hints, never permissions.
|
|
145
|
+
*/
|
|
146
|
+
readonly annotation: AnnotationReport;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Annotations on a display whose config declares `annotate` (P053). The host's state is kept and
|
|
151
|
+
* given to every frame the display runs (a reload, a navigation recovered from).
|
|
152
|
+
*/
|
|
153
|
+
export interface AnnotationControls {
|
|
154
|
+
/** Lets the person point at elements, select text, or both (default both). */
|
|
155
|
+
start(mode?: AnnotateMode): void;
|
|
156
|
+
stop(): void;
|
|
157
|
+
/** Marks these anchors (replacing the marks before); the frame reports where they are. */
|
|
158
|
+
show(marks: readonly AnnotationMark[]): void;
|
|
159
|
+
/** Brings a mark into view in the frame and makes it the active one. */
|
|
160
|
+
focus(id: string): void;
|
|
161
|
+
/** Removes every mark. */
|
|
162
|
+
clear(): void;
|
|
103
163
|
}
|
|
104
164
|
|
|
105
165
|
export interface DisplayControls {
|
|
@@ -115,6 +175,8 @@ export interface DisplayControls {
|
|
|
115
175
|
event: E,
|
|
116
176
|
listener: (detail: DisplayEvents[E]) => void,
|
|
117
177
|
): () => void;
|
|
178
|
+
/** Annotations (`annotate`): throws on use where the config doesn't declare it. */
|
|
179
|
+
readonly annotations: AnnotationControls;
|
|
118
180
|
/** Runs the content again from the start, in a new frame. */
|
|
119
181
|
reload(): void;
|
|
120
182
|
/** Tears the view down and removes the frame. */
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export {
|
|
8
8
|
CAPABILITIES,
|
|
9
9
|
CAPABILITY_NAMES,
|
|
10
|
+
MAX_COPY_CHARS,
|
|
10
11
|
capabilitySpec,
|
|
11
12
|
isCapabilityName,
|
|
12
13
|
type CapabilityArgs,
|
|
@@ -28,6 +29,7 @@ export type {
|
|
|
28
29
|
LayoutConfig,
|
|
29
30
|
Limits,
|
|
30
31
|
PortholeConfig,
|
|
32
|
+
StorageConfig,
|
|
31
33
|
ThemeConfig,
|
|
32
34
|
ThemeValue,
|
|
33
35
|
} from "./config/schema.ts";
|
|
@@ -64,6 +66,7 @@ export {
|
|
|
64
66
|
effectiveGates,
|
|
65
67
|
egressOrigins,
|
|
66
68
|
embedderCsp,
|
|
69
|
+
embedderFrameSourcesOf,
|
|
67
70
|
type CspDirective,
|
|
68
71
|
type EgressKind,
|
|
69
72
|
} from "./compile/policy.ts";
|
|
@@ -105,6 +108,7 @@ export type {
|
|
|
105
108
|
ShellConnection,
|
|
106
109
|
} from "./protocol/adapter.ts";
|
|
107
110
|
export {
|
|
111
|
+
COPY_TEXT_METHOD,
|
|
108
112
|
MCP_APPS_METHODS,
|
|
109
113
|
MCP_APPS_PROTOCOL_VERSION,
|
|
110
114
|
MCP_APPS_STYLE_VARIABLES,
|
|
@@ -124,7 +128,9 @@ export type {
|
|
|
124
128
|
Refusal,
|
|
125
129
|
} from "./bridge/types.ts";
|
|
126
130
|
export { createHost } from "./host/create-host.ts";
|
|
131
|
+
export { writeClipboard } from "./host/clipboard.ts";
|
|
127
132
|
export type {
|
|
133
|
+
AnnotationControls,
|
|
128
134
|
DisplayControls,
|
|
129
135
|
DisplayEvents,
|
|
130
136
|
FrameBlock,
|
|
@@ -134,3 +140,21 @@ export type {
|
|
|
134
140
|
MountOptions,
|
|
135
141
|
PortholeHost,
|
|
136
142
|
} from "./host/types.ts";
|
|
143
|
+
|
|
144
|
+
// Annotations (P053): anchors, what a frame reports, and the host's checks on both.
|
|
145
|
+
export {
|
|
146
|
+
ANNOTATE_METHODS,
|
|
147
|
+
ANNOTATION_LIMITS,
|
|
148
|
+
type AnchorResult,
|
|
149
|
+
type AnchorState,
|
|
150
|
+
type AnnotateMode,
|
|
151
|
+
type AnnotationAnchor,
|
|
152
|
+
type AnnotationMark,
|
|
153
|
+
type AnnotationRect,
|
|
154
|
+
type AnnotationReport,
|
|
155
|
+
type AnnotationSelector,
|
|
156
|
+
type CssSelector,
|
|
157
|
+
type TextPositionSelector,
|
|
158
|
+
type TextQuoteSelector,
|
|
159
|
+
} from "./annotate/types.ts";
|
|
160
|
+
export { parseAnchor, parseRect, parseReport } from "./annotate/validate.ts";
|
package/src/mcp/adapter.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
MCP_APPS_PROTOCOL_VERSION,
|
|
15
15
|
type HostContext,
|
|
16
16
|
} from "../protocol/mcp-apps.ts";
|
|
17
|
-
import { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.ts";
|
|
17
|
+
import { COPY_TEXT_METHOD, MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.ts";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The MCP Apps adapter (P004, P007): the host side of the protocol for full MCP Apps views, written
|
|
@@ -283,6 +283,15 @@ function connect(session: AdapterSession, options: McpAppsAdapterOptions): McpAp
|
|
|
283
283
|
() => ({}),
|
|
284
284
|
);
|
|
285
285
|
return;
|
|
286
|
+
case COPY_TEXT_METHOD:
|
|
287
|
+
// Porthole's own request (the standard has no clipboard request): the host copies, and
|
|
288
|
+
// shows the person what (P052).
|
|
289
|
+
if (typeof params.text !== "string") {
|
|
290
|
+
invalid("needs text");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
await call(request, "copyText", { text: params.text }, done);
|
|
294
|
+
return;
|
|
286
295
|
case MCP_APPS_METHODS.requestDisplayMode: {
|
|
287
296
|
if (typeof params.mode !== "string" || !DISPLAY_MODES.has(params.mode)) {
|
|
288
297
|
invalid("needs a display mode");
|
package/src/presets/presets.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
LayoutConfig,
|
|
10
10
|
Limits,
|
|
11
11
|
PortholeConfig,
|
|
12
|
+
StorageConfig,
|
|
12
13
|
ThemeValue,
|
|
13
14
|
UrlPermission,
|
|
14
15
|
UrlSandboxExtra,
|
|
@@ -45,6 +46,8 @@ export interface PresetOptions {
|
|
|
45
46
|
readonly isolation?: Exclude<IsolationKind, "external">;
|
|
46
47
|
readonly contentOrigins?: readonly string[];
|
|
47
48
|
readonly embedder?: EmbedderConfig;
|
|
49
|
+
/** Domain isolation: what displays sharing an origin share (`StorageConfig`), for the manual. */
|
|
50
|
+
readonly storage?: StorageConfig;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** The url preset's options: where the app is served, and the embedding page's extra frames. */
|
|
@@ -174,6 +177,7 @@ function build(
|
|
|
174
177
|
guide: options.guide ?? {},
|
|
175
178
|
contentOrigins: options.contentOrigins ?? [],
|
|
176
179
|
embedder: options.embedder ?? {},
|
|
180
|
+
...(options.storage === undefined ? {} : { storage: options.storage }),
|
|
177
181
|
});
|
|
178
182
|
}
|
|
179
183
|
|
package/src/protocol/mcp-apps.ts
CHANGED
|
@@ -115,6 +115,12 @@ export const MCP_APPS_STYLE_VARIABLES: readonly string[] = [
|
|
|
115
115
|
*/
|
|
116
116
|
export const PORTHOLE_STYLES = "porthole/styles";
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Porthole's own request for copying text (`copyText`, P052): `{ text }`, answered `{}` when the
|
|
120
|
+
* host copied it and `{ isError: true }` when it didn't. The standard has no clipboard request.
|
|
121
|
+
*/
|
|
122
|
+
export const COPY_TEXT_METHOD = "porthole/copy-text";
|
|
123
|
+
|
|
118
124
|
/** The logger name the shell's error reports (`notifications/message`, level `error`) carry. */
|
|
119
125
|
export const SHELL_LOGGER = "porthole";
|
|
120
126
|
|
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
type JsonRpcRequest,
|
|
9
9
|
type JsonRpcResponse,
|
|
10
10
|
} from "./jsonrpc.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
COPY_TEXT_METHOD,
|
|
13
|
+
MCP_APPS_METHODS as M,
|
|
14
|
+
MCP_APPS_PROTOCOL_VERSION,
|
|
15
|
+
type HostContext,
|
|
16
|
+
} from "./mcp-apps.ts";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* The core's adapter: the host side of the MCP Apps subset the shell speaks, which content using
|
|
@@ -42,6 +47,7 @@ const REQUESTS: Readonly<
|
|
|
42
47
|
}),
|
|
43
48
|
},
|
|
44
49
|
[M.openLink]: { capability: "openLink", args: (params) => ({ url: params.url }) },
|
|
50
|
+
[COPY_TEXT_METHOD]: { capability: "copyText", args: (params) => ({ text: params.text }) },
|
|
45
51
|
[M.downloadFile]: {
|
|
46
52
|
capability: "downloadFile",
|
|
47
53
|
args: (params) => ({ contents: params.contents }),
|
|
@@ -63,7 +69,10 @@ function answer(
|
|
|
63
69
|
outcome: CallOutcome<unknown>,
|
|
64
70
|
): { result: unknown } | { error: { code: number; message: string } } {
|
|
65
71
|
const flagged =
|
|
66
|
-
capability === "sendPrompt" ||
|
|
72
|
+
capability === "sendPrompt" ||
|
|
73
|
+
capability === "openLink" ||
|
|
74
|
+
capability === "downloadFile" ||
|
|
75
|
+
capability === "copyText";
|
|
67
76
|
if (!outcome.ok) {
|
|
68
77
|
return flagged
|
|
69
78
|
? { result: { isError: true } }
|
package/src/react/dialog.tsx
CHANGED
|
@@ -66,6 +66,8 @@ function words(request: ConfirmRequest): Words {
|
|
|
66
66
|
return { heading: "Share this with the agent?", accept: "Share", detail: json(request.args) };
|
|
67
67
|
case "readData":
|
|
68
68
|
return { heading: "Let it read this?", accept: "Allow", detail: request.args.uri };
|
|
69
|
+
case "copyText":
|
|
70
|
+
return { heading: "Copy this text?", accept: "Copy", detail: request.args.text };
|
|
69
71
|
default:
|
|
70
72
|
return { heading: "Allow this?", accept: "Allow", detail: json(request.args) };
|
|
71
73
|
}
|