@theoremjs/react 0.0.0-stage → 0.3.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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -2
  3. package/client/attachment-hover-preview.d.ts +11 -0
  4. package/client/attachment-hover-preview.js +36 -0
  5. package/client/audio-level.d.ts +5 -0
  6. package/client/audio-level.js +24 -0
  7. package/client/composer-attachments.d.ts +30 -0
  8. package/client/composer-attachments.js +33 -0
  9. package/client/composer-drawer.d.ts +19 -0
  10. package/client/composer-drawer.js +15 -0
  11. package/client/composer-hints.d.ts +25 -0
  12. package/client/composer-hints.js +20 -0
  13. package/client/composer-primary.d.ts +16 -0
  14. package/client/composer-primary.js +25 -0
  15. package/client/decision-transport.d.ts +48 -0
  16. package/client/decision-transport.js +102 -0
  17. package/client/decode-composer-draft.d.ts +11 -0
  18. package/client/decode-composer-draft.js +12 -0
  19. package/client/encode-composer-draft.d.ts +9 -0
  20. package/client/encode-composer-draft.js +34 -0
  21. package/client/encode-files.d.ts +18 -0
  22. package/client/encode-files.js +54 -0
  23. package/client/failure.d.ts +32 -0
  24. package/client/failure.js +34 -0
  25. package/client/generation-selection.d.ts +15 -0
  26. package/client/generation-selection.js +24 -0
  27. package/client/host-call.d.ts +51 -0
  28. package/client/host-call.js +42 -0
  29. package/client/host-transport.d.ts +68 -0
  30. package/client/host-transport.js +74 -0
  31. package/client/image-output.d.ts +6 -0
  32. package/client/image-output.js +13 -0
  33. package/client/index.d.ts +20 -0
  34. package/client/index.js +18 -0
  35. package/client/ink-waveform.d.ts +17 -0
  36. package/client/ink-waveform.js +69 -0
  37. package/client/live/apply-live-turn-tool-event.d.ts +14 -0
  38. package/client/live/apply-live-turn-tool-event.js +39 -0
  39. package/client/live/live-captions.d.ts +23 -0
  40. package/client/live/live-captions.js +70 -0
  41. package/client/live/live-mic-forward.d.ts +29 -0
  42. package/client/live/live-mic-forward.js +37 -0
  43. package/client/live/live-state.d.ts +11 -0
  44. package/client/live/live-state.js +20 -0
  45. package/client/live/live-tool.d.ts +11 -0
  46. package/client/live/live-tool.js +1 -0
  47. package/client/live/live-video.d.ts +10 -0
  48. package/client/live/live-video.js +71 -0
  49. package/client/live/run-live-tool-call.d.ts +17 -0
  50. package/client/live/run-live-tool-call.js +32 -0
  51. package/client/live-client.d.ts +141 -0
  52. package/client/live-client.js +638 -0
  53. package/client/live-errors.d.ts +2 -0
  54. package/client/live-errors.js +9 -0
  55. package/client/live-messages.d.ts +110 -0
  56. package/client/live-messages.js +67 -0
  57. package/client/mic-capture.d.ts +4 -0
  58. package/client/mic-capture.js +6 -0
  59. package/client/oauth-popup.d.ts +26 -0
  60. package/client/oauth-popup.js +30 -0
  61. package/client/pcm-downsample.d.ts +4 -0
  62. package/client/pcm-downsample.js +45 -0
  63. package/client/run-commit.d.ts +61 -0
  64. package/client/run-commit.js +124 -0
  65. package/client/run-session.d.ts +80 -0
  66. package/client/run-session.js +215 -0
  67. package/client/schema-fields.d.ts +63 -0
  68. package/client/schema-fields.js +178 -0
  69. package/client/shaped-data.d.ts +118 -0
  70. package/client/shaped-data.js +314 -0
  71. package/client/source-citations.d.ts +13 -0
  72. package/client/source-citations.js +56 -0
  73. package/client/state-fields.d.ts +16 -0
  74. package/client/state-fields.js +44 -0
  75. package/client/tool-result.d.ts +78 -0
  76. package/client/tool-result.js +275 -0
  77. package/client/tool-resume.d.ts +33 -0
  78. package/client/tool-resume.js +11 -0
  79. package/client/trace-feed.d.ts +13 -0
  80. package/client/trace-feed.js +18 -0
  81. package/client/trace-story.d.ts +163 -0
  82. package/client/trace-story.js +416 -0
  83. package/client/trace-view.d.ts +130 -0
  84. package/client/trace-view.js +293 -0
  85. package/client/transcript-groups.d.ts +114 -0
  86. package/client/transcript-groups.js +167 -0
  87. package/client/transcript-scroll.d.ts +7 -0
  88. package/client/transcript-scroll.js +6 -0
  89. package/client/transport.d.ts +176 -0
  90. package/client/transport.js +256 -0
  91. package/client/turn-client.d.ts +35 -0
  92. package/client/turn-client.js +93 -0
  93. package/client/voice-label.d.ts +2 -0
  94. package/client/voice-label.js +15 -0
  95. package/client/voice-recorder.d.ts +27 -0
  96. package/client/voice-recorder.js +165 -0
  97. package/client/walk-away.d.ts +34 -0
  98. package/client/walk-away.js +48 -0
  99. package/client/wire-line.d.ts +56 -0
  100. package/client/wire-line.js +90 -0
  101. package/client/worklets/mic-capture.js +16 -0
  102. package/components/InkWaveform.d.ts +10 -0
  103. package/components/InkWaveform.js +108 -0
  104. package/components/live/use-live-runner-controls.d.ts +48 -0
  105. package/components/live/use-live-runner-controls.js +100 -0
  106. package/components/live/use-live-runner-model.d.ts +45 -0
  107. package/components/live/use-live-runner-model.js +175 -0
  108. package/components/live/use-live-runner-ui.d.ts +65 -0
  109. package/components/live/use-live-runner-ui.js +159 -0
  110. package/components/live/use-live-session-client.d.ts +41 -0
  111. package/components/live/use-live-session-client.js +123 -0
  112. package/components/use-composer-voice.d.ts +28 -0
  113. package/components/use-composer-voice.js +90 -0
  114. package/components/use-editor-selection.d.ts +3 -0
  115. package/components/use-editor-selection.js +16 -0
  116. package/components/use-voice-playback.d.ts +16 -0
  117. package/components/use-voice-playback.js +68 -0
  118. package/hooks/use-described.d.ts +8 -0
  119. package/hooks/use-described.js +23 -0
  120. package/hooks/use-theorem-chat-actions.d.ts +75 -0
  121. package/hooks/use-theorem-chat-actions.js +265 -0
  122. package/hooks/use-theorem-chat-state.d.ts +66 -0
  123. package/hooks/use-theorem-chat-state.js +118 -0
  124. package/hooks/use-theorem-chat.d.ts +63 -0
  125. package/hooks/use-theorem-chat.js +340 -0
  126. package/hooks/use-theorem-decision.d.ts +23 -0
  127. package/hooks/use-theorem-decision.js +75 -0
  128. package/hooks/use-theorem-host.d.ts +44 -0
  129. package/hooks/use-theorem-host.js +117 -0
  130. package/hooks/use-theorem-interface.d.ts +19 -0
  131. package/hooks/use-theorem-interface.js +25 -0
  132. package/hooks/use-trace-records.d.ts +4 -0
  133. package/hooks/use-trace-records.js +7 -0
  134. package/index.d.ts +14 -0
  135. package/index.js +14 -0
  136. package/live.d.ts +8 -0
  137. package/live.js +10 -0
  138. package/package.json +73 -4
  139. package/server/credential-store.d.ts +24 -0
  140. package/server/credential-store.js +14 -0
  141. package/server/decision-handler.d.ts +34 -0
  142. package/server/decision-handler.js +55 -0
  143. package/server/handler.d.ts +114 -0
  144. package/server/handler.js +568 -0
  145. package/server/json-response.d.ts +2 -0
  146. package/server/json-response.js +7 -0
  147. package/server/mod.d.ts +16 -0
  148. package/server/mod.js +15 -0
  149. package/server/request-check.d.ts +16 -0
  150. package/server/request-check.js +34 -0
  151. package/server/session-store.d.ts +74 -0
  152. package/server/session-store.js +49 -0
  153. package/server/steer-inbox.d.ts +35 -0
  154. package/server/steer-inbox.js +52 -0
  155. package/server/walk-away.d.ts +22 -0
  156. package/server/walk-away.js +41 -0
  157. package/styles/ink-controls.css +134 -0
  158. package/ui/ChatComposerBar.d.ts +39 -0
  159. package/ui/ChatComposerBar.js +251 -0
  160. package/ui/ChatTranscript.d.ts +30 -0
  161. package/ui/ChatTranscript.js +474 -0
  162. package/ui/ConsoleFrame.d.ts +47 -0
  163. package/ui/ConsoleFrame.js +46 -0
  164. package/ui/DecisionAnswers.d.ts +15 -0
  165. package/ui/DecisionAnswers.js +102 -0
  166. package/ui/LiveRunner.d.ts +28 -0
  167. package/ui/LiveRunner.js +193 -0
  168. package/ui/SchemaFields.d.ts +10 -0
  169. package/ui/SchemaFields.js +122 -0
  170. package/ui/ShapedData.d.ts +11 -0
  171. package/ui/ShapedData.js +151 -0
  172. package/ui/SidePanel.d.ts +57 -0
  173. package/ui/SidePanel.js +50 -0
  174. package/ui/StateFields.d.ts +36 -0
  175. package/ui/StateFields.js +96 -0
  176. package/ui/TheoremChat.d.ts +71 -0
  177. package/ui/TheoremChat.js +146 -0
  178. package/ui/TheoremDecision.d.ts +38 -0
  179. package/ui/TheoremDecision.js +87 -0
  180. package/ui/TheoremHost.d.ts +38 -0
  181. package/ui/TheoremHost.js +150 -0
  182. package/ui/ToolGateCard.d.ts +28 -0
  183. package/ui/ToolGateCard.js +100 -0
  184. package/ui/ToolResult.d.ts +10 -0
  185. package/ui/ToolResult.js +60 -0
  186. package/ui/ToolResultCharts.d.ts +5 -0
  187. package/ui/ToolResultCharts.js +86 -0
  188. package/ui/TraceCharts.d.ts +13 -0
  189. package/ui/TraceCharts.js +235 -0
  190. package/ui/TraceGuardrails.d.ts +10 -0
  191. package/ui/TraceGuardrails.js +56 -0
  192. package/ui/TraceInspector.d.ts +6 -0
  193. package/ui/TraceInspector.js +189 -0
  194. package/ui/TraceInspectorPanel.d.ts +24 -0
  195. package/ui/TraceInspectorPanel.js +57 -0
  196. package/ui/TraceOverview.d.ts +34 -0
  197. package/ui/TraceOverview.js +96 -0
  198. package/ui/TraceSpanDetail.d.ts +10 -0
  199. package/ui/TraceSpanDetail.js +188 -0
  200. package/ui/TraceStory.d.ts +20 -0
  201. package/ui/TraceStory.js +107 -0
  202. package/ui/TraceValues.d.ts +36 -0
  203. package/ui/TraceValues.js +113 -0
  204. package/ui/TraceWaterfall.d.ts +13 -0
  205. package/ui/TraceWaterfall.js +132 -0
  206. package/ui/VoiceNote.d.ts +12 -0
  207. package/ui/VoiceNote.js +28 -0
  208. package/ui/built/theme.css +774 -0
  209. package/ui/built/theorem.d.ts +13 -0
  210. package/ui/built/theorem.js +631 -0
  211. package/ui/built/theorem.variants.d.ts +16 -0
  212. package/ui/disclosure-motion.d.ts +6 -0
  213. package/ui/disclosure-motion.js +163 -0
  214. package/ui/icons.d.ts +4 -0
  215. package/ui/icons.js +37 -0
  216. package/ui/index.d.ts +25 -0
  217. package/ui/index.js +29 -0
  218. package/ui/labels-provider.d.ts +16 -0
  219. package/ui/labels-provider.js +61 -0
  220. package/ui/labels.d.ts +1153 -0
  221. package/ui/labels.js +603 -0
  222. package/ui/theme.d.ts +10 -0
  223. package/ui/theme.js +13 -0
  224. package/ui/theorem-theme.d.ts +10 -0
  225. package/ui/theorem-theme.js +83 -0
  226. package/ui/trace-palette.d.ts +24 -0
  227. package/ui/trace-palette.js +24 -0
  228. package/ui/trace-patterns.d.ts +32 -0
  229. package/ui/trace-patterns.js +44 -0
  230. package/ui/transcript-copy-text.d.ts +4 -0
  231. package/ui/transcript-copy-text.js +39 -0
