@adecore/agents-react 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/chat/logic/generated-image.d.ts +27 -0
  2. package/dist/chat/logic/generated-image.js +32 -0
  3. package/dist/chat/logic/timeline-target.d.ts +13 -1
  4. package/dist/chat/logic/timeline-target.js +26 -0
  5. package/dist/chat/logic/timeline.js +16 -10
  6. package/dist/chat/subagent-conversation.d.ts +2 -1
  7. package/dist/chat/subagent-conversation.js +7 -1
  8. package/dist/chat/timeline-flash.d.ts +3 -1
  9. package/dist/chat/timeline-flash.js +2 -2
  10. package/dist/chat/ui/ImageView.d.ts +8 -2
  11. package/dist/chat/ui/ImageView.js +12 -6
  12. package/dist/chat/ui/SubagentTimeline.js +6 -2
  13. package/dist/chat/ui/Timeline.js +67 -7
  14. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +23 -0
  15. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +103 -0
  16. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  17. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  18. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +15 -0
  19. package/dist/chat/ui/intelligent-ui/UiReply.js +329 -0
  20. package/dist/chat/ui/intelligent-ui/block-local.d.ts +11 -0
  21. package/dist/chat/ui/intelligent-ui/block-local.js +42 -0
  22. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  23. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  24. package/dist/chat/ui/intelligent-ui/node-text.d.ts +15 -0
  25. package/dist/chat/ui/intelligent-ui/node-text.js +106 -0
  26. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  27. package/dist/chat/ui/intelligent-ui/registry.js +51 -0
  28. package/dist/chat/ui/intelligent-ui/render-context.d.ts +53 -0
  29. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  30. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  31. package/dist/chat/ui/intelligent-ui/renderers/chart.js +123 -0
  32. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  33. package/dist/chat/ui/intelligent-ui/renderers/choices.js +45 -0
  34. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  35. package/dist/chat/ui/intelligent-ui/renderers/content.js +59 -0
  36. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  37. package/dist/chat/ui/intelligent-ui/renderers/data.js +80 -0
  38. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +8 -0
  39. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +72 -0
  40. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  41. package/dist/chat/ui/intelligent-ui/renderers/links.js +87 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  46. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  47. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  48. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  49. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  50. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  51. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  52. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  53. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  54. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  55. package/dist/chat/ui/intelligent-ui/use-ui-links.js +126 -0
  56. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +21 -0
  57. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +272 -0
  58. package/dist/chat/ui/reply-context.d.ts +8 -0
  59. package/dist/chat/ui/reply-context.js +1 -0
  60. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  61. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  62. package/dist/chat/ui/rows/MessageRows.js +21 -4
  63. package/dist/chat/ui/rows/Rows.js +4 -2
  64. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  65. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  66. package/dist/chat/ui/rows/WorkRows.js +4 -3
  67. package/dist/host.d.ts +11 -1
  68. package/dist/locales/en/agent-chat.json +101 -0
  69. package/dist/locales/nl/agent-chat.json +101 -0
  70. package/dist/state/chats.js +13 -0
  71. package/dist/theme.css +19 -0
  72. package/package.json +128 -7
  73. package/src/chat/logic/generated-image.ts +48 -0
  74. package/src/chat/logic/timeline-target.ts +41 -1
  75. package/src/chat/logic/timeline.ts +19 -10
  76. package/src/chat/subagent-conversation.ts +13 -2
  77. package/src/chat/timeline-flash.ts +5 -3
  78. package/src/chat/ui/ImageView.tsx +51 -8
  79. package/src/chat/ui/SubagentTimeline.tsx +24 -1
  80. package/src/chat/ui/Timeline.tsx +97 -21
  81. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +219 -0
  82. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  83. package/src/chat/ui/intelligent-ui/UiReply.tsx +541 -0
  84. package/src/chat/ui/intelligent-ui/block-local.ts +51 -0
  85. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  86. package/src/chat/ui/intelligent-ui/node-text.ts +119 -0
  87. package/src/chat/ui/intelligent-ui/registry.ts +53 -0
  88. package/src/chat/ui/intelligent-ui/render-context.ts +84 -0
  89. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +319 -0
  90. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +102 -0
  91. package/src/chat/ui/intelligent-ui/renderers/content.tsx +140 -0
  92. package/src/chat/ui/intelligent-ui/renderers/data.tsx +181 -0
  93. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +144 -0
  94. package/src/chat/ui/intelligent-ui/renderers/links.tsx +158 -0
  95. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  96. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  97. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  98. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  99. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  100. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  101. package/src/chat/ui/intelligent-ui/use-ui-links.ts +143 -0
  102. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +308 -0
  103. package/src/chat/ui/reply-context.ts +7 -0
  104. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  105. package/src/chat/ui/rows/MessageRows.tsx +60 -4
  106. package/src/chat/ui/rows/Rows.tsx +4 -2
  107. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  108. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  109. package/src/host.ts +27 -1
  110. package/src/locales/en/agent-chat.json +101 -0
  111. package/src/locales/nl/agent-chat.json +101 -0
  112. package/src/state/chats.ts +13 -0
  113. package/src/theme.css +19 -0
@@ -0,0 +1,143 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { ChatUiLinkReading, ChatUiQueryState } from '@adecore/agent-contracts';
3
+ import { uiLinkTargets, type UiLinkTarget } from '@adecore/intelligent-ui/links';
4
+ import { uiInputValues, UI_HOST_LIMITS, type UiBlock, UiState } from '@adecore/intelligent-ui';
5
+ import { chatHost } from '../../../host';
6
+ import type { UiRenderContext } from './render-context';
7
+
8
+ interface LinkState {
9
+ target: string;
10
+ reading: ChatUiLinkReading;
11
+ }
12
+
13
+ // Inputs that change in a run, such as a dragged slider, check their links once the run rested.
14
+ const LINK_SETTLE_MS = 300;
15
+
16
+ export function useUiLinks(
17
+ block: UiBlock,
18
+ state: UiState,
19
+ context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>,
20
+ reads: Readonly<Record<string, string>>,
21
+ frozen?: ChatUiQueryState['blocks'][string]
22
+ ) {
23
+ const [checked, setChecked] = useState<Record<string, LinkState>>({});
24
+ // What each node was last checked as, so a change of input checks only the targets it moved.
25
+ const asked = useRef(new Map<string, string>());
26
+ const host = chatHost().intelligentUi;
27
+ const values = uiInputValues(block, state);
28
+ const scope = state.scope();
29
+ const queryValues = Object.fromEntries(
30
+ Object.keys(block.queries)
31
+ .filter((name) => Object.hasOwn(scope, name))
32
+ .map((name) => [name, scope[name]])
33
+ );
34
+ let targets: Record<string, UiLinkTarget> = {};
35
+ try {
36
+ if (block.complete) {
37
+ targets = uiLinkTargets(block, values, queryValues);
38
+ }
39
+ } catch {}
40
+ const signature = JSON.stringify(targets);
41
+ const input = JSON.stringify(values);
42
+ const tickets = JSON.stringify(reads);
43
+ const defaultInput = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
44
+ const link = host?.link;
45
+ useEffect(() => {
46
+ asked.current.clear();
47
+ }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, link]);
48
+ useEffect(() => {
49
+ if (!link || !block.revision || context.phase !== 'final') {
50
+ return;
51
+ }
52
+ let stopped = false;
53
+ const validate = async () => {
54
+ const moved = Object.entries(JSON.parse(signature) as Record<string, UiLinkTarget>)
55
+ .slice(0, UI_HOST_LIMITS.links)
56
+ .filter(([nodeId, target]) => asked.current.get(nodeId) !== JSON.stringify(target));
57
+ for (const [nodeId, target] of moved) {
58
+ const record = (reading: ChatUiLinkReading) => {
59
+ asked.current.set(nodeId, JSON.stringify(target));
60
+ setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
61
+ };
62
+ try {
63
+ const reading = await link(context.scopeId, {
64
+ chatId: context.chatId,
65
+ itemId: context.itemId,
66
+ blockId: block.id,
67
+ revision: block.revision!,
68
+ nodeId,
69
+ values: JSON.parse(input),
70
+ reads: JSON.parse(tickets)
71
+ });
72
+ if (stopped) {
73
+ return;
74
+ }
75
+ record(reading);
76
+ } catch {
77
+ if (stopped) {
78
+ return;
79
+ }
80
+ record({ state: 'plain' });
81
+ }
82
+ }
83
+ };
84
+ // The first check runs at once; a later one waits until the inputs rested.
85
+ const timer = setTimeout(() => void validate(), asked.current.size === 0 ? 0 : LINK_SETTLE_MS);
86
+ return () => {
87
+ stopped = true;
88
+ clearTimeout(timer);
89
+ };
90
+ }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
91
+ const idOf = (target: UiLinkTarget) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
92
+ const resolution = (target: UiLinkTarget): ChatUiLinkReading => {
93
+ const id = idOf(target);
94
+ if (id === undefined) {
95
+ return { state: 'plain' };
96
+ }
97
+ if (checked[id]?.target === JSON.stringify(target)) {
98
+ return checked[id].reading;
99
+ }
100
+ if (input === defaultInput && frozen && frozen.revision === block.revision) {
101
+ const firstQueries = Object.fromEntries(
102
+ Object.entries(frozen.readings)
103
+ .filter(([, reading]) => reading.state === 'fresh')
104
+ .map(([name, reading]) => [name, reading.value])
105
+ );
106
+ if (JSON.stringify(queryValues) === JSON.stringify(firstQueries)) {
107
+ return frozen.links?.[id] ?? { state: 'plain' };
108
+ }
109
+ }
110
+ return { state: 'plain' };
111
+ };
112
+ const open = (target: UiLinkTarget) => {
113
+ const nodeId = idOf(target);
114
+ if (!nodeId || !link || !block.revision) {
115
+ return;
116
+ }
117
+ void link(context.scopeId, {
118
+ chatId: context.chatId,
119
+ itemId: context.itemId,
120
+ blockId: block.id,
121
+ revision: block.revision,
122
+ nodeId,
123
+ values,
124
+ reads: { ...reads }
125
+ })
126
+ .then((reading) => {
127
+ setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
128
+ if (reading.state === 'chip' && reading.target) {
129
+ host?.openLink?.(context.scopeId, reading);
130
+ }
131
+ })
132
+ .catch(() => {
133
+ setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
134
+ });
135
+ };
136
+ const hostOpenUrl = host?.openUrl;
137
+ const openUrl = useCallback((url: string) => hostOpenUrl?.(context.scopeId, url), [hostOpenUrl, context.scopeId]);
138
+ return {
139
+ link: link ? resolution : undefined,
140
+ openLink: link && host?.openLink ? open : undefined,
141
+ openUrl: hostOpenUrl ? openUrl : undefined
142
+ };
143
+ }
@@ -0,0 +1,308 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { ChatUiQueryReading, ChatUiQueryState } from '@adecore/agent-contracts';
3
+ import { uiInputValues, UI_CATALOG_VERSION, UI_HOST_LIMITS, UiState, type UiBlock, type UiViewNode, type UiNode } from '@adecore/intelligent-ui';
4
+ import { chatHost } from '../../../host';
5
+ import type { UiLiveValue, UiLiveStatus, UiRenderContext } from './render-context';
6
+
7
+ interface LocalReading {
8
+ identity: string;
9
+ reading: ChatUiQueryReading;
10
+ successful?: { inputs: string; reading: ChatUiQueryReading };
11
+ }
12
+
13
+ const NO_READINGS: ChatUiQueryState['blocks'][string]['readings'] = {};
14
+ // A changed input reads once it has rested this long, so dragging a slider reads its last value only.
15
+ const INPUT_SETTLE_MS = 300;
16
+
17
+ /* The reading that holds for these inputs: this view's own read of them, or the frozen first one for the inputs the block starts with. */
18
+ function successfulReading(
19
+ local: LocalReading | undefined,
20
+ initial: ChatUiQueryReading | undefined,
21
+ identity: string,
22
+ signature: string,
23
+ defaultInputs: string
24
+ ): ChatUiQueryReading | undefined {
25
+ if (local?.identity === identity && local.successful?.inputs === signature) {
26
+ return local.successful.reading;
27
+ }
28
+ return signature === defaultInputs && initial?.state === 'fresh' ? initial : undefined;
29
+ }
30
+
31
+ function settleDeadline(): number {
32
+ return Date.now() + INPUT_SETTLE_MS;
33
+ }
34
+
35
+ function documentVisible(): boolean {
36
+ return document.visibilityState !== 'hidden';
37
+ }
38
+
39
+ export function useUiQueries(
40
+ block: UiBlock,
41
+ state: UiState,
42
+ context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>,
43
+ element: HTMLDivElement | null,
44
+ frozen?: ChatUiQueryState['blocks'][string]
45
+ ) {
46
+ const [readings, setReadings] = useState<Record<string, LocalReading>>({});
47
+ const [busyInput, setBusyInput] = useState<string | null>(null);
48
+ const lastRead = useRef({ inputKey: '', at: 0 });
49
+ const names = Object.keys(block.queries);
50
+ const inputs = JSON.stringify(uiInputValues(block, state));
51
+ const identity = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id, block.revision]);
52
+ const inputKey = JSON.stringify([identity, inputs]);
53
+ const reading = busyInput === inputKey;
54
+ const defaultInputs = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
55
+ const initial = frozen && frozen.revision === block.revision ? frozen.readings : NO_READINGS;
56
+ const successfulFor = (name: string, signature: string): ChatUiQueryReading | undefined =>
57
+ successfulReading(readings[name], initial[name], identity, signature, defaultInputs);
58
+ const readsFor = (values: Readonly<Record<string, unknown>>): Record<string, string> | null => {
59
+ const signature = JSON.stringify(values);
60
+ const reads: Record<string, string> = {};
61
+ for (const name of names) {
62
+ const local = readings[name];
63
+ const successful = successfulFor(name, signature);
64
+ if (!successful?.readId || (local?.identity === identity && local.reading.state === 'refused')) {
65
+ return null;
66
+ }
67
+ reads[name] = successful.readId;
68
+ }
69
+ return reads;
70
+ };
71
+ const reads = readsFor(JSON.parse(inputs));
72
+ const ready = reads !== null;
73
+ const query = chatHost().intelligentUi?.query;
74
+ const subscribe = chatHost().intelligentUi?.subscribe;
75
+ useEffect(() => {
76
+ const scope = state.scope();
77
+ for (const name of Object.keys(block.queries)) {
78
+ const successful = successfulReading(readings[name], initial[name], identity, inputs, defaultInputs);
79
+ if (successful && JSON.stringify(scope[name]) !== JSON.stringify(successful.value)) {
80
+ state.setQuery(name, successful.value, block);
81
+ }
82
+ }
83
+ }, [state, block, readings, identity, inputs, defaultInputs, initial]);
84
+ useEffect(() => {
85
+ if (
86
+ !query ||
87
+ !element ||
88
+ !block.complete ||
89
+ block.catalogVersion !== UI_CATALOG_VERSION ||
90
+ !block.revision ||
91
+ !Object.keys(block.queries).length ||
92
+ context.phase !== 'final'
93
+ ) {
94
+ return;
95
+ }
96
+ let stopped = false;
97
+ let visible = typeof IntersectionObserver === 'undefined';
98
+ let busy = false;
99
+
100
+ let timer: ReturnType<typeof setTimeout> | null = null;
101
+ // The first read of this block reads at once; a changed input waits until it rests.
102
+ const changed = lastRead.current.inputKey !== '' && lastRead.current.inputKey !== inputKey;
103
+ const settledAt = changed ? settleDeadline() : 0;
104
+ const refresh = async () => {
105
+ if (stopped || !visible || !documentVisible() || busy) {
106
+ return;
107
+ }
108
+ const wait = Math.max(
109
+ settledAt - Date.now(),
110
+ lastRead.current.inputKey === inputKey ? UI_HOST_LIMITS.refreshMilliseconds - (Date.now() - lastRead.current.at) : 0
111
+ );
112
+ if (wait > 0) {
113
+ if (timer === null) {
114
+ if (!visible || !documentVisible()) {
115
+ return;
116
+ }
117
+ timer = setTimeout(() => {
118
+ timer = null;
119
+ void refresh();
120
+ }, wait);
121
+ }
122
+ return;
123
+ }
124
+ busy = true;
125
+ lastRead.current = { inputKey, at: Date.now() };
126
+ setBusyInput(inputKey);
127
+ for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
128
+ if (!visible || !documentVisible()) {
129
+ break;
130
+ }
131
+ try {
132
+ const reading = await query(context.scopeId, {
133
+ chatId: context.chatId,
134
+ itemId: context.itemId,
135
+ blockId: block.id,
136
+ revision: block.revision!,
137
+ query: name,
138
+ values: JSON.parse(inputs)
139
+ });
140
+ if (stopped) {
141
+ return;
142
+ }
143
+ if (reading.state === 'fresh') {
144
+ state.setQuery(name, reading.value, block);
145
+ }
146
+ setReadings((previous) => ({
147
+ ...previous,
148
+ [name]: {
149
+ identity,
150
+ reading,
151
+ successful:
152
+ reading.state === 'fresh' ? { inputs, reading } : previous[name]?.identity === identity ? previous[name].successful : undefined
153
+ }
154
+ }));
155
+ } catch (error) {
156
+ if (stopped) {
157
+ return;
158
+ }
159
+ setReadings((previous) => ({
160
+ ...previous,
161
+ [name]: {
162
+ identity,
163
+ reading: { state: 'failed', readAt: Date.now(), code: 'unreadable', reason: error instanceof Error ? error.message : undefined },
164
+ successful: previous[name]?.identity === identity ? previous[name].successful : undefined
165
+ }
166
+ }));
167
+ }
168
+ }
169
+ busy = false;
170
+ if (stopped) {
171
+ return;
172
+ }
173
+ setBusyInput(null);
174
+ timer = setTimeout(() => {
175
+ timer = null;
176
+ void refresh();
177
+ }, UI_HOST_LIMITS.refreshMilliseconds);
178
+ };
179
+ const observer =
180
+ typeof IntersectionObserver === 'undefined'
181
+ ? null
182
+ : new IntersectionObserver((entries) => {
183
+ visible = entries.some((entry) => entry.isIntersecting);
184
+ if (visible) {
185
+ void refresh();
186
+ } else if (timer !== null) {
187
+ clearTimeout(timer);
188
+ timer = null;
189
+ }
190
+ });
191
+ if (element) {
192
+ observer?.observe(element);
193
+ }
194
+ const focus = () => {
195
+ void refresh();
196
+ };
197
+ document.addEventListener('visibilitychange', focus);
198
+ const unsubscribe = subscribe?.(context.scopeId, context.chatId, focus);
199
+ void refresh();
200
+ return () => {
201
+ stopped = true;
202
+ // A read cut short by this cleanup never reaches its own reset, which would leave the choices closed.
203
+ setBusyInput((current) => (current === inputKey ? null : current));
204
+ observer?.disconnect();
205
+ unsubscribe?.();
206
+ document.removeEventListener('visibilitychange', focus);
207
+ if (timer !== null) {
208
+ clearTimeout(timer);
209
+ }
210
+ };
211
+ }, [block, state, context.scopeId, context.chatId, context.itemId, context.phase, inputs, query, subscribe, element, identity, inputKey]);
212
+ const currentReadings = Object.fromEntries(
213
+ names.flatMap((name) => {
214
+ const local = readings[name];
215
+ const reading = local?.identity === identity ? local.reading : initial[name];
216
+ return reading ? [[name, reading]] : [];
217
+ })
218
+ );
219
+ const failed = Object.entries(currentReadings).find(([, value]) => value.state !== 'fresh');
220
+ const live: UiLiveStatus | undefined = names.length
221
+ ? {
222
+ state: reading || (!ready && !failed) ? 'reading' : (failed?.[1].state ?? 'fresh'),
223
+ sources: names.map((name) => block.queries[name].source),
224
+ readAt: Object.values(currentReadings)
225
+ .filter((reading) => reading.state === 'fresh')
226
+ .reduce<number | null>((at, value) => (at === null ? value.readAt : Math.min(at, value.readAt)), null),
227
+ source: failed ? block.queries[failed[0]]?.source : undefined,
228
+ reason: failed?.[1].reason,
229
+ code: failed?.[1].code
230
+ }
231
+ : undefined;
232
+ return { live, reads: reads ?? {}, reading, ready, readsFor, currentReadings };
233
+ }
234
+
235
+ export function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], readings: Readonly<Record<string, ChatUiQueryReading>>) {
236
+ const [committed, setCommitted] = useState<{ token: string; values: Map<string, Record<string, unknown>>; previous: Map<string, Record<string, unknown>> }>(
237
+ {
238
+ token: '',
239
+ values: new Map(),
240
+ previous: new Map()
241
+ }
242
+ );
243
+ const dependencies = new Map<string, Set<string>>();
244
+ const names = new Set(Object.keys(block.queries));
245
+ const references = (value: unknown, into: Set<string>) => {
246
+ if (!value || typeof value !== 'object') {
247
+ return;
248
+ }
249
+ const expression = value as Record<string, unknown>;
250
+ if (expression.kind === 'reference' && names.has(String(expression.name))) {
251
+ into.add(String(expression.name));
252
+ }
253
+ for (const child of Object.values(expression)) {
254
+ if (Array.isArray(child)) {
255
+ child.forEach((item) => references(item, into));
256
+ } else if (typeof child === 'object') {
257
+ references(child, into);
258
+ }
259
+ }
260
+ };
261
+ const written = (nodes: readonly UiNode[], inherited = new Set<string>()) => {
262
+ for (const node of nodes) {
263
+ const related = new Set(inherited);
264
+ references(node.expressions, related);
265
+ dependencies.set(node.id, related);
266
+ written(node.children, node.type === 'Each' ? related : inherited);
267
+ }
268
+ };
269
+ written(block.nodes);
270
+ const values = new Map<string, Record<string, unknown>>();
271
+ const sources = new Map<string, Set<string>>();
272
+ const collect = (nodes: readonly UiViewNode[]) => {
273
+ for (const node of nodes) {
274
+ const related = dependencies.get(node.sourceId ?? node.id);
275
+ if (related?.size && !node.error) {
276
+ values.set(node.id, node.props);
277
+ sources.set(node.id, related);
278
+ }
279
+ collect(node.children);
280
+ }
281
+ };
282
+ collect(nodes);
283
+ const token = JSON.stringify([block.revision, Object.entries(readings).map(([name, reading]) => [name, reading.state, reading.readId, reading.readAt])]);
284
+ const previous = committed.token === token ? committed.previous : committed.values;
285
+ if (committed.token !== token) {
286
+ setCommitted({ token, values, previous });
287
+ }
288
+ if (!names.size) {
289
+ return undefined;
290
+ }
291
+ return (id: string, prop: string, path?: readonly [number, string]): UiLiveValue | undefined => {
292
+ const related = sources.get(id);
293
+ if (!related) {
294
+ return undefined;
295
+ }
296
+ const get = (props?: Record<string, unknown>): unknown => {
297
+ const value = props?.[prop];
298
+ return path && Array.isArray(value) ? value[path[0]]?.[path[1]] : value;
299
+ };
300
+ const before = get(previous.get(id));
301
+ const value = get(values.get(id));
302
+ return {
303
+ previous: before,
304
+ changed: before !== undefined && JSON.stringify(before) !== JSON.stringify(value),
305
+ stale: [...related].some((name) => readings[name]?.state === 'failed' || readings[name]?.state === 'refused')
306
+ };
307
+ };
308
+ }
@@ -1,4 +1,11 @@
1
1
  import { createContext } from 'react';
2
+ import type { UiReplyTarget } from '../logic/timeline-target';
2
3
  import type { ChatInfo } from '@adecore/agent-contracts';
3
4
 
4
5
  export const ReplyContext = createContext<{ provider: ChatInfo['provider']; chatId?: string } | null>(null);
6
+
7
+ export const UiReplyNavigationContext = createContext<{
8
+ chatId: string;
9
+ reveal(target: UiReplyTarget): void;
10
+ flash: (UiReplyTarget & { nonce: number }) | null;
11
+ } | null>(null);