@adecore/agent-contracts 0.18.4 → 0.19.0
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/chat.d.ts +12 -0
- package/dist/protocol.d.ts +12 -0
- package/dist/visual-viewport.d.ts +5 -0
- package/dist/visual-viewport.js +128 -0
- package/dist/visual.d.ts +45 -0
- package/dist/visual.js +41 -3
- package/package.json +1 -1
- package/src/visual-viewport.ts +128 -0
- package/src/visual.ts +56 -4
package/dist/chat.d.ts
CHANGED
|
@@ -3383,6 +3383,10 @@ export declare const ChatVisualsResultSchema: z.ZodObject<{
|
|
|
3383
3383
|
title: z.ZodString;
|
|
3384
3384
|
at: z.ZodNumber;
|
|
3385
3385
|
maxHeight: z.ZodNumber;
|
|
3386
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
3387
|
+
inline: "inline";
|
|
3388
|
+
wide: "wide";
|
|
3389
|
+
}>>;
|
|
3386
3390
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
3387
3391
|
size: z.ZodNumber;
|
|
3388
3392
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -3396,6 +3400,10 @@ export declare const ChatVisualsEventSchema: z.ZodObject<{
|
|
|
3396
3400
|
title: z.ZodString;
|
|
3397
3401
|
at: z.ZodNumber;
|
|
3398
3402
|
maxHeight: z.ZodNumber;
|
|
3403
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
3404
|
+
inline: "inline";
|
|
3405
|
+
wide: "wide";
|
|
3406
|
+
}>>;
|
|
3399
3407
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
3400
3408
|
size: z.ZodNumber;
|
|
3401
3409
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -4815,6 +4823,10 @@ export declare const ChatAttachResultSchema: z.ZodObject<{
|
|
|
4815
4823
|
title: z.ZodString;
|
|
4816
4824
|
at: z.ZodNumber;
|
|
4817
4825
|
maxHeight: z.ZodNumber;
|
|
4826
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
4827
|
+
inline: "inline";
|
|
4828
|
+
wide: "wide";
|
|
4829
|
+
}>>;
|
|
4818
4830
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
4819
4831
|
size: z.ZodNumber;
|
|
4820
4832
|
turnId: z.ZodOptional<z.ZodString>;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1838,6 +1838,10 @@ export declare const AGENT_REQUEST_SCHEMAS: {
|
|
|
1838
1838
|
title: z.ZodString;
|
|
1839
1839
|
at: z.ZodNumber;
|
|
1840
1840
|
maxHeight: z.ZodNumber;
|
|
1841
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
1842
|
+
inline: "inline";
|
|
1843
|
+
wide: "wide";
|
|
1844
|
+
}>>;
|
|
1841
1845
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
1842
1846
|
size: z.ZodNumber;
|
|
1843
1847
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -2881,6 +2885,10 @@ export declare const AGENT_REQUEST_SCHEMAS: {
|
|
|
2881
2885
|
title: z.ZodString;
|
|
2882
2886
|
at: z.ZodNumber;
|
|
2883
2887
|
maxHeight: z.ZodNumber;
|
|
2888
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
2889
|
+
inline: "inline";
|
|
2890
|
+
wide: "wide";
|
|
2891
|
+
}>>;
|
|
2884
2892
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
2885
2893
|
size: z.ZodNumber;
|
|
2886
2894
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -4514,6 +4522,10 @@ export declare const AGENT_EVENT_SCHEMAS: {
|
|
|
4514
4522
|
title: z.ZodString;
|
|
4515
4523
|
at: z.ZodNumber;
|
|
4516
4524
|
maxHeight: z.ZodNumber;
|
|
4525
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
4526
|
+
inline: "inline";
|
|
4527
|
+
wide: "wide";
|
|
4528
|
+
}>>;
|
|
4517
4529
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
4518
4530
|
size: z.ZodNumber;
|
|
4519
4531
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Installed by the host on every load, including pages saved with an older bootstrap.
|
|
2
|
+
export function visualViewportScript(methods) {
|
|
3
|
+
return `(function () {
|
|
4
|
+
var methods = ${JSON.stringify(methods)};
|
|
5
|
+
var viewport = null;
|
|
6
|
+
var expected = 0;
|
|
7
|
+
var touch = null;
|
|
8
|
+
var momentum = 0;
|
|
9
|
+
var style = document.createElement('style');
|
|
10
|
+
document.head.appendChild(style);
|
|
11
|
+
function request(value) {
|
|
12
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: methods.scrollRequest, params: value }, '*');
|
|
13
|
+
}
|
|
14
|
+
function stopMomentum() {
|
|
15
|
+
if (momentum) { cancelAnimationFrame(momentum); momentum = 0; }
|
|
16
|
+
}
|
|
17
|
+
function apply(top) {
|
|
18
|
+
window.scrollTo({ top: top, left: window.scrollX, behavior: 'instant' });
|
|
19
|
+
expected = window.scrollY;
|
|
20
|
+
}
|
|
21
|
+
window.addEventListener('message', function (event) {
|
|
22
|
+
var data = event.data;
|
|
23
|
+
if (event.source !== window.parent || !data || data.jsonrpc !== '2.0' || data.method !== methods.viewportChanged) { return; }
|
|
24
|
+
var next = data.params;
|
|
25
|
+
if (next === null) {
|
|
26
|
+
viewport = null;
|
|
27
|
+
style.textContent = '';
|
|
28
|
+
stopMomentum();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!next || !Number.isFinite(next.top) || next.top < 0 || !Number.isFinite(next.height) || next.height <= 0) { return; }
|
|
32
|
+
if (viewport === null) {
|
|
33
|
+
style.textContent = 'html{overflow:hidden!important;scroll-behavior:auto!important;overflow-anchor:none!important}';
|
|
34
|
+
}
|
|
35
|
+
viewport = next;
|
|
36
|
+
apply(next.top);
|
|
37
|
+
});
|
|
38
|
+
function nestedScroll(path, delta, axis) {
|
|
39
|
+
for (var i = 0; i < path.length; i++) {
|
|
40
|
+
var node = path[i];
|
|
41
|
+
if (node === document.body || node === document.documentElement) { break; }
|
|
42
|
+
if (!node || node.nodeType !== 1) { continue; }
|
|
43
|
+
var css = getComputedStyle(node);
|
|
44
|
+
var horizontal = axis === 'x';
|
|
45
|
+
var overflow = horizontal ? css.overflowX : css.overflowY;
|
|
46
|
+
if (!/^(auto|scroll|overlay)$/.test(overflow)) { continue; }
|
|
47
|
+
var end = horizontal ? node.scrollWidth - node.clientWidth : node.scrollHeight - node.clientHeight;
|
|
48
|
+
if (end <= 1) { continue; }
|
|
49
|
+
var position = horizontal ? node.scrollLeft : node.scrollTop;
|
|
50
|
+
var overscroll = horizontal ? css.overscrollBehaviorX : css.overscrollBehaviorY;
|
|
51
|
+
if (overscroll === 'contain' || overscroll === 'none' || (delta < 0 ? position > 0 : position < end - 1)) { return true; }
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
window.addEventListener('wheel', function (event) {
|
|
56
|
+
if (!viewport || !event.isTrusted || event.defaultPrevented || event.ctrlKey || event.metaKey || event.shiftKey) { return; }
|
|
57
|
+
if (!event.deltaY || Math.abs(event.deltaX) > Math.abs(event.deltaY)) { return; }
|
|
58
|
+
if (nestedScroll(event.composedPath(), event.deltaY, 'y') || !event.cancelable) { return; }
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
stopMomentum();
|
|
61
|
+
var unit = event.deltaMode === 2 ? viewport.height : event.deltaMode === 1 ? 16 : 1;
|
|
62
|
+
request({ by: event.deltaY * unit });
|
|
63
|
+
}, { passive: false });
|
|
64
|
+
window.addEventListener('keydown', function (event) {
|
|
65
|
+
if (!viewport || !event.isTrusted || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) { return; }
|
|
66
|
+
var target = event.target;
|
|
67
|
+
if (target && (target.isContentEditable || (typeof target.closest === 'function' && target.closest('input,textarea,select,[role="slider"],[role="spinbutton"],[role="listbox"],[role="combobox"],[role="tree"],[role="grid"],[role="menu"]')))) { return; }
|
|
68
|
+
if (event.key === ' ' && target && typeof target.closest === 'function' && target.closest('button,[role="button"]')) { return; }
|
|
69
|
+
var page = Math.max(40, viewport.height * 0.9);
|
|
70
|
+
var delta = event.key === 'ArrowDown' ? 40 : event.key === 'ArrowUp' ? -40 : event.key === 'PageDown' ? page : event.key === 'PageUp' ? -page : event.key === ' ' ? (event.shiftKey ? -page : page) : 0;
|
|
71
|
+
var edge = event.key === 'Home' ? 'start' : event.key === 'End' ? 'end' : null;
|
|
72
|
+
if ((!delta && !edge) || nestedScroll(event.composedPath(), delta || (edge === 'start' ? -1 : 1), 'y')) { return; }
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
stopMomentum();
|
|
75
|
+
request(edge ? { edge: edge } : { by: delta });
|
|
76
|
+
});
|
|
77
|
+
window.addEventListener('touchstart', function (event) {
|
|
78
|
+
stopMomentum();
|
|
79
|
+
touch = null;
|
|
80
|
+
if (!viewport || !event.isTrusted || event.touches.length !== 1) { return; }
|
|
81
|
+
var point = event.touches[0];
|
|
82
|
+
touch = { x: point.clientX, y: point.clientY, time: performance.now(), velocity: 0, own: null, path: event.composedPath() };
|
|
83
|
+
}, { passive: true });
|
|
84
|
+
window.addEventListener('touchmove', function (event) {
|
|
85
|
+
if (!viewport || !touch || event.defaultPrevented || event.touches.length !== 1) { touch = null; return; }
|
|
86
|
+
var point = event.touches[0];
|
|
87
|
+
var delta = touch.y - point.clientY;
|
|
88
|
+
var horizontal = touch.x - point.clientX;
|
|
89
|
+
if (touch.own === null) {
|
|
90
|
+
if (Math.max(Math.abs(delta), Math.abs(horizontal)) < 4) { return; }
|
|
91
|
+
touch.own = Math.abs(delta) >= Math.abs(horizontal) && !nestedScroll(touch.path, delta, 'y');
|
|
92
|
+
}
|
|
93
|
+
if (!touch.own || !event.cancelable) { return; }
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
var now = performance.now();
|
|
96
|
+
touch.velocity = Math.max(-4, Math.min(4, delta / Math.max(1, now - touch.time)));
|
|
97
|
+
touch.x = point.clientX;
|
|
98
|
+
touch.y = point.clientY;
|
|
99
|
+
touch.time = now;
|
|
100
|
+
request({ by: delta });
|
|
101
|
+
}, { passive: false });
|
|
102
|
+
window.addEventListener('touchend', function () {
|
|
103
|
+
if (!touch) { return; }
|
|
104
|
+
var velocity = touch.own && performance.now() - touch.time < 80 ? touch.velocity : 0;
|
|
105
|
+
touch = null;
|
|
106
|
+
var last = performance.now();
|
|
107
|
+
var tick = function (now) {
|
|
108
|
+
momentum = 0;
|
|
109
|
+
if (!viewport || Math.abs(velocity) < 0.02) { return; }
|
|
110
|
+
var elapsed = Math.min(32, now - last);
|
|
111
|
+
last = now;
|
|
112
|
+
var decay = Math.exp(-elapsed / 180);
|
|
113
|
+
request({ by: velocity * 180 * (1 - decay) });
|
|
114
|
+
velocity *= decay;
|
|
115
|
+
momentum = requestAnimationFrame(tick);
|
|
116
|
+
};
|
|
117
|
+
if (Math.abs(velocity) >= 0.02) { momentum = requestAnimationFrame(tick); }
|
|
118
|
+
}, { passive: true });
|
|
119
|
+
window.addEventListener('touchcancel', function () { touch = null; stopMomentum(); }, { passive: true });
|
|
120
|
+
window.addEventListener('scroll', function () {
|
|
121
|
+
if (!viewport || Math.abs(window.scrollY - expected) < 1) { return; }
|
|
122
|
+
// Focus, fragment links and scrollIntoView can scroll an overflow-hidden document.
|
|
123
|
+
expected = window.scrollY;
|
|
124
|
+
request({ to: expected });
|
|
125
|
+
}, { passive: true });
|
|
126
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: methods.viewportReady }, '*');
|
|
127
|
+
})();`;
|
|
128
|
+
}
|
package/dist/visual.d.ts
CHANGED
|
@@ -9,11 +9,20 @@ export declare const VISUAL_LIMITS: {
|
|
|
9
9
|
export declare const VISUAL_MEASURE_WIDTHS: readonly number[];
|
|
10
10
|
export declare const VisualHeightSchema: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
|
|
11
11
|
export type VisualHeight = z.infer<typeof VisualHeightSchema>;
|
|
12
|
+
export declare const VisualLayoutSchema: z.ZodEnum<{
|
|
13
|
+
inline: "inline";
|
|
14
|
+
wide: "wide";
|
|
15
|
+
}>;
|
|
16
|
+
export type VisualLayout = z.infer<typeof VisualLayoutSchema>;
|
|
12
17
|
export declare const ChatVisualSchema: z.ZodObject<{
|
|
13
18
|
id: z.ZodString;
|
|
14
19
|
title: z.ZodString;
|
|
15
20
|
at: z.ZodNumber;
|
|
16
21
|
maxHeight: z.ZodNumber;
|
|
22
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
inline: "inline";
|
|
24
|
+
wide: "wide";
|
|
25
|
+
}>>;
|
|
17
26
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
18
27
|
size: z.ZodNumber;
|
|
19
28
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -24,6 +33,10 @@ export declare const ChatVisualsSchema: z.ZodArray<z.ZodObject<{
|
|
|
24
33
|
title: z.ZodString;
|
|
25
34
|
at: z.ZodNumber;
|
|
26
35
|
maxHeight: z.ZodNumber;
|
|
36
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
37
|
+
inline: "inline";
|
|
38
|
+
wide: "wide";
|
|
39
|
+
}>>;
|
|
27
40
|
heights: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
|
|
28
41
|
size: z.ZodNumber;
|
|
29
42
|
turnId: z.ZodOptional<z.ZodString>;
|
|
@@ -35,6 +48,9 @@ export declare const VISUAL_BRIDGE_METHODS: {
|
|
|
35
48
|
readonly hostContextChanged: 'ui/notifications/host-context-changed';
|
|
36
49
|
readonly sizeChanged: 'ui/notifications/size-changed';
|
|
37
50
|
readonly openLink: 'ui/open-link';
|
|
51
|
+
readonly viewportReady: 'adecore/visual/viewport-ready';
|
|
52
|
+
readonly viewportChanged: 'adecore/visual/viewport-changed';
|
|
53
|
+
readonly scrollRequest: 'adecore/visual/scroll-request';
|
|
38
54
|
};
|
|
39
55
|
export type VisualAppearance = 'light' | 'dark';
|
|
40
56
|
export interface VisualTheme {
|
|
@@ -42,6 +58,27 @@ export interface VisualTheme {
|
|
|
42
58
|
variables: Readonly<Record<string, string>>;
|
|
43
59
|
}
|
|
44
60
|
export type VisualRequestId = string | number;
|
|
61
|
+
export interface VisualViewport {
|
|
62
|
+
top: number;
|
|
63
|
+
height: number;
|
|
64
|
+
}
|
|
65
|
+
export type VisualScrollRequest = {
|
|
66
|
+
by: number;
|
|
67
|
+
} | {
|
|
68
|
+
to: number;
|
|
69
|
+
} | {
|
|
70
|
+
edge: 'start' | 'end';
|
|
71
|
+
};
|
|
72
|
+
export declare function visualViewportMessage(viewport: VisualViewport | null): {
|
|
73
|
+
readonly jsonrpc: '2.0';
|
|
74
|
+
readonly method: "adecore/visual/viewport-changed";
|
|
75
|
+
readonly params: VisualViewport | null;
|
|
76
|
+
};
|
|
77
|
+
export declare function visualScrollMessage(request: VisualScrollRequest): {
|
|
78
|
+
readonly jsonrpc: '2.0';
|
|
79
|
+
readonly method: "adecore/visual/scroll-request";
|
|
80
|
+
readonly params: VisualScrollRequest;
|
|
81
|
+
};
|
|
45
82
|
export type VisualMessage = {
|
|
46
83
|
method: typeof VISUAL_BRIDGE_METHODS.sandboxProxyReady;
|
|
47
84
|
} | {
|
|
@@ -58,6 +95,14 @@ export type VisualMessage = {
|
|
|
58
95
|
method: typeof VISUAL_BRIDGE_METHODS.openLink;
|
|
59
96
|
id: VisualRequestId;
|
|
60
97
|
url: string;
|
|
98
|
+
} | {
|
|
99
|
+
method: typeof VISUAL_BRIDGE_METHODS.viewportReady;
|
|
100
|
+
} | {
|
|
101
|
+
method: typeof VISUAL_BRIDGE_METHODS.viewportChanged;
|
|
102
|
+
viewport: VisualViewport | null;
|
|
103
|
+
} | {
|
|
104
|
+
method: typeof VISUAL_BRIDGE_METHODS.scrollRequest;
|
|
105
|
+
request: VisualScrollRequest;
|
|
61
106
|
};
|
|
62
107
|
export declare function visualProxyReadyMessage(): {
|
|
63
108
|
readonly jsonrpc: '2.0';
|
package/dist/visual.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { visualViewportScript } from './visual-viewport.js';
|
|
2
3
|
/*
|
|
3
4
|
* What a host accepts when an agent publishes a visual. The schemas below check shapes only, so a
|
|
4
5
|
* host that widens a limit never makes an older client refuse a whole attach.
|
|
@@ -17,6 +18,7 @@ export const VISUAL_LIMITS = {
|
|
|
17
18
|
export const VISUAL_MEASURE_WIDTHS = [320, 400, 480, 560, 640, 720, 800, 900, 1000, 1200];
|
|
18
19
|
// `[width, height]` of a page measured in a frame that wide, in CSS pixels.
|
|
19
20
|
export const VisualHeightSchema = z.tuple([z.number().positive(), z.number().nonnegative()]);
|
|
21
|
+
export const VisualLayoutSchema = z.enum(['inline', 'wide']);
|
|
20
22
|
/*
|
|
21
23
|
* A page an agent published in a chat, shown in the thread at `at`. It lives beside the chat in the
|
|
22
24
|
* host's data folder and goes with the chat, a clear of it, or a person who removes it.
|
|
@@ -28,6 +30,8 @@ export const ChatVisualSchema = z.object({
|
|
|
28
30
|
// On the clock of an item's `createdAt`, which is what places the visual among the items.
|
|
29
31
|
at: z.number(),
|
|
30
32
|
maxHeight: z.number().positive(),
|
|
33
|
+
// Absent on older visuals, which keep the reply column's width.
|
|
34
|
+
layout: VisualLayoutSchema.optional(),
|
|
31
35
|
// Ascending by width; absent while nobody measured the page.
|
|
32
36
|
heights: z.array(VisualHeightSchema).optional(),
|
|
33
37
|
// Bytes of the stored page.
|
|
@@ -74,8 +78,18 @@ export const VISUAL_BRIDGE_METHODS = {
|
|
|
74
78
|
// From the page to the app: the size of its document.
|
|
75
79
|
sizeChanged: 'ui/notifications/size-changed',
|
|
76
80
|
// From the page to the app, as a request: a link to open outside the frame.
|
|
77
|
-
openLink: 'ui/open-link'
|
|
81
|
+
openLink: 'ui/open-link',
|
|
82
|
+
// Host extensions, independent of the bootstrap stored with a page.
|
|
83
|
+
viewportReady: 'adecore/visual/viewport-ready',
|
|
84
|
+
viewportChanged: 'adecore/visual/viewport-changed',
|
|
85
|
+
scrollRequest: 'adecore/visual/scroll-request'
|
|
78
86
|
};
|
|
87
|
+
export function visualViewportMessage(viewport) {
|
|
88
|
+
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.viewportChanged, params: viewport };
|
|
89
|
+
}
|
|
90
|
+
export function visualScrollMessage(request) {
|
|
91
|
+
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.scrollRequest, params: request };
|
|
92
|
+
}
|
|
79
93
|
export function visualProxyReadyMessage() {
|
|
80
94
|
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.sandboxProxyReady };
|
|
81
95
|
}
|
|
@@ -146,6 +160,26 @@ export function parseVisualMessage(data) {
|
|
|
146
160
|
}
|
|
147
161
|
return width === undefined ? { method: data.method, height } : { method: data.method, width, height };
|
|
148
162
|
}
|
|
163
|
+
case VISUAL_BRIDGE_METHODS.viewportReady:
|
|
164
|
+
return { method: data.method };
|
|
165
|
+
case VISUAL_BRIDGE_METHODS.viewportChanged: {
|
|
166
|
+
if (data.params === null) {
|
|
167
|
+
return { method: data.method, viewport: null };
|
|
168
|
+
}
|
|
169
|
+
const top = sizeOf(params.top);
|
|
170
|
+
const height = sizeOf(params.height);
|
|
171
|
+
return top !== undefined && height !== undefined && height > 0 ? { method: data.method, viewport: { top, height } } : undefined;
|
|
172
|
+
}
|
|
173
|
+
case VISUAL_BRIDGE_METHODS.scrollRequest: {
|
|
174
|
+
if (typeof params.by === 'number' && Number.isFinite(params.by)) {
|
|
175
|
+
return { method: data.method, request: { by: Math.max(-100_000, Math.min(params.by, 100_000)) } };
|
|
176
|
+
}
|
|
177
|
+
const top = sizeOf(params.to);
|
|
178
|
+
if (top !== undefined) {
|
|
179
|
+
return { method: data.method, request: { to: top } };
|
|
180
|
+
}
|
|
181
|
+
return params.edge === 'start' || params.edge === 'end' ? { method: data.method, request: { edge: params.edge } } : undefined;
|
|
182
|
+
}
|
|
149
183
|
case VISUAL_BRIDGE_METHODS.openLink: {
|
|
150
184
|
const url = webUrl(params.url);
|
|
151
185
|
const id = data.id;
|
|
@@ -302,8 +336,9 @@ export function visualThemeFragment(theme) {
|
|
|
302
336
|
export const VISUAL_PAGE_RULES = `A visual is one self-contained HTML document of at most ${VISUAL_LIMITS.bytes / 1024 / 1024} MiB: styles in an inline <style>, code in an inline <script>, ` +
|
|
303
337
|
'and the data it shows inlined. Public http(s) URLs load as they are, so a chart library from a CDN or an image on the web works; ' +
|
|
304
338
|
'relative paths and local files do not. Prefer inline SVG or a canvas when the drawing needs no library. A link to an http(s) page opens in the browser.';
|
|
305
|
-
export const VISUAL_LAYOUT_GUIDE = "The page appears
|
|
306
|
-
'
|
|
339
|
+
export const VISUAL_LAYOUT_GUIDE = "The page appears as a borderless frame on the thread's own background. By default it follows the reply column (up to 768px in a chat view). " +
|
|
340
|
+
'Opt into the wide layout for application mockups or dashboards that need more space: the host uses the available chat pane width with side margins. ' +
|
|
341
|
+
'Both layouts shrink on narrow screens. Lay the page out at a fluid width (percentages, flex, grid) ' +
|
|
307
342
|
'with no horizontal padding on the outermost element. Draw no outer card, border, shadow or banner title: the page is part of the reply. ' +
|
|
308
343
|
'The frame takes the height of the page, so give a chart a fixed height in pixels, never one relative to the viewport.';
|
|
309
344
|
export const VISUAL_THEME_GUIDE = `The page is themed through CSS custom properties on :root that follow the app's light and dark appearance live: ${VISUAL_THEME_VARIABLES.join(', ')}. ` +
|
|
@@ -603,6 +638,9 @@ window.addEventListener('message', function (event) {
|
|
|
603
638
|
document.open();
|
|
604
639
|
document.write(params.html);
|
|
605
640
|
document.close();
|
|
641
|
+
var viewport = document.createElement('script');
|
|
642
|
+
viewport.textContent = ${inlineJson(visualViewportScript(VISUAL_BRIDGE_METHODS))};
|
|
643
|
+
document.head.appendChild(viewport);
|
|
606
644
|
});
|
|
607
645
|
window.parent.postMessage({ jsonrpc: '2.0', method: methods.ready }, '*');
|
|
608
646
|
})();`;
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Installed by the host on every load, including pages saved with an older bootstrap.
|
|
2
|
+
export function visualViewportScript(methods: { viewportReady: string; viewportChanged: string; scrollRequest: string }): string {
|
|
3
|
+
return `(function () {
|
|
4
|
+
var methods = ${JSON.stringify(methods)};
|
|
5
|
+
var viewport = null;
|
|
6
|
+
var expected = 0;
|
|
7
|
+
var touch = null;
|
|
8
|
+
var momentum = 0;
|
|
9
|
+
var style = document.createElement('style');
|
|
10
|
+
document.head.appendChild(style);
|
|
11
|
+
function request(value) {
|
|
12
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: methods.scrollRequest, params: value }, '*');
|
|
13
|
+
}
|
|
14
|
+
function stopMomentum() {
|
|
15
|
+
if (momentum) { cancelAnimationFrame(momentum); momentum = 0; }
|
|
16
|
+
}
|
|
17
|
+
function apply(top) {
|
|
18
|
+
window.scrollTo({ top: top, left: window.scrollX, behavior: 'instant' });
|
|
19
|
+
expected = window.scrollY;
|
|
20
|
+
}
|
|
21
|
+
window.addEventListener('message', function (event) {
|
|
22
|
+
var data = event.data;
|
|
23
|
+
if (event.source !== window.parent || !data || data.jsonrpc !== '2.0' || data.method !== methods.viewportChanged) { return; }
|
|
24
|
+
var next = data.params;
|
|
25
|
+
if (next === null) {
|
|
26
|
+
viewport = null;
|
|
27
|
+
style.textContent = '';
|
|
28
|
+
stopMomentum();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!next || !Number.isFinite(next.top) || next.top < 0 || !Number.isFinite(next.height) || next.height <= 0) { return; }
|
|
32
|
+
if (viewport === null) {
|
|
33
|
+
style.textContent = 'html{overflow:hidden!important;scroll-behavior:auto!important;overflow-anchor:none!important}';
|
|
34
|
+
}
|
|
35
|
+
viewport = next;
|
|
36
|
+
apply(next.top);
|
|
37
|
+
});
|
|
38
|
+
function nestedScroll(path, delta, axis) {
|
|
39
|
+
for (var i = 0; i < path.length; i++) {
|
|
40
|
+
var node = path[i];
|
|
41
|
+
if (node === document.body || node === document.documentElement) { break; }
|
|
42
|
+
if (!node || node.nodeType !== 1) { continue; }
|
|
43
|
+
var css = getComputedStyle(node);
|
|
44
|
+
var horizontal = axis === 'x';
|
|
45
|
+
var overflow = horizontal ? css.overflowX : css.overflowY;
|
|
46
|
+
if (!/^(auto|scroll|overlay)$/.test(overflow)) { continue; }
|
|
47
|
+
var end = horizontal ? node.scrollWidth - node.clientWidth : node.scrollHeight - node.clientHeight;
|
|
48
|
+
if (end <= 1) { continue; }
|
|
49
|
+
var position = horizontal ? node.scrollLeft : node.scrollTop;
|
|
50
|
+
var overscroll = horizontal ? css.overscrollBehaviorX : css.overscrollBehaviorY;
|
|
51
|
+
if (overscroll === 'contain' || overscroll === 'none' || (delta < 0 ? position > 0 : position < end - 1)) { return true; }
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
window.addEventListener('wheel', function (event) {
|
|
56
|
+
if (!viewport || !event.isTrusted || event.defaultPrevented || event.ctrlKey || event.metaKey || event.shiftKey) { return; }
|
|
57
|
+
if (!event.deltaY || Math.abs(event.deltaX) > Math.abs(event.deltaY)) { return; }
|
|
58
|
+
if (nestedScroll(event.composedPath(), event.deltaY, 'y') || !event.cancelable) { return; }
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
stopMomentum();
|
|
61
|
+
var unit = event.deltaMode === 2 ? viewport.height : event.deltaMode === 1 ? 16 : 1;
|
|
62
|
+
request({ by: event.deltaY * unit });
|
|
63
|
+
}, { passive: false });
|
|
64
|
+
window.addEventListener('keydown', function (event) {
|
|
65
|
+
if (!viewport || !event.isTrusted || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) { return; }
|
|
66
|
+
var target = event.target;
|
|
67
|
+
if (target && (target.isContentEditable || (typeof target.closest === 'function' && target.closest('input,textarea,select,[role="slider"],[role="spinbutton"],[role="listbox"],[role="combobox"],[role="tree"],[role="grid"],[role="menu"]')))) { return; }
|
|
68
|
+
if (event.key === ' ' && target && typeof target.closest === 'function' && target.closest('button,[role="button"]')) { return; }
|
|
69
|
+
var page = Math.max(40, viewport.height * 0.9);
|
|
70
|
+
var delta = event.key === 'ArrowDown' ? 40 : event.key === 'ArrowUp' ? -40 : event.key === 'PageDown' ? page : event.key === 'PageUp' ? -page : event.key === ' ' ? (event.shiftKey ? -page : page) : 0;
|
|
71
|
+
var edge = event.key === 'Home' ? 'start' : event.key === 'End' ? 'end' : null;
|
|
72
|
+
if ((!delta && !edge) || nestedScroll(event.composedPath(), delta || (edge === 'start' ? -1 : 1), 'y')) { return; }
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
stopMomentum();
|
|
75
|
+
request(edge ? { edge: edge } : { by: delta });
|
|
76
|
+
});
|
|
77
|
+
window.addEventListener('touchstart', function (event) {
|
|
78
|
+
stopMomentum();
|
|
79
|
+
touch = null;
|
|
80
|
+
if (!viewport || !event.isTrusted || event.touches.length !== 1) { return; }
|
|
81
|
+
var point = event.touches[0];
|
|
82
|
+
touch = { x: point.clientX, y: point.clientY, time: performance.now(), velocity: 0, own: null, path: event.composedPath() };
|
|
83
|
+
}, { passive: true });
|
|
84
|
+
window.addEventListener('touchmove', function (event) {
|
|
85
|
+
if (!viewport || !touch || event.defaultPrevented || event.touches.length !== 1) { touch = null; return; }
|
|
86
|
+
var point = event.touches[0];
|
|
87
|
+
var delta = touch.y - point.clientY;
|
|
88
|
+
var horizontal = touch.x - point.clientX;
|
|
89
|
+
if (touch.own === null) {
|
|
90
|
+
if (Math.max(Math.abs(delta), Math.abs(horizontal)) < 4) { return; }
|
|
91
|
+
touch.own = Math.abs(delta) >= Math.abs(horizontal) && !nestedScroll(touch.path, delta, 'y');
|
|
92
|
+
}
|
|
93
|
+
if (!touch.own || !event.cancelable) { return; }
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
var now = performance.now();
|
|
96
|
+
touch.velocity = Math.max(-4, Math.min(4, delta / Math.max(1, now - touch.time)));
|
|
97
|
+
touch.x = point.clientX;
|
|
98
|
+
touch.y = point.clientY;
|
|
99
|
+
touch.time = now;
|
|
100
|
+
request({ by: delta });
|
|
101
|
+
}, { passive: false });
|
|
102
|
+
window.addEventListener('touchend', function () {
|
|
103
|
+
if (!touch) { return; }
|
|
104
|
+
var velocity = touch.own && performance.now() - touch.time < 80 ? touch.velocity : 0;
|
|
105
|
+
touch = null;
|
|
106
|
+
var last = performance.now();
|
|
107
|
+
var tick = function (now) {
|
|
108
|
+
momentum = 0;
|
|
109
|
+
if (!viewport || Math.abs(velocity) < 0.02) { return; }
|
|
110
|
+
var elapsed = Math.min(32, now - last);
|
|
111
|
+
last = now;
|
|
112
|
+
var decay = Math.exp(-elapsed / 180);
|
|
113
|
+
request({ by: velocity * 180 * (1 - decay) });
|
|
114
|
+
velocity *= decay;
|
|
115
|
+
momentum = requestAnimationFrame(tick);
|
|
116
|
+
};
|
|
117
|
+
if (Math.abs(velocity) >= 0.02) { momentum = requestAnimationFrame(tick); }
|
|
118
|
+
}, { passive: true });
|
|
119
|
+
window.addEventListener('touchcancel', function () { touch = null; stopMomentum(); }, { passive: true });
|
|
120
|
+
window.addEventListener('scroll', function () {
|
|
121
|
+
if (!viewport || Math.abs(window.scrollY - expected) < 1) { return; }
|
|
122
|
+
// Focus, fragment links and scrollIntoView can scroll an overflow-hidden document.
|
|
123
|
+
expected = window.scrollY;
|
|
124
|
+
request({ to: expected });
|
|
125
|
+
}, { passive: true });
|
|
126
|
+
window.parent.postMessage({ jsonrpc: '2.0', method: methods.viewportReady }, '*');
|
|
127
|
+
})();`;
|
|
128
|
+
}
|
package/src/visual.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { visualViewportScript } from './visual-viewport.ts';
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* What a host accepts when an agent publishes a visual. The schemas below check shapes only, so a
|
|
@@ -22,6 +23,9 @@ export const VISUAL_MEASURE_WIDTHS: readonly number[] = [320, 400, 480, 560, 640
|
|
|
22
23
|
export const VisualHeightSchema = z.tuple([z.number().positive(), z.number().nonnegative()]);
|
|
23
24
|
export type VisualHeight = z.infer<typeof VisualHeightSchema>;
|
|
24
25
|
|
|
26
|
+
export const VisualLayoutSchema = z.enum(['inline', 'wide']);
|
|
27
|
+
export type VisualLayout = z.infer<typeof VisualLayoutSchema>;
|
|
28
|
+
|
|
25
29
|
/*
|
|
26
30
|
* A page an agent published in a chat, shown in the thread at `at`. It lives beside the chat in the
|
|
27
31
|
* host's data folder and goes with the chat, a clear of it, or a person who removes it.
|
|
@@ -33,6 +37,8 @@ export const ChatVisualSchema = z.object({
|
|
|
33
37
|
// On the clock of an item's `createdAt`, which is what places the visual among the items.
|
|
34
38
|
at: z.number(),
|
|
35
39
|
maxHeight: z.number().positive(),
|
|
40
|
+
// Absent on older visuals, which keep the reply column's width.
|
|
41
|
+
layout: VisualLayoutSchema.optional(),
|
|
36
42
|
// Ascending by width; absent while nobody measured the page.
|
|
37
43
|
heights: z.array(VisualHeightSchema).optional(),
|
|
38
44
|
// Bytes of the stored page.
|
|
@@ -84,7 +90,11 @@ export const VISUAL_BRIDGE_METHODS = {
|
|
|
84
90
|
// From the page to the app: the size of its document.
|
|
85
91
|
sizeChanged: 'ui/notifications/size-changed',
|
|
86
92
|
// From the page to the app, as a request: a link to open outside the frame.
|
|
87
|
-
openLink: 'ui/open-link'
|
|
93
|
+
openLink: 'ui/open-link',
|
|
94
|
+
// Host extensions, independent of the bootstrap stored with a page.
|
|
95
|
+
viewportReady: 'adecore/visual/viewport-ready',
|
|
96
|
+
viewportChanged: 'adecore/visual/viewport-changed',
|
|
97
|
+
scrollRequest: 'adecore/visual/scroll-request'
|
|
88
98
|
} as const;
|
|
89
99
|
|
|
90
100
|
export type VisualAppearance = 'light' | 'dark';
|
|
@@ -97,13 +107,31 @@ export interface VisualTheme {
|
|
|
97
107
|
|
|
98
108
|
export type VisualRequestId = string | number;
|
|
99
109
|
|
|
110
|
+
export interface VisualViewport {
|
|
111
|
+
top: number;
|
|
112
|
+
height: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type VisualScrollRequest = { by: number } | { to: number } | { edge: 'start' | 'end' };
|
|
116
|
+
|
|
117
|
+
export function visualViewportMessage(viewport: VisualViewport | null) {
|
|
118
|
+
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.viewportChanged, params: viewport } as const;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function visualScrollMessage(request: VisualScrollRequest) {
|
|
122
|
+
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.scrollRequest, params: request } as const;
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
// A message of the bridge as one side reads it from the other.
|
|
101
126
|
export type VisualMessage =
|
|
102
127
|
| { method: typeof VISUAL_BRIDGE_METHODS.sandboxProxyReady }
|
|
103
128
|
| { method: typeof VISUAL_BRIDGE_METHODS.sandboxResourceReady; html: string }
|
|
104
129
|
| { method: typeof VISUAL_BRIDGE_METHODS.hostContextChanged; theme: VisualTheme }
|
|
105
130
|
| { method: typeof VISUAL_BRIDGE_METHODS.sizeChanged; width?: number; height: number }
|
|
106
|
-
| { method: typeof VISUAL_BRIDGE_METHODS.openLink; id: VisualRequestId; url: string }
|
|
131
|
+
| { method: typeof VISUAL_BRIDGE_METHODS.openLink; id: VisualRequestId; url: string }
|
|
132
|
+
| { method: typeof VISUAL_BRIDGE_METHODS.viewportReady }
|
|
133
|
+
| { method: typeof VISUAL_BRIDGE_METHODS.viewportChanged; viewport: VisualViewport | null }
|
|
134
|
+
| { method: typeof VISUAL_BRIDGE_METHODS.scrollRequest; request: VisualScrollRequest };
|
|
107
135
|
|
|
108
136
|
export function visualProxyReadyMessage() {
|
|
109
137
|
return { jsonrpc: '2.0', method: VISUAL_BRIDGE_METHODS.sandboxProxyReady } as const;
|
|
@@ -186,6 +214,26 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
|
|
|
186
214
|
}
|
|
187
215
|
return width === undefined ? { method: data.method, height } : { method: data.method, width, height };
|
|
188
216
|
}
|
|
217
|
+
case VISUAL_BRIDGE_METHODS.viewportReady:
|
|
218
|
+
return { method: data.method };
|
|
219
|
+
case VISUAL_BRIDGE_METHODS.viewportChanged: {
|
|
220
|
+
if (data.params === null) {
|
|
221
|
+
return { method: data.method, viewport: null };
|
|
222
|
+
}
|
|
223
|
+
const top = sizeOf(params.top);
|
|
224
|
+
const height = sizeOf(params.height);
|
|
225
|
+
return top !== undefined && height !== undefined && height > 0 ? { method: data.method, viewport: { top, height } } : undefined;
|
|
226
|
+
}
|
|
227
|
+
case VISUAL_BRIDGE_METHODS.scrollRequest: {
|
|
228
|
+
if (typeof params.by === 'number' && Number.isFinite(params.by)) {
|
|
229
|
+
return { method: data.method, request: { by: Math.max(-100_000, Math.min(params.by, 100_000)) } };
|
|
230
|
+
}
|
|
231
|
+
const top = sizeOf(params.to);
|
|
232
|
+
if (top !== undefined) {
|
|
233
|
+
return { method: data.method, request: { to: top } };
|
|
234
|
+
}
|
|
235
|
+
return params.edge === 'start' || params.edge === 'end' ? { method: data.method, request: { edge: params.edge } } : undefined;
|
|
236
|
+
}
|
|
189
237
|
case VISUAL_BRIDGE_METHODS.openLink: {
|
|
190
238
|
const url = webUrl(params.url);
|
|
191
239
|
const id = data.id;
|
|
@@ -356,8 +404,9 @@ export const VISUAL_PAGE_RULES =
|
|
|
356
404
|
'relative paths and local files do not. Prefer inline SVG or a canvas when the drawing needs no library. A link to an http(s) page opens in the browser.';
|
|
357
405
|
|
|
358
406
|
export const VISUAL_LAYOUT_GUIDE =
|
|
359
|
-
"The page appears
|
|
360
|
-
'
|
|
407
|
+
"The page appears as a borderless frame on the thread's own background. By default it follows the reply column (up to 768px in a chat view). " +
|
|
408
|
+
'Opt into the wide layout for application mockups or dashboards that need more space: the host uses the available chat pane width with side margins. ' +
|
|
409
|
+
'Both layouts shrink on narrow screens. Lay the page out at a fluid width (percentages, flex, grid) ' +
|
|
361
410
|
'with no horizontal padding on the outermost element. Draw no outer card, border, shadow or banner title: the page is part of the reply. ' +
|
|
362
411
|
'The frame takes the height of the page, so give a chart a fixed height in pixels, never one relative to the viewport.';
|
|
363
412
|
|
|
@@ -678,6 +727,9 @@ window.addEventListener('message', function (event) {
|
|
|
678
727
|
document.open();
|
|
679
728
|
document.write(params.html);
|
|
680
729
|
document.close();
|
|
730
|
+
var viewport = document.createElement('script');
|
|
731
|
+
viewport.textContent = ${inlineJson(visualViewportScript(VISUAL_BRIDGE_METHODS))};
|
|
732
|
+
document.head.appendChild(viewport);
|
|
681
733
|
});
|
|
682
734
|
window.parent.postMessage({ jsonrpc: '2.0', method: methods.ready }, '*');
|
|
683
735
|
})();`;
|