@adecore/agents-react 0.17.0-beta.2 → 0.17.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.
Files changed (49) hide show
  1. package/dist/chat/chat-client.d.ts +2 -1
  2. package/dist/chat/chat-client.js +8 -2
  3. package/dist/chat/logic/fork.js +2 -0
  4. package/dist/chat/logic/thread-cards.d.ts +9 -0
  5. package/dist/chat/logic/thread-cards.js +35 -13
  6. package/dist/chat/logic/timeline.d.ts +5 -1
  7. package/dist/chat/logic/timeline.js +1 -1
  8. package/dist/chat/logic/visual-height.d.ts +4 -0
  9. package/dist/chat/logic/visual-height.js +32 -0
  10. package/dist/chat/ui/Timeline.js +8 -3
  11. package/dist/chat/ui/VisualDialogs.d.ts +3 -0
  12. package/dist/chat/ui/VisualDialogs.js +32 -0
  13. package/dist/chat/ui/VisualFrame.d.ts +8 -0
  14. package/dist/chat/ui/VisualFrame.js +95 -0
  15. package/dist/chat/ui/rows/Rows.js +3 -0
  16. package/dist/chat/ui/rows/VisualRow.d.ts +5 -0
  17. package/dist/chat/ui/rows/VisualRow.js +23 -0
  18. package/dist/chat/ui/visual-theme.d.ts +10 -0
  19. package/dist/chat/ui/visual-theme.js +172 -0
  20. package/dist/chat/visual-bridge.d.ts +37 -0
  21. package/dist/chat/visual-bridge.js +129 -0
  22. package/dist/chat/visuals.d.ts +9 -0
  23. package/dist/chat/visuals.js +8 -0
  24. package/dist/host.d.ts +5 -0
  25. package/dist/host.js +2 -1
  26. package/dist/locales/en/agent-chat.json +12 -0
  27. package/dist/locales/nl/agent-chat.json +12 -0
  28. package/dist/state/chats.d.ts +4 -1
  29. package/dist/state/chats.js +14 -4
  30. package/dist/theme.css +15 -0
  31. package/package.json +38 -3
  32. package/src/chat/chat-client.ts +10 -1
  33. package/src/chat/logic/fork.ts +2 -0
  34. package/src/chat/logic/thread-cards.ts +46 -13
  35. package/src/chat/logic/timeline.ts +5 -2
  36. package/src/chat/logic/visual-height.ts +38 -0
  37. package/src/chat/ui/Timeline.tsx +11 -2
  38. package/src/chat/ui/VisualDialogs.tsx +64 -0
  39. package/src/chat/ui/VisualFrame.tsx +136 -0
  40. package/src/chat/ui/rows/Rows.tsx +3 -0
  41. package/src/chat/ui/rows/VisualRow.tsx +47 -0
  42. package/src/chat/ui/visual-theme.ts +197 -0
  43. package/src/chat/visual-bridge.ts +176 -0
  44. package/src/chat/visuals.ts +22 -0
  45. package/src/host.ts +12 -1
  46. package/src/locales/en/agent-chat.json +12 -0
  47. package/src/locales/nl/agent-chat.json +12 -0
  48. package/src/state/chats.ts +19 -5
  49. package/src/theme.css +15 -0
@@ -0,0 +1,172 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import { cleanVisualVariables } from '@adecore/agent-contracts/visual';
3
+ import { chatHost } from '../../host.js';
4
+ /*
5
+ * A page's theme from the app's tokens. The names follow common component themes, where `--accent`
6
+ * is a quiet hover ground and `--primary` the color that stands out, so the app's accent is the
7
+ * primary, the ring and the first series of a chart. A variable without a token here, or with a
8
+ * token the app does not set, keeps the page's default for this appearance.
9
+ */
10
+ export function visualThemeOf(tokens, appearance, background) {
11
+ const text = tokens.color('--text');
12
+ const quiet = tokens.color('--surface-hover');
13
+ const raised = tokens.color('--surface-raised');
14
+ const accent = tokens.color('--accent');
15
+ const onAccent = tokens.color('--accent-text');
16
+ const variables = {
17
+ '--background': background,
18
+ '--foreground': text,
19
+ '--muted': quiet,
20
+ '--muted-foreground': tokens.color('--text-muted'),
21
+ '--card': raised,
22
+ '--card-foreground': text,
23
+ '--popover': raised,
24
+ '--popover-foreground': text,
25
+ '--secondary': quiet,
26
+ '--secondary-foreground': text,
27
+ '--border': tokens.color('--border'),
28
+ '--input': tokens.color('--border-strong'),
29
+ '--ring': accent,
30
+ '--primary': accent,
31
+ '--primary-foreground': onAccent,
32
+ '--accent': quiet,
33
+ '--accent-foreground': text,
34
+ '--destructive': tokens.color('--status-error'),
35
+ '--destructive-foreground': onAccent,
36
+ '--warning': tokens.color('--status-needs-you'),
37
+ '--success': tokens.color('--positive'),
38
+ '--success-foreground': tokens.color('--positive-text'),
39
+ '--info': tokens.color('--status-running'),
40
+ '--code-background': tokens.color('--chat-code-bg'),
41
+ '--code-foreground': tokens.color('--chat-output'),
42
+ '--chart-1': tokens.color('--chart-1') || accent,
43
+ '--chart-2': tokens.color('--chart-2'),
44
+ '--chart-3': tokens.color('--chart-3'),
45
+ '--chart-4': tokens.color('--chart-4'),
46
+ '--chart-5': tokens.color('--chart-5'),
47
+ '--chart-6': tokens.color('--chart-6'),
48
+ '--radius': tokens.length('--radius-lg'),
49
+ '--font-sans': tokens.value('--font-sans'),
50
+ '--font-mono': tokens.value('--font-mono')
51
+ };
52
+ return { appearance, variables: cleanVisualVariables(variables) };
53
+ }
54
+ const RGB = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+)(%?)\s*)?\)$/;
55
+ function rgbaOf(color) {
56
+ const match = RGB.exec(color.trim());
57
+ if (match === null) {
58
+ return null;
59
+ }
60
+ const alpha = match[4] === undefined ? 1 : Number(match[4]) / (match[5] === '%' ? 100 : 1);
61
+ return { red: Number(match[1]), green: Number(match[2]), blue: Number(match[3]), alpha };
62
+ }
63
+ function isTransparent(color) {
64
+ return color === 'transparent' || rgbaOf(color)?.alpha === 0;
65
+ }
66
+ /*
67
+ * What lies behind an element, from the computed backgrounds of it and its ancestors, nearest first:
68
+ * the first one that covers what is under it, with every translucent one in front of it laid over it.
69
+ * Null when none of them paints.
70
+ */
71
+ export function backgroundBehind(layers) {
72
+ const translucent = [];
73
+ for (const layer of layers) {
74
+ if (layer === '' || isTransparent(layer)) {
75
+ continue;
76
+ }
77
+ const color = rgbaOf(layer);
78
+ if (color !== null && color.alpha < 1) {
79
+ translucent.push(color);
80
+ continue;
81
+ }
82
+ if (color === null || translucent.length === 0) {
83
+ // A color in a space other than sRGB stays as the engine wrote it, since there is no laying another over it here.
84
+ return color === null ? layer : `rgb(${color.red}, ${color.green}, ${color.blue})`;
85
+ }
86
+ let { red, green, blue } = color;
87
+ for (const over of translucent.reverse()) {
88
+ red = over.red * over.alpha + red * (1 - over.alpha);
89
+ green = over.green * over.alpha + green * (1 - over.alpha);
90
+ blue = over.blue * over.alpha + blue * (1 - over.alpha);
91
+ }
92
+ return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)})`;
93
+ }
94
+ return null;
95
+ }
96
+ function sameTheme(left, right) {
97
+ return left.appearance === right.appearance && JSON.stringify(left.variables) === JSON.stringify(right.variables);
98
+ }
99
+ /*
100
+ * The theme where `element` stands. Colors and lengths are read off a probe in that very place, so a
101
+ * nested theme answers for itself and every value comes out the way the engine computes it.
102
+ */
103
+ function readTheme(element, mode) {
104
+ const view = element.ownerDocument.defaultView ?? window;
105
+ const style = view.getComputedStyle(element);
106
+ const probe = element.ownerDocument.createElement('span');
107
+ probe.style.position = 'absolute';
108
+ probe.style.visibility = 'hidden';
109
+ element.append(probe);
110
+ try {
111
+ const probed = view.getComputedStyle(probe);
112
+ const declared = (token) => style.getPropertyValue(token).trim();
113
+ const computed = (property, token) => {
114
+ if (declared(token) === '') {
115
+ return '';
116
+ }
117
+ probe.style[property] = `var(${token})`;
118
+ return probed[property];
119
+ };
120
+ const tokens = {
121
+ color: (token) => computed('color', token),
122
+ length: (token) => computed('borderTopLeftRadius', token),
123
+ value: declared
124
+ };
125
+ const layers = [];
126
+ for (let node = element; node !== null; node = node.parentElement) {
127
+ layers.push(view.getComputedStyle(node).backgroundColor);
128
+ }
129
+ const scheme = style.colorScheme.trim();
130
+ const appearance = scheme === 'light' || scheme === 'dark' ? scheme : mode;
131
+ return visualThemeOf(tokens, appearance, backgroundBehind(layers) ?? tokens.color('--surface'));
132
+ }
133
+ finally {
134
+ probe.remove();
135
+ }
136
+ }
137
+ /* The attributes an app switches its theme or its accent with. */
138
+ const THEME_ATTRIBUTES = ['class', 'style', 'data-theme'];
139
+ /*
140
+ * The app's theme for a visual drawn in `ref`, null until the element is there. It is read again when
141
+ * the app turns light or dark, and when the root of the document changes a theme attribute, so an
142
+ * accent or a font a person picks reaches every frame as well.
143
+ */
144
+ export function useVisualTheme(ref) {
145
+ const mode = chatHost().code.useMode();
146
+ const [theme, setTheme] = useState(null);
147
+ useLayoutEffect(() => {
148
+ const element = ref.current;
149
+ if (element === null) {
150
+ return;
151
+ }
152
+ const read = () => {
153
+ const next = readTheme(element, mode);
154
+ setTheme((current) => (current !== null && sameTheme(current, next) ? current : next));
155
+ };
156
+ read();
157
+ const owner = element.ownerDocument;
158
+ const observer = new MutationObserver(read);
159
+ for (const root of [owner.documentElement, owner.body]) {
160
+ if (root !== null) {
161
+ observer.observe(root, { attributes: true, attributeFilter: THEME_ATTRIBUTES });
162
+ }
163
+ }
164
+ const scheme = owner.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
165
+ scheme?.addEventListener('change', read);
166
+ return () => {
167
+ observer.disconnect();
168
+ scheme?.removeEventListener('change', read);
169
+ };
170
+ }, [ref, mode]);
171
+ return theme;
172
+ }
@@ -0,0 +1,37 @@
1
+ import { type ChatVisual, type VisualAppearance, type VisualTheme } from '@adecore/agent-contracts/visual';
2
+ export interface VisualFrameWindow {
3
+ postMessage(message: unknown, targetOrigin: string): void;
4
+ }
5
+ export interface VisualBridgeOptions {
6
+ frame(): VisualFrameWindow | null;
7
+ visual: Pick<ChatVisual, 'maxHeight'>;
8
+ page: Promise<string>;
9
+ theme: VisualTheme;
10
+ mayOpenLink(): boolean;
11
+ openLink(url: string): void;
12
+ onHeight(height: number): void;
13
+ onAppearance(appearance: VisualAppearance): void;
14
+ onFailure(): void;
15
+ after?(ms: number, run: () => void): () => void;
16
+ }
17
+ export declare class VisualBridge {
18
+ private readonly options;
19
+ private theme;
20
+ private html;
21
+ private heard;
22
+ private waiting;
23
+ private sent;
24
+ private live;
25
+ private disposed;
26
+ private cancelSilence;
27
+ constructor(options: VisualBridgeOptions);
28
+ receive(event: {
29
+ source: unknown;
30
+ data: unknown;
31
+ }): void;
32
+ setTheme(theme: VisualTheme): void;
33
+ loaded(): void;
34
+ dispose(): void;
35
+ private sendPage;
36
+ private postTheme;
37
+ }
@@ -0,0 +1,129 @@
1
+ import { VISUAL_BRIDGE_METHODS, parseVisualMessage, visualHostContextMessage, visualOpenLinkResult, visualResourceReadyMessage } from '@adecore/agent-contracts/visual';
2
+ import { clampVisualHeight } from './logic/visual-height.js';
3
+ /*
4
+ * How long a frame that finished loading may stay silent before it counts as failed. The sandbox host
5
+ * page speaks while it loads, so silence means the address served something else; the margin is for
6
+ * its message and the load event, which cross between processes and may arrive in either order.
7
+ */
8
+ const SILENCE_MS = 3000;
9
+ function timeout(ms, run) {
10
+ const timer = setTimeout(run, ms);
11
+ return () => clearTimeout(timer);
12
+ }
13
+ /*
14
+ * Talks to the frame of one visual. It hands the sandbox host page the visual's page when it says it
15
+ * listens, keeps the page in the app's theme, follows the size the page reports and opens the links a
16
+ * person follows in it. A message counts only when it comes from the frame's own window, and the
17
+ * messages that come from the page count only once the page was sent.
18
+ */
19
+ export class VisualBridge {
20
+ options;
21
+ theme;
22
+ html = null;
23
+ /* Whether the sandbox host page ever said it listens, which is what tells it from an error page. */
24
+ heard = false;
25
+ /* The sandbox host page listens and has not been sent the page yet. */
26
+ waiting = false;
27
+ /* The theme the page was last sent, from the moment it was sent; null before that. */
28
+ sent = null;
29
+ /* Whether the page reported a size since it was sent, which says it runs and is drawn. */
30
+ live = false;
31
+ disposed = false;
32
+ cancelSilence = () => undefined;
33
+ constructor(options) {
34
+ this.options = options;
35
+ this.theme = options.theme;
36
+ options.page.then((html) => {
37
+ this.html = html;
38
+ this.sendPage();
39
+ }, () => {
40
+ if (!this.disposed) {
41
+ options.onFailure();
42
+ }
43
+ });
44
+ }
45
+ /* A `message` event of the window the frame is in. */
46
+ receive(event) {
47
+ const frame = this.options.frame();
48
+ if (this.disposed || frame === null || event.source !== frame) {
49
+ return;
50
+ }
51
+ const message = parseVisualMessage(event.data);
52
+ switch (message?.method) {
53
+ case VISUAL_BRIDGE_METHODS.sandboxProxyReady:
54
+ // A frame that loads again starts over from the sandbox host page.
55
+ this.heard = true;
56
+ this.waiting = true;
57
+ this.sent = null;
58
+ this.live = false;
59
+ this.cancelSilence();
60
+ this.sendPage();
61
+ return;
62
+ case VISUAL_BRIDGE_METHODS.sizeChanged:
63
+ if (this.sent === null) {
64
+ return;
65
+ }
66
+ if (!this.live) {
67
+ this.live = true;
68
+ this.options.onAppearance(this.sent.appearance);
69
+ }
70
+ this.options.onHeight(clampVisualHeight(this.options.visual, message.height));
71
+ return;
72
+ case VISUAL_BRIDGE_METHODS.openLink:
73
+ if (this.sent === null) {
74
+ return;
75
+ }
76
+ if (this.options.mayOpenLink()) {
77
+ this.options.openLink(message.url);
78
+ }
79
+ frame.postMessage(visualOpenLinkResult(message.id), '*');
80
+ return;
81
+ default:
82
+ return;
83
+ }
84
+ }
85
+ /* The app's theme changed; a page that was sent follows it at once, without loading again. */
86
+ setTheme(theme) {
87
+ this.theme = theme;
88
+ const frame = this.options.frame();
89
+ if (this.disposed || this.sent === null || frame === null) {
90
+ return;
91
+ }
92
+ this.postTheme(frame);
93
+ if (this.live) {
94
+ this.options.onAppearance(theme.appearance);
95
+ }
96
+ }
97
+ /* The frame fired `load`. */
98
+ loaded() {
99
+ if (this.heard || this.disposed) {
100
+ return;
101
+ }
102
+ this.cancelSilence();
103
+ this.cancelSilence = (this.options.after ?? timeout)(SILENCE_MS, () => {
104
+ if (!this.heard && !this.disposed) {
105
+ this.options.onFailure();
106
+ }
107
+ });
108
+ }
109
+ dispose() {
110
+ this.disposed = true;
111
+ this.cancelSilence();
112
+ }
113
+ sendPage() {
114
+ const frame = this.options.frame();
115
+ if (this.disposed || !this.waiting || this.html === null || frame === null) {
116
+ return;
117
+ }
118
+ this.waiting = false;
119
+ // Any origin, since the frame's own is opaque; `receive` checks every answer's source instead.
120
+ frame.postMessage(visualResourceReadyMessage(this.html), '*');
121
+ // The page writes itself as that message arrives and listens before the next one, so a theme
122
+ // that changed since the frame's address was set follows right after it.
123
+ this.postTheme(frame);
124
+ }
125
+ postTheme(frame) {
126
+ this.sent = this.theme;
127
+ frame.postMessage(visualHostContextMessage(this.theme), '*');
128
+ }
129
+ }
@@ -0,0 +1,9 @@
1
+ interface VisualDialog {
2
+ chatKey: string | null;
3
+ visualId: string | null;
4
+ kind: 'expand' | 'remove' | null;
5
+ open(chatKey: string, visualId: string, kind: 'expand' | 'remove'): void;
6
+ close(): void;
7
+ }
8
+ export declare const useVisualDialog: import("zustand").UseBoundStore<import("zustand").StoreApi<VisualDialog>>;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ import { create } from 'zustand';
2
+ export const useVisualDialog = create((set) => ({
3
+ chatKey: null,
4
+ visualId: null,
5
+ kind: null,
6
+ open: (chatKey, visualId, kind) => set({ chatKey, visualId, kind }),
7
+ close: () => set({ chatKey: null, visualId: null, kind: null })
8
+ }));
package/dist/host.d.ts CHANGED
@@ -106,6 +106,10 @@ export interface ThreadCard {
106
106
  at: number;
107
107
  render(): ReactNode;
108
108
  }
109
+ export interface VisualHost {
110
+ frameUrl: string;
111
+ openLink(url: string): void;
112
+ }
109
113
  export interface ReplyAuthor {
110
114
  name: string;
111
115
  mark?: ReactNode;
@@ -190,6 +194,7 @@ export interface ChatHost {
190
194
  SubagentSlot: ComponentType<SubagentSlotProps> | null;
191
195
  useThreadCards(scopeId: string, chatId: string): readonly ThreadCard[];
192
196
  useReplyAuthor(scopeId: string, chatId: string): ReplyAuthor | null;
197
+ visuals: VisualHost | null;
193
198
  }
194
199
  export declare function setChatHost(patch: Partial<ChatHost>): void;
195
200
  export declare function chatHost(): ChatHost;
package/dist/host.js CHANGED
@@ -41,7 +41,8 @@ const DEFAULT_HOST = {
41
41
  ComposerSlot: null,
42
42
  SubagentSlot: null,
43
43
  useThreadCards: () => NO_CARDS,
44
- useReplyAuthor: () => null
44
+ useReplyAuthor: () => null,
45
+ visuals: null
45
46
  };
46
47
  let host = DEFAULT_HOST;
47
48
  export function setChatHost(patch) {
@@ -416,6 +416,18 @@
416
416
  "renameFailed": "The bookmark could not be renamed",
417
417
  "removeFailed": "The bookmark could not be removed"
418
418
  },
419
+ "visuals": {
420
+ "actions": "Actions for {{title}}",
421
+ "expand": "Open large",
422
+ "more": "More actions",
423
+ "remove": "Remove visual",
424
+ "removeTitle": "Remove this visual?",
425
+ "removeDescription": "{{title}} is deleted from the chat. This cannot be undone.",
426
+ "removeFailed": "The visual could not be removed",
427
+ "loading": "Loading {{title}}",
428
+ "failed": "Could not load {{title}}",
429
+ "renderFailed": "This visual failed to render"
430
+ },
419
431
  "image": {
420
432
  "zoomIn": "Zoom in",
421
433
  "zoomOut": "Zoom out",
@@ -416,6 +416,18 @@
416
416
  "renameFailed": "De bladwijzer kon niet worden hernoemd",
417
417
  "removeFailed": "De bladwijzer kon niet worden verwijderd"
418
418
  },
419
+ "visuals": {
420
+ "actions": "Acties voor {{title}}",
421
+ "expand": "Groot openen",
422
+ "more": "Meer acties",
423
+ "remove": "Visualisatie verwijderen",
424
+ "removeTitle": "Deze visualisatie verwijderen?",
425
+ "removeDescription": "{{title}} wordt uit de chat verwijderd. Dit kan niet ongedaan worden gemaakt.",
426
+ "removeFailed": "De visualisatie kon niet worden verwijderd",
427
+ "loading": "{{title}} wordt geladen",
428
+ "failed": "{{title}} kon niet worden geladen",
429
+ "renderFailed": "Deze visualisatie kon niet worden getekend"
430
+ },
419
431
  "image": {
420
432
  "zoomIn": "Inzoomen",
421
433
  "zoomOut": "Uitzoomen",
@@ -1,4 +1,4 @@
1
- import type { ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
1
+ import type { ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
2
2
  export interface ChatState {
3
3
  info: ChatInfo;
4
4
  items: Record<string, ChatItem>;
@@ -7,6 +7,7 @@ export interface ChatState {
7
7
  history?: ChatHistoryPage;
8
8
  waitingBefore?: string[];
9
9
  bookmarks?: ChatBookmark[];
10
+ visuals?: ChatVisual[];
10
11
  }
11
12
  export type ChatHistoryPage = ChatHistoryResult['history'];
12
13
  export interface ChatPage {
@@ -21,6 +22,7 @@ export interface ChatSink {
21
22
  apply(chatId: string, event: ChatEvent): void;
22
23
  status(chatId: string, info: ChatInfo): void;
23
24
  bookmarks(chatId: string, bookmarks: ChatBookmark[]): void;
25
+ visuals(chatId: string, visuals: ChatVisual[]): void;
24
26
  forget(chatId: string): void;
25
27
  }
26
28
  interface ChatsStore {
@@ -31,6 +33,7 @@ interface ChatsStore {
31
33
  apply(key: string, event: ChatEvent): void;
32
34
  status(key: string, info: ChatInfo): void;
33
35
  bookmarks(key: string, bookmarks: ChatBookmark[]): void;
36
+ visuals(key: string, visuals: ChatVisual[]): void;
34
37
  forget(key: string): void;
35
38
  forgetWhere(matches: (key: string) => boolean): void;
36
39
  }
@@ -105,11 +105,11 @@ export const useChats = create((set) => ({
105
105
  statusByKey: {},
106
106
  reset(key, info, items, page) {
107
107
  set((s) => {
108
- // The bookmarks are not part of the thread; they come in on their own and outlive a reset of it.
109
- const bookmarks = s.byKey[key]?.bookmarks;
110
- const next = stateOf(info, items, page);
108
+ // The bookmarks and the visuals are not part of the thread; they come in on their own and outlive a reset of it.
109
+ const { bookmarks, visuals } = s.byKey[key] ?? {};
110
+ const next = { ...stateOf(info, items, page), ...(bookmarks === undefined ? {} : { bookmarks }), ...(visuals === undefined ? {} : { visuals }) };
111
111
  return {
112
- byKey: { ...s.byKey, [key]: bookmarks === undefined ? next : { ...next, bookmarks } },
112
+ byKey: { ...s.byKey, [key]: next },
113
113
  statusByKey: withStatus(s.statusByKey, key, info)
114
114
  };
115
115
  });
@@ -161,6 +161,15 @@ export const useChats = create((set) => ({
161
161
  return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
162
162
  });
163
163
  },
164
+ visuals(key, visuals) {
165
+ set((s) => {
166
+ const current = s.byKey[key];
167
+ if (!current) {
168
+ return {};
169
+ }
170
+ return { byKey: { ...s.byKey, [key]: { ...current, visuals } } };
171
+ });
172
+ },
164
173
  forget(key) {
165
174
  set((s) => {
166
175
  const next = { ...s.byKey };
@@ -182,6 +191,7 @@ export function chatSink(keyOf) {
182
191
  apply: (chatId, event) => useChats.getState().apply(keyOf(chatId), event),
183
192
  status: (chatId, info) => useChats.getState().status(keyOf(chatId), info),
184
193
  bookmarks: (chatId, bookmarks) => useChats.getState().bookmarks(keyOf(chatId), bookmarks),
194
+ visuals: (chatId, visuals) => useChats.getState().visuals(keyOf(chatId), visuals),
185
195
  forget: (chatId) => useChats.getState().forget(keyOf(chatId))
186
196
  };
187
197
  }
package/dist/theme.css CHANGED
@@ -20,6 +20,13 @@
20
20
  --chart-context-system: var(--text-faint);
21
21
  /* The current hit of a find, which has to stand out from the accent of a bookmark next to it. */
22
22
  --chat-find: #facc15;
23
+ /* The series of a chart in a visual after the first, which is the accent. A categorical order checked
24
+ for color vision deficiency on both grounds: orange, aqua, yellow, magenta, green. */
25
+ --chart-2: #eb6834;
26
+ --chart-3: #1baf7a;
27
+ --chart-4: #eda100;
28
+ --chart-5: #e87ba4;
29
+ --chart-6: #008300;
23
30
  }
24
31
 
25
32
  [data-theme="dark"] {
@@ -34,6 +41,11 @@
34
41
  --chart-context-conversation: #6dd08b;
35
42
  --chart-context-system: var(--text-faint);
36
43
  --chat-find: #f5c542;
44
+ --chart-2: #d95926;
45
+ --chart-3: #199e70;
46
+ --chart-4: #c98500;
47
+ --chart-5: #d55181;
48
+ --chart-6: #008300;
37
49
  }
38
50
 
39
51
  /* Output and diffs take the terminal's colors when the app loads `@adecore/terminal/terminal.css`,
@@ -43,6 +55,7 @@
43
55
  --chat-output: var(--term-fg, var(--text));
44
56
  --chat-added: var(--term-ansi-green, var(--positive));
45
57
  --chat-removed: var(--term-ansi-red, var(--status-error));
58
+ --chart-1: var(--accent);
46
59
  }
47
60
 
48
61
  @theme inline {
@@ -114,6 +127,8 @@
114
127
  @starting-style { opacity: 0; }
115
128
  }
116
129
  @keyframes chat-fade { from { opacity: 0; } to { opacity: 1; } }
130
+ /* A visual still on its way says so only once it takes long enough to be noticed. */
131
+ .chat-visual-wait { animation: chat-fade 200ms ease-out 600ms both; }
117
132
 
118
133
  /* The row a jump from outside the chat (the card of a change in a file) landed on. */
119
134
  .chat-flash { animation: chat-flash 1600ms ease-out both; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.17.0-beta.2",
3
+ "version": "0.17.0",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -184,6 +184,11 @@
184
184
  "types": "./dist/chat/logic/tools.d.ts",
185
185
  "default": "./dist/chat/logic/tools.js"
186
186
  },
187
+ "./chat/logic/visual-height": {
188
+ "source": "./src/chat/logic/visual-height.ts",
189
+ "types": "./dist/chat/logic/visual-height.d.ts",
190
+ "default": "./dist/chat/logic/visual-height.js"
191
+ },
187
192
  "./chat/logic/welcome": {
188
193
  "source": "./src/chat/logic/welcome.ts",
189
194
  "types": "./dist/chat/logic/welcome.d.ts",
@@ -269,6 +274,16 @@
269
274
  "types": "./dist/chat/timeline-scroll.d.ts",
270
275
  "default": "./dist/chat/timeline-scroll.js"
271
276
  },
277
+ "./chat/visual-bridge": {
278
+ "source": "./src/chat/visual-bridge.ts",
279
+ "types": "./dist/chat/visual-bridge.d.ts",
280
+ "default": "./dist/chat/visual-bridge.js"
281
+ },
282
+ "./chat/visuals": {
283
+ "source": "./src/chat/visuals.ts",
284
+ "types": "./dist/chat/visuals.d.ts",
285
+ "default": "./dist/chat/visuals.js"
286
+ },
272
287
  "./chat/ui/AccountPill": {
273
288
  "source": "./src/chat/ui/AccountPill.tsx",
274
289
  "types": "./dist/chat/ui/AccountPill.d.ts",
@@ -419,6 +434,16 @@
419
434
  "types": "./dist/chat/ui/UploadThumb.d.ts",
420
435
  "default": "./dist/chat/ui/UploadThumb.js"
421
436
  },
437
+ "./chat/ui/VisualDialogs": {
438
+ "source": "./src/chat/ui/VisualDialogs.tsx",
439
+ "types": "./dist/chat/ui/VisualDialogs.d.ts",
440
+ "default": "./dist/chat/ui/VisualDialogs.js"
441
+ },
442
+ "./chat/ui/VisualFrame": {
443
+ "source": "./src/chat/ui/VisualFrame.tsx",
444
+ "types": "./dist/chat/ui/VisualFrame.d.ts",
445
+ "default": "./dist/chat/ui/VisualFrame.js"
446
+ },
422
447
  "./chat/ui/Welcome": {
423
448
  "source": "./src/chat/ui/Welcome.tsx",
424
449
  "types": "./dist/chat/ui/Welcome.d.ts",
@@ -549,6 +574,11 @@
549
574
  "types": "./dist/chat/ui/rows/TaskRow.d.ts",
550
575
  "default": "./dist/chat/ui/rows/TaskRow.js"
551
576
  },
577
+ "./chat/ui/rows/VisualRow": {
578
+ "source": "./src/chat/ui/rows/VisualRow.tsx",
579
+ "types": "./dist/chat/ui/rows/VisualRow.d.ts",
580
+ "default": "./dist/chat/ui/rows/VisualRow.js"
581
+ },
552
582
  "./chat/ui/rows/WorkRows": {
553
583
  "source": "./src/chat/ui/rows/WorkRows.tsx",
554
584
  "types": "./dist/chat/ui/rows/WorkRows.d.ts",
@@ -569,6 +599,11 @@
569
599
  "types": "./dist/chat/ui/useToggleSet.d.ts",
570
600
  "default": "./dist/chat/ui/useToggleSet.js"
571
601
  },
602
+ "./chat/ui/visual-theme": {
603
+ "source": "./src/chat/ui/visual-theme.ts",
604
+ "types": "./dist/chat/ui/visual-theme.d.ts",
605
+ "default": "./dist/chat/ui/visual-theme.js"
606
+ },
572
607
  "./host": {
573
608
  "source": "./src/host.ts",
574
609
  "types": "./dist/host.d.ts",
@@ -848,7 +883,7 @@
848
883
  "@lezer/highlight": "^1.2.3",
849
884
  "@lezer/markdown": "^1.7.2",
850
885
  "@pierre/diffs": "^1.4.3",
851
- "@adecore/agent-contracts": "0.17.0-beta.2",
886
+ "@adecore/agent-contracts": "0.17.0",
852
887
  "@tanstack/react-virtual": "^3.14.13",
853
888
  "clsx": "^2.1.1",
854
889
  "lucide-react": "^1.47.0",
@@ -861,7 +896,7 @@
861
896
  "zustand": "^5.0.15"
862
897
  },
863
898
  "peerDependencies": {
864
- "@adecore/ui": "^0.17.0-beta.2",
899
+ "@adecore/ui": "^0.17.0",
865
900
  "i18next": "^26.4.2",
866
901
  "react": "^19.3.0",
867
902
  "react-dom": "^19.3.0",