@@ -0,0 +1,293 @@
1
+ import { inlineContent, TRACE_ATTRIBUTE_GROUPS, TRACE_FIELDS, TRACE_SPAN_TYPES, TRACE_STATUS, traceAttributeMeta, traceEventMeta, traceSpanMeta, } from '@theoremjs/agents';
2
+ import { isRecord } from '@theoremjs/agents/kernel';
3
+ /** A unix-nanosecond timestamp string as unix milliseconds. */
4
+ export function nanosToMs(nanos) {
5
+ return Number(BigInt(nanos) / 1000n) / 1_000;
6
+ }
7
+ function spanId(traceId, id) {
8
+ return `${traceId}:${id}`;
9
+ }
10
+ function byStart(a, b) {
11
+ return a.startMs - b.startMs;
12
+ }
13
+ /**
14
+ * Every span of `records` as one tree. A span written twice keeps its later
15
+ * copy; a span whose parent has not arrived yet (a Live response before its
16
+ * session root) is a root until the parent does.
17
+ */
18
+ export function traceTree(records) {
19
+ const nodes = new Map();
20
+ for (const record of records) {
21
+ for (const span of record.spans) {
22
+ const startMs = nanosToMs(span.startTimeUnixNano);
23
+ nodes.set(spanId(span.traceId, span.spanId), {
24
+ id: spanId(span.traceId, span.spanId),
25
+ span,
26
+ record,
27
+ meta: traceSpanMeta(span),
28
+ startMs,
29
+ durationMs: nanosToMs(span.endTimeUnixNano) - startMs,
30
+ children: [],
31
+ });
32
+ }
33
+ }
34
+ const roots = [];
35
+ for (const node of nodes.values()) {
36
+ const parent = node.span.parentSpanId ? nodes.get(spanId(node.span.traceId, node.span.parentSpanId)) : undefined;
37
+ (parent ? parent.children : roots).push(node);
38
+ }
39
+ for (const node of nodes.values())
40
+ node.children.sort(byStart);
41
+ return roots.sort(byStart);
42
+ }
43
+ /** Every node of `nodes`, depth first. */
44
+ export function traceSpans(nodes) {
45
+ return nodes.flatMap((node) => [node, ...traceSpans(node.children)]);
46
+ }
47
+ const MODEL_CALL_TYPES = new Set(['call', 'response']);
48
+ function numberAttribute(span, key) {
49
+ const value = span.attributes[key];
50
+ return typeof value === 'number' ? value : undefined;
51
+ }
52
+ function isEstimated(span, side) {
53
+ const estimated = span.attributes['theorem.usage.estimated'];
54
+ return Array.isArray(estimated) && estimated.includes(side);
55
+ }
56
+ function sum(calls, key, partial) {
57
+ const values = calls.map((span) => numberAttribute(span, key));
58
+ const reported = values.filter((value) => value !== undefined);
59
+ if (reported.length === 0)
60
+ return undefined;
61
+ return {
62
+ value: reported.reduce((total, value) => total + value, 0),
63
+ complete: reported.length === calls.length && !calls.some((span) => partial?.(span) === true),
64
+ };
65
+ }
66
+ function tokenSum(calls, key, side) {
67
+ const total = sum(calls, key);
68
+ return total && { ...total, estimated: calls.some((span) => isEstimated(span, side)) };
69
+ }
70
+ export function traceTotals(nodes) {
71
+ const spans = traceSpans(nodes);
72
+ const calls = spans.filter((node) => MODEL_CALL_TYPES.has(node.meta.type)).map((node) => node.span);
73
+ const start = Math.min(...spans.map((node) => node.startMs));
74
+ const end = Math.max(...spans.map((node) => node.startMs + node.durationMs));
75
+ const input = tokenSum(calls, 'gen_ai.usage.input_tokens', 'input');
76
+ const cached = sum(calls, 'gen_ai.usage.cache_read.input_tokens');
77
+ const output = tokenSum(calls, 'gen_ai.usage.output_tokens', 'output');
78
+ const cost = sum(calls, 'theorem.usage.cost_usd', (span) => span.attributes['theorem.usage.cost_partial'] === true);
79
+ return {
80
+ spans: spans.length,
81
+ errors: spans.filter((node) => node.span.status.code === 'ERROR').length,
82
+ durationMs: spans.length === 0 ? 0 : end - start,
83
+ calls: calls.length,
84
+ ...(input && { input }),
85
+ ...(cached && { cached }),
86
+ ...(output && { output }),
87
+ ...(cost && { cost }),
88
+ };
89
+ }
90
+ /** The operators each kind offers, by PowerSearch operator key. */
91
+ export const TRACE_SEARCH_OPERATORS = {
92
+ options: ['isAnyOf', 'isNoneOf'],
93
+ number: ['greaterThan', 'lessThan'],
94
+ text: ['contains', 'notContains'],
95
+ flag: ['isTrue', 'isFalse'],
96
+ };
97
+ /** The field that searches any text a span holds: its name, its values, its stored text. */
98
+ export const TRACE_TEXT_FIELD = 'text';
99
+ /** Span fields beside the attributes. */
100
+ const TYPE_FIELD = 'type';
101
+ const STATUS_FIELD = 'status';
102
+ const DURATION_FIELD = 'duration';
103
+ const EVENTS_FIELD = 'events';
104
+ const NUMBER_FORMATS = new Set(['number', 'tokens', 'seconds', 'milliseconds', 'usd']);
105
+ const TEXT_FORMATS = new Set(['text', 'id', 'list']);
106
+ function optionsOf(seen, known) {
107
+ return [...seen].sort().map((value) => {
108
+ const meta = known[value];
109
+ return meta ? { value, label: meta.label, doc: meta.doc } : { value, label: value };
110
+ });
111
+ }
112
+ function attributeValues(value) {
113
+ return Array.isArray(value) ? value : [value];
114
+ }
115
+ function attributeKind(meta, value) {
116
+ if (meta?.options)
117
+ return 'options';
118
+ const format = meta?.format;
119
+ if (format === 'boolean' || (!meta && typeof value === 'boolean'))
120
+ return 'flag';
121
+ if ((format && NUMBER_FORMATS.has(format)) || (!meta && typeof value === 'number'))
122
+ return 'number';
123
+ if ((format && TEXT_FORMATS.has(format)) || (!meta && typeof value === 'string'))
124
+ return 'text';
125
+ return undefined;
126
+ }
127
+ /**
128
+ * The fields a search over `nodes` offers: type, status, duration and events,
129
+ * then every attribute the spans recorded that compares as a value. Stored
130
+ * text, JSON and structured values are reached by the text field.
131
+ */
132
+ export function traceSearchFields(nodes) {
133
+ const spans = traceSpans(nodes);
134
+ const types = new Set(spans.map((node) => node.meta.type));
135
+ const statuses = new Set(spans.map((node) => node.span.status.code));
136
+ const events = new Set(spans.flatMap((node) => node.span.events.map((event) => event.name)));
137
+ const attributes = new Map();
138
+ for (const node of spans) {
139
+ for (const [key, value] of Object.entries(node.span.attributes)) {
140
+ const kind = attributeKind(traceAttributeMeta(key), value);
141
+ if (!kind)
142
+ continue;
143
+ const field = attributes.get(key) ?? { kind, seen: new Set() };
144
+ if (kind === 'options')
145
+ for (const item of attributeValues(value))
146
+ if (typeof item === 'string')
147
+ field.seen.add(item);
148
+ attributes.set(key, field);
149
+ }
150
+ }
151
+ const eventLabels = Object.fromEntries([...events].flatMap((name) => {
152
+ const meta = traceEventMeta(name);
153
+ return meta ? [[name, meta]] : [];
154
+ }));
155
+ const fields = [
156
+ { key: TYPE_FIELD, ...TRACE_FIELDS.type, kind: 'options', options: optionsOf(types, TRACE_SPAN_TYPES) },
157
+ { key: STATUS_FIELD, ...TRACE_FIELDS.status, kind: 'options', options: optionsOf(statuses, TRACE_STATUS) },
158
+ { key: DURATION_FIELD, ...TRACE_FIELDS.duration, kind: 'number', format: 'milliseconds' },
159
+ ];
160
+ if (events.size > 0)
161
+ fields.push({ key: EVENTS_FIELD, ...TRACE_FIELDS.events, kind: 'options', options: optionsOf(events, eventLabels) });
162
+ for (const [key, { kind, seen }] of [...attributes].sort(([a], [b]) => a.localeCompare(b))) {
163
+ const meta = traceAttributeMeta(key);
164
+ fields.push({
165
+ key,
166
+ label: meta?.label ?? key,
167
+ doc: meta?.doc ?? key,
168
+ kind,
169
+ ...(meta && { group: TRACE_ATTRIBUTE_GROUPS[meta.group].label, format: meta.format }),
170
+ ...(kind === 'options' && { options: optionsOf(seen, meta?.options ?? {}) }),
171
+ });
172
+ }
173
+ return fields;
174
+ }
175
+ /** Every string a span holds: what it is, its name and status, and its attributes and events with stored text inlined. */
176
+ function spanText(node) {
177
+ const strings = [node.meta.label, node.meta.subject ?? '', node.span.name, node.span.status.message ?? ''];
178
+ const collect = (value) => {
179
+ if (typeof value === 'string')
180
+ strings.push(value);
181
+ else if (Array.isArray(value))
182
+ for (const item of value)
183
+ collect(item);
184
+ else if (value && typeof value === 'object')
185
+ for (const nested of Object.values(value))
186
+ collect(nested);
187
+ };
188
+ collect(inlineContent(node.record, { attributes: node.span.attributes, events: node.span.events }));
189
+ for (const event of node.span.events)
190
+ strings.push(traceEventMeta(event.name)?.label ?? '');
191
+ return strings.join('\n').toLocaleLowerCase();
192
+ }
193
+ /** A field's values on one span: strings for options and text, a number, or a yes/no. */
194
+ function fieldValues(node, field) {
195
+ switch (field) {
196
+ case TYPE_FIELD:
197
+ return [node.meta.type];
198
+ case STATUS_FIELD:
199
+ return [node.span.status.code];
200
+ case DURATION_FIELD:
201
+ return [node.durationMs];
202
+ case EVENTS_FIELD:
203
+ return node.span.events.map((event) => event.name);
204
+ default: {
205
+ const value = node.span.attributes[field];
206
+ if (value === undefined)
207
+ return [];
208
+ return attributeValues(value).filter((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean');
209
+ }
210
+ }
211
+ }
212
+ function stringList(value) {
213
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
214
+ }
215
+ function matchesFilter(node, filter) {
216
+ const { value } = filter.value;
217
+ if (filter.field === TRACE_TEXT_FIELD) {
218
+ const found = typeof value === 'string' && spanText(node).includes(value.toLocaleLowerCase());
219
+ return filter.operator === 'notContains' ? !found : found;
220
+ }
221
+ const values = fieldValues(node, filter.field);
222
+ switch (filter.operator) {
223
+ case 'isAnyOf':
224
+ return values.some((item) => typeof item === 'string' && stringList(value).includes(item));
225
+ case 'isNoneOf':
226
+ return !values.some((item) => typeof item === 'string' && stringList(value).includes(item));
227
+ case 'greaterThan':
228
+ return typeof value === 'number' && values.some((item) => typeof item === 'number' && item > value);
229
+ case 'lessThan':
230
+ return typeof value === 'number' && values.some((item) => typeof item === 'number' && item < value);
231
+ case 'contains':
232
+ case 'notContains': {
233
+ const found = typeof value === 'string' && values.some((item) => String(item).toLocaleLowerCase().includes(value.toLocaleLowerCase()));
234
+ return filter.operator === 'contains' ? found : !found;
235
+ }
236
+ case 'isTrue':
237
+ return values.includes(true);
238
+ case 'isFalse':
239
+ return values.includes(false);
240
+ default:
241
+ return false;
242
+ }
243
+ }
244
+ /** Whether a span meets every filter. */
245
+ function traceMatches(node, filters) {
246
+ return filters.every((filter) => matchesFilter(node, filter));
247
+ }
248
+ /**
249
+ * The tree cut to the spans that meet every filter, each kept under its
250
+ * ancestors so it reads in place; `matches` counts the spans that met them.
251
+ */
252
+ export function filterTraceTree(nodes, filters) {
253
+ if (filters.length === 0)
254
+ return { nodes: [...nodes], matches: traceSpans(nodes).length };
255
+ let matches = 0;
256
+ const keep = (node) => {
257
+ const hit = traceMatches(node, filters);
258
+ if (hit)
259
+ matches += 1;
260
+ const children = node.children.flatMap((child) => keep(child) ?? []);
261
+ return hit || children.length > 0 ? { ...node, children } : undefined;
262
+ };
263
+ return { nodes: nodes.flatMap((node) => keep(node) ?? []), matches };
264
+ }
265
+ /** The ids of the spans under `nodes` that meet every filter; undefined when there are none to meet. */
266
+ export function traceMatchIds(nodes, filters) {
267
+ if (filters.length === 0)
268
+ return undefined;
269
+ return new Set(traceSpans(nodes).filter((node) => traceMatches(node, filters)).map((node) => node.id));
270
+ }
271
+ /** Formats whose value is stored text or structure: shown on click, never inline. */
272
+ const STORED_FORMATS = new Set(['content', 'json', 'messages', 'parts']);
273
+ function isStringList(value) {
274
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
275
+ }
276
+ /** An object, or a list of objects, as items to read by a catalog entry's fields. */
277
+ function fieldItems(value) {
278
+ if (isRecord(value))
279
+ return [value];
280
+ return Array.isArray(value) && value.every(isRecord) ? value : undefined;
281
+ }
282
+ export function traceValueShape(meta, value) {
283
+ if (meta && STORED_FORMATS.has(meta.format))
284
+ return { kind: 'stored' };
285
+ if (typeof value === 'string')
286
+ return { kind: 'text', value };
287
+ if (typeof value === 'number' || typeof value === 'boolean')
288
+ return { kind: 'scalar', value };
289
+ if (isStringList(value))
290
+ return { kind: 'list', value };
291
+ const items = meta?.fields ? fieldItems(value) : undefined;
292
+ return meta?.fields && items ? { kind: 'fields', fields: meta.fields, items } : { kind: 'stored' };
293
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Group flat transcript blocks into user / assistant turns for message-shell UI.
3
+ * Tools, reasoning, and mid-turn narration stay inside the assistant turn
4
+ * (Seance-style), not as separate messages.
5
+ */
6
+ import type { TranscriptBlock } from '@theoremjs/agents/interface';
7
+ export type TranscriptTurnGroup = {
8
+ kind: 'user';
9
+ key: string;
10
+ blocks: TranscriptBlock[];
11
+ } | {
12
+ kind: 'assistant';
13
+ key: string;
14
+ blocks: TranscriptBlock[];
15
+ /** The reply's work and when it stopped, from its latest stamped `turn-done`. */
16
+ workedMs?: number;
17
+ endedAt?: number;
18
+ };
19
+ export type TraceItem = {
20
+ kind: 'reasoning';
21
+ id: string;
22
+ text: string;
23
+ } | {
24
+ kind: 'narration';
25
+ id: string;
26
+ text: string;
27
+ } | {
28
+ kind: 'tool';
29
+ id: string;
30
+ block: Extract<TranscriptBlock, {
31
+ kind: 'tool';
32
+ }>;
33
+ };
34
+ export type ComposedAssistantTurn = {
35
+ /** Ordered reasoning / narration / tools for the disclosure. */
36
+ trace: TraceItem[];
37
+ /** Gate tools rendered as interactive cards outside the collapsed list. */
38
+ gatedTools: Extract<TranscriptBlock, {
39
+ kind: 'tool';
40
+ }>[];
41
+ /** Final answer segments (text / media / structured / grounding / evidence / error). */
42
+ body: TranscriptBlock[];
43
+ /** True when the disclosure control should appear. */
44
+ hasTrace: boolean;
45
+ };
46
+ export declare function groupTranscriptBlocks(blocks: readonly TranscriptBlock[]): TranscriptTurnGroup[];
47
+ /** A reply's text to copy; a failure is the message's status, not part of the reply. */
48
+ export declare function assistantTurnCopyText(blocks: readonly TranscriptBlock[]): string;
49
+ /** A turn's work, for the status line: running, or done (with its duration when known). */
50
+ export type WorkStatus = {
51
+ phase: 'working' | 'worked';
52
+ elapsedMs?: number;
53
+ };
54
+ /** Working while streaming; worked after, when it has a duration or a trace; else nothing to show. */
55
+ export declare function workStatus(args: {
56
+ streaming: boolean;
57
+ hasTrace: boolean;
58
+ elapsedMs?: number;
59
+ }): WorkStatus | null;
60
+ /**
61
+ * Split an assistant turn into Seance-style trace + final body.
62
+ *
63
+ * - `thought` → always reasoning in the trace
64
+ * - non-gate `tool` → tool item in the trace
65
+ * - first gate `tool` → interactive card outside the collapsed list; later gates → trace
66
+ * - `text` before/between tools → narration; text and answer kinds after the
67
+ * latest tool → body (while streaming too, so the answer streams formatted)
68
+ * - No tools: thoughts still go to trace; remaining kinds → body
69
+ */
70
+ export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[]): ComposedAssistantTurn;
71
+ /** The trailing user group while its reply has not started streaming back. */
72
+ export declare function pendingPromptOf(groups: readonly TranscriptTurnGroup[]): TranscriptTurnGroup | undefined;
73
+ export type AssistantTurnTiming = {
74
+ /**
75
+ * Keyed by its prompt, not its blocks: the reply stays mounted from the
76
+ * "Working…" placeholder through streaming and commit (which re-keys blocks).
77
+ */
78
+ key: string;
79
+ live: boolean;
80
+ /** While live: when the reply started, its approval waits skipped. */
81
+ startedAt?: number;
82
+ /** Once stopped: how long it worked and when it stopped, as its blocks record. */
83
+ workedMs?: number;
84
+ endedAt?: number;
85
+ };
86
+ /**
87
+ * A turn sent in this session, keyed by its prompt: when it last stopped, and
88
+ * how long it sat paused on approvals, which doesn't count as work.
89
+ */
90
+ export type TurnSpan = {
91
+ endedAt?: number;
92
+ pausedMs: number;
93
+ };
94
+ /**
95
+ * The key of the assistant group at `index`: its prompt's, so the reply keeps
96
+ * one identity from "Working…" through commit. Block ids restart every reply.
97
+ */
98
+ export declare function replyKey(groups: readonly TranscriptTurnGroup[], index: number): string;
99
+ /** The key of the reply to `prompt`, before and after it streams. */
100
+ export declare function promptReplyKey(prompt: TranscriptTurnGroup): string;
101
+ /**
102
+ * Key and timing for the assistant group at `index`. A stopped reply reads its
103
+ * time from its blocks; a live one counts from its span in this session.
104
+ */
105
+ export declare function assistantTurnTiming(args: {
106
+ groups: readonly TranscriptTurnGroup[];
107
+ index: number;
108
+ streaming: boolean;
109
+ timeOf: (key: string) => number;
110
+ spans: ReadonlyMap<string, TurnSpan>;
111
+ }): AssistantTurnTiming;
112
+ export declare function toolCallLabel(tool: Extract<TranscriptBlock, {
113
+ kind: 'tool';
114
+ }>['tool']): string;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Group flat transcript blocks into user / assistant turns for message-shell UI.
3
+ * Tools, reasoning, and mid-turn narration stay inside the assistant turn
4
+ * (Seance-style), not as separate messages.
5
+ */
6
+ import { humanize } from "./shaped-data.js";
7
+ const USER_KINDS = new Set(['user-text', 'user-attachment', 'user-voice']);
8
+ function isUserTranscriptBlock(block) {
9
+ return USER_KINDS.has(block.kind);
10
+ }
11
+ /** Hide bookkeeping from the message body. */
12
+ function isHiddenTranscriptBlock(block) {
13
+ return block.kind === 'turn-done';
14
+ }
15
+ export function groupTranscriptBlocks(blocks) {
16
+ const groups = [];
17
+ for (const block of blocks) {
18
+ if (block.kind === 'turn-done') {
19
+ const last = groups.at(-1);
20
+ if (last?.kind === 'assistant' && block.workedMs !== undefined) {
21
+ last.workedMs = block.workedMs;
22
+ last.endedAt = block.endedAt;
23
+ }
24
+ }
25
+ if (isHiddenTranscriptBlock(block))
26
+ continue;
27
+ const isUser = isUserTranscriptBlock(block);
28
+ const last = groups.at(-1);
29
+ if (last && last.kind === (isUser ? 'user' : 'assistant')) {
30
+ last.blocks.push(block);
31
+ continue;
32
+ }
33
+ groups.push({
34
+ kind: isUser ? 'user' : 'assistant',
35
+ key: block.id,
36
+ blocks: [block],
37
+ });
38
+ }
39
+ return groups;
40
+ }
41
+ /** A reply's text to copy; a failure is the message's status, not part of the reply. */
42
+ export function assistantTurnCopyText(blocks) {
43
+ return blocks
44
+ .filter((block) => block.kind === 'text')
45
+ .map((block) => block.text)
46
+ .filter(Boolean)
47
+ .join('\n\n');
48
+ }
49
+ /** Working while streaming; worked after, when it has a duration or a trace; else nothing to show. */
50
+ export function workStatus(args) {
51
+ const elapsed = args.elapsedMs !== undefined ? { elapsedMs: args.elapsedMs } : {};
52
+ if (args.streaming)
53
+ return { phase: 'working', ...elapsed };
54
+ if (args.elapsedMs !== undefined && args.elapsedMs > 0)
55
+ return { phase: 'worked', elapsedMs: args.elapsedMs };
56
+ return args.hasTrace ? { phase: 'worked' } : null;
57
+ }
58
+ function isGatedTool(block) {
59
+ return block.kind === 'tool' && block.tool.state?.phase === 'gate';
60
+ }
61
+ function lastToolIndexOf(blocks) {
62
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
63
+ if (blocks[i]?.kind === 'tool')
64
+ return i;
65
+ }
66
+ return -1;
67
+ }
68
+ function pushTextBlock(block, args, trace, body) {
69
+ // Text before a tool call is narration; text after the latest one is the
70
+ // answer, streamed in place. If another tool call follows, it becomes narration.
71
+ if (args.hasTools && args.index < args.lastToolIndex) {
72
+ if (block.text.trim()) {
73
+ trace.push({ kind: 'narration', id: block.id, text: block.text });
74
+ }
75
+ return;
76
+ }
77
+ body.push(block);
78
+ }
79
+ function classifyNonGateBlock(block, args, trace, body) {
80
+ if (block.kind === 'thought') {
81
+ if (block.text.trim()) {
82
+ trace.push({ kind: 'reasoning', id: block.id, text: block.text });
83
+ }
84
+ return;
85
+ }
86
+ if (block.kind === 'tool') {
87
+ trace.push({ kind: 'tool', id: block.id, block });
88
+ return;
89
+ }
90
+ if (block.kind === 'text') {
91
+ pushTextBlock(block, args, trace, body);
92
+ return;
93
+ }
94
+ body.push(block);
95
+ }
96
+ /**
97
+ * Split an assistant turn into Seance-style trace + final body.
98
+ *
99
+ * - `thought` → always reasoning in the trace
100
+ * - non-gate `tool` → tool item in the trace
101
+ * - first gate `tool` → interactive card outside the collapsed list; later gates → trace
102
+ * - `text` before/between tools → narration; text and answer kinds after the
103
+ * latest tool → body (while streaming too, so the answer streams formatted)
104
+ * - No tools: thoughts still go to trace; remaining kinds → body
105
+ */
106
+ export function composeAssistantTurn(blocks) {
107
+ const visible = blocks.filter((block) => !isHiddenTranscriptBlock(block));
108
+ // One card at a time: the gate answered next. The step's later gates wait in the trace.
109
+ const nextGate = visible.find(isGatedTool);
110
+ const gatedTools = nextGate ? [nextGate] : [];
111
+ const nonGate = visible.filter((block) => block !== nextGate);
112
+ const lastToolIndex = lastToolIndexOf(nonGate);
113
+ const hasTools = lastToolIndex >= 0;
114
+ const trace = [];
115
+ const body = [];
116
+ for (const [index, block] of nonGate.entries()) {
117
+ classifyNonGateBlock(block, { hasTools, index, lastToolIndex }, trace, body);
118
+ }
119
+ return {
120
+ trace,
121
+ gatedTools,
122
+ body,
123
+ hasTrace: trace.length > 0,
124
+ };
125
+ }
126
+ /** The trailing user group while its reply has not started streaming back. */
127
+ export function pendingPromptOf(groups) {
128
+ const last = groups.at(-1);
129
+ return last?.kind === 'user' ? last : undefined;
130
+ }
131
+ /**
132
+ * The key of the assistant group at `index`: its prompt's, so the reply keeps
133
+ * one identity from "Working…" through commit. Block ids restart every reply.
134
+ */
135
+ export function replyKey(groups, index) {
136
+ const prompt = groups[index - 1];
137
+ return prompt?.kind === 'user' ? promptReplyKey(prompt) : (groups[index]?.key ?? String(index));
138
+ }
139
+ /** The key of the reply to `prompt`, before and after it streams. */
140
+ export function promptReplyKey(prompt) {
141
+ return `${prompt.key}:reply`;
142
+ }
143
+ /**
144
+ * Key and timing for the assistant group at `index`. A stopped reply reads its
145
+ * time from its blocks; a live one counts from its span in this session.
146
+ */
147
+ export function assistantTurnTiming(args) {
148
+ const key = replyKey(args.groups, args.index);
149
+ const live = args.streaming && args.index === args.groups.length - 1;
150
+ const group = args.groups[args.index];
151
+ if (!live) {
152
+ return group?.kind === 'assistant' && group.workedMs !== undefined
153
+ ? { key, live, workedMs: group.workedMs, endedAt: group.endedAt }
154
+ : { key, live };
155
+ }
156
+ const prompt = args.groups[args.index - 1];
157
+ const span = prompt?.kind === 'user' ? args.spans.get(prompt.key) : undefined;
158
+ if (!prompt || !span)
159
+ return { key, live };
160
+ return { key, live, startedAt: args.timeOf(prompt.key) + span.pausedMs };
161
+ }
162
+ export function toolCallLabel(tool) {
163
+ const phase = tool.state?.phase;
164
+ if (phase === 'error' || phase === 'cancel')
165
+ return humanize(tool.name);
166
+ return (phase === 'complete' ? tool.activityPast : tool.activity) ?? humanize(tool.name);
167
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Transcript scroll geometry — pure helpers.
3
+ */
4
+ export declare function resolveScrollToBottomScrollTop(args: {
5
+ scrollHeight: number;
6
+ clientHeight: number;
7
+ }): number;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Transcript scroll geometry — pure helpers.
3
+ */
4
+ export function resolveScrollToBottomScrollTop(args) {
5
+ return Math.max(0, args.scrollHeight - args.clientHeight);
6
+ }