@adecore/agent-contracts 0.18.4 → 0.20.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 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>;
@@ -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,5 @@
1
+ export declare function visualViewportScript(methods: {
2
+ viewportReady: string;
3
+ viewportChanged: string;
4
+ scrollRequest: string;
5
+ }): string;
@@ -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-x:auto!important;overflow-y: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 in the reply as a borderless frame on the thread's own background, as wide as the reply column: " +
306
- 'about 360px on a phone and up to about 1200px on a wide screen. Lay it out at a fluid width (percentages, flex, grid) ' +
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agent-contracts",
3
- "version": "0.18.4",
3
+ "version": "0.20.0",
4
4
  "description": "Schemas and port contracts for agent chats, provider accounts, models and usage.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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-x:auto!important;overflow-y: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 in the reply as a borderless frame on the thread's own background, as wide as the reply column: " +
360
- 'about 360px on a phone and up to about 1200px on a wide screen. Lay it out at a fluid width (percentages, flex, grid) ' +
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
  })();`;