@north-light/crouter 0.3.192 → 0.3.193

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 (167) hide show
  1. package/dist/api/client.d.ts +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +22 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +7 -0
  12. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  17. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  18. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  19. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  20. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  21. package/dist/clients/attach/input/overlay-owner.js +1 -1
  22. package/dist/clients/attach/overlays/auth.js +34 -8
  23. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  24. package/dist/clients/attach/overlays/pickers.js +37 -43
  25. package/dist/clients/attach/viewer.js +643 -620
  26. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  27. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  28. package/dist/clients/inbox/controller.d.ts +2 -3
  29. package/dist/clients/inbox/controller.js +41 -42
  30. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  31. package/dist/clients/inbox/page-adapter.js +54 -0
  32. package/dist/clients/inbox/resolve.d.ts +2 -2
  33. package/dist/clients/inbox/resolve.js +1 -1
  34. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  35. package/dist/clients/inbox/review-visibility.js +1 -1
  36. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  37. package/dist/clients/inbox/tui/ansi.js +2 -2
  38. package/dist/clients/inbox/tui/input.d.ts +1 -3
  39. package/dist/clients/inbox/tui/input.js +242 -553
  40. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  41. package/dist/clients/inbox/tui/panel.js +59 -187
  42. package/dist/clients/inbox/tui/render.d.ts +0 -25
  43. package/dist/clients/inbox/tui/render.js +165 -416
  44. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  45. package/dist/clients/inbox/tui/slots.js +156 -0
  46. package/dist/clients/inbox/tui/types.d.ts +25 -13
  47. package/dist/clients/inbox/tui.js +5 -3
  48. package/dist/commands/attention.js +2 -2
  49. package/dist/commands/human/doc.js +66 -20
  50. package/dist/commands/human/prompts.d.ts +3 -3
  51. package/dist/commands/human/prompts.js +158 -112
  52. package/dist/commands/human/queue.d.ts +0 -1
  53. package/dist/commands/human/queue.js +34 -94
  54. package/dist/commands/human/shared.d.ts +10 -4
  55. package/dist/commands/human/shared.js +62 -13
  56. package/dist/commands/human.js +8 -4
  57. package/dist/commands/memory/read.js +13 -5
  58. package/dist/commands/node/bash.js +15 -28
  59. package/dist/commands/surface/node/focus.js +1 -1
  60. package/dist/core/__tests__/human-deliver.test.js +40 -46
  61. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  62. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  63. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  64. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  65. package/dist/core/auth-interaction.d.ts +2 -2
  66. package/dist/core/bash-jobs.d.ts +32 -0
  67. package/dist/core/bash-jobs.js +72 -1
  68. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  69. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  70. package/dist/core/canvas/attention.js +1 -1
  71. package/dist/core/canvas/browse/app.js +1 -1
  72. package/dist/core/canvas/nav-model.d.ts +1 -1
  73. package/dist/core/canvas/nav-model.js +1 -1
  74. package/dist/core/canvas/remote-canvas-source.js +1 -1
  75. package/dist/core/config.js +4 -1
  76. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  77. package/dist/core/human/__tests__/page-render.test.js +49 -0
  78. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  79. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  80. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  81. package/dist/core/human/__tests__/page.test.js +139 -0
  82. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  83. package/dist/core/human/claim.js +2 -2
  84. package/dist/core/human/convention.d.ts +4 -4
  85. package/dist/core/human/convention.js +6 -13
  86. package/dist/core/human/markdown-html.d.ts +8 -0
  87. package/dist/core/human/markdown-html.js +581 -0
  88. package/dist/core/human/page-catalog.d.ts +6 -0
  89. package/dist/core/human/page-catalog.js +43 -0
  90. package/dist/core/human/page-render.d.ts +3 -0
  91. package/dist/core/human/page-render.js +131 -0
  92. package/dist/core/human/page-schema.d.ts +312 -0
  93. package/dist/core/human/page-schema.js +291 -0
  94. package/dist/core/human/page-synth.d.ts +9 -0
  95. package/dist/core/human/page-synth.js +17 -0
  96. package/dist/core/human/page.d.ts +22 -0
  97. package/dist/core/human/page.js +427 -0
  98. package/dist/core/human/review-schema.d.ts +19 -0
  99. package/dist/core/human/review-schema.js +34 -0
  100. package/dist/core/human/scan.d.ts +12 -3
  101. package/dist/core/human/scan.js +69 -26
  102. package/dist/core/human/summary.d.ts +5 -6
  103. package/dist/core/human/summary.js +54 -37
  104. package/dist/core/human/tickets.d.ts +61 -11
  105. package/dist/core/human/tickets.js +100 -57
  106. package/dist/core/human/types.d.ts +21 -66
  107. package/dist/core/human/types.js +1 -5
  108. package/dist/core/preview-registry.js +0 -1
  109. package/dist/core/review/ticket-filter.d.ts +1 -1
  110. package/dist/core/review/ticket-filter.js +2 -8
  111. package/dist/core/runtime/broker/read-ops.js +3 -0
  112. package/dist/core/runtime/broker.js +19 -10
  113. package/dist/core/runtime/package-health.js +1 -1
  114. package/dist/core/termrender/termrender.d.ts +1 -1
  115. package/dist/core/termrender/termrender.js +2 -2
  116. package/dist/core/tui/page-host.d.ts +1 -1
  117. package/dist/core/tui/page-host.js +2 -2
  118. package/dist/core/user-settings.d.ts +19 -0
  119. package/dist/core/user-settings.js +22 -0
  120. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  121. package/dist/daemon/api/handlers/bash-jobs.js +89 -0
  122. package/dist/daemon/api/handlers/human.js +16 -14
  123. package/dist/daemon/api/handlers/inbox.js +311 -227
  124. package/dist/daemon/api/router.d.ts +5 -1
  125. package/dist/daemon/api/router.js +5 -0
  126. package/dist/daemon/api/server.js +2 -0
  127. package/dist/daemon/cron-run.js +23 -10
  128. package/dist/daemon/human/finish.d.ts +8 -3
  129. package/dist/daemon/human/finish.js +35 -15
  130. package/dist/pages/bundle.css +1 -0
  131. package/dist/pages/bundle.js +1347 -0
  132. package/dist/pages/comments.d.ts +34 -0
  133. package/dist/pages/comments.js +72 -0
  134. package/dist/pages/elements/cards.d.ts +17 -0
  135. package/dist/pages/elements/cards.js +785 -0
  136. package/dist/pages/elements/chart.d.ts +1 -0
  137. package/dist/pages/elements/chart.js +648 -0
  138. package/dist/pages/elements/options.d.ts +21 -0
  139. package/dist/pages/elements/options.js +604 -0
  140. package/dist/pages/elements/pages.d.ts +24 -0
  141. package/dist/pages/elements/pages.js +355 -0
  142. package/dist/pages/elements/slot.d.ts +21 -0
  143. package/dist/pages/elements/slot.js +105 -0
  144. package/dist/pages/elements/table.d.ts +19 -0
  145. package/dist/pages/elements/table.js +906 -0
  146. package/dist/pages/elements/text.d.ts +25 -0
  147. package/dist/pages/elements/text.js +951 -0
  148. package/dist/pages/entry.d.ts +8 -0
  149. package/dist/pages/entry.js +8 -0
  150. package/dist/pages/host.d.ts +132 -0
  151. package/dist/pages/host.js +149 -0
  152. package/dist/pages/register.d.ts +2 -0
  153. package/dist/pages/register.js +3 -0
  154. package/dist/pages/slot-config.d.ts +36 -0
  155. package/dist/pages/slot-config.js +50 -0
  156. package/dist/pages/types.d.ts +98 -0
  157. package/dist/pages/types.js +23 -0
  158. package/dist/types.d.ts +4 -0
  159. package/dist/types.js +2 -0
  160. package/package.json +8 -8
  161. package/runtime.lock.json +156 -101
  162. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  163. package/dist/clients/inbox/deck-adapter.js +0 -63
  164. package/dist/core/human/deck-factories.d.ts +0 -8
  165. package/dist/core/human/deck-factories.js +0 -15
  166. package/dist/core/human/deck-schema.d.ts +0 -70
  167. package/dist/core/human/deck-schema.js +0 -92
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,648 @@
1
+ import { artifactData } from '../host.js';
2
+ import { registerElement } from '../register.js';
3
+ import { readSlotConfig } from '../slot-config.js';
4
+ /** The five chart tokens, cycled when a config declares more than five series. */
5
+ const SERIES_COLORS = [
6
+ 'var(--crtr-chart-1)',
7
+ 'var(--crtr-chart-2)',
8
+ 'var(--crtr-chart-3)',
9
+ 'var(--crtr-chart-4)',
10
+ 'var(--crtr-chart-5)',
11
+ ];
12
+ const SVG_NS = 'http://www.w3.org/2000/svg';
13
+ const HEIGHT = 260;
14
+ const MARGIN_TOP = 12;
15
+ const MARGIN_RIGHT = 12;
16
+ const MARGIN_BOTTOM = 28;
17
+ const Y_TICK_TARGET = 5;
18
+ /** Horizontal room one x tick label needs before labels start colliding. */
19
+ const X_TICK_SPACING = 88;
20
+ /** Rough advance width of one character at tick size, used to keep edge labels in the box. */
21
+ const LABEL_CHAR_WIDTH = 5.8;
22
+ const MAX_X_LABEL_CHARS = 14;
23
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T ]|$)/;
24
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
25
+ const STYLE = `
26
+ :host {
27
+ display: block;
28
+ }
29
+
30
+ [part="container"] {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: var(--crtr-space-2);
34
+ width: 100%;
35
+ }
36
+
37
+ [part="title"] {
38
+ color: var(--crtr-color-text);
39
+ font-weight: 500;
40
+ }
41
+
42
+ [part="legend"] {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ align-items: center;
46
+ gap: var(--crtr-space-1) var(--crtr-space-4);
47
+ color: var(--crtr-color-text-muted);
48
+ font-size: calc(var(--crtr-font-size) * 0.85);
49
+ }
50
+
51
+ [part="legend-item"] {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: var(--crtr-space-1);
55
+ min-width: 0;
56
+ }
57
+
58
+ [part="legend-swatch"] {
59
+ flex: none;
60
+ width: calc(var(--crtr-space-2) - 1px);
61
+ height: calc(var(--crtr-space-2) - 1px);
62
+ border-radius: 2px;
63
+ }
64
+
65
+ [part="legend-label"] {
66
+ overflow: hidden;
67
+ text-overflow: ellipsis;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ [part="plot"] {
72
+ width: 100%;
73
+ }
74
+
75
+ [part="chart"] {
76
+ display: block;
77
+ overflow: visible;
78
+ }
79
+
80
+ [part="empty"] {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ padding: 0 var(--crtr-space-4);
85
+ color: var(--crtr-color-text-muted);
86
+ border: 1px dashed var(--crtr-color-border);
87
+ border-radius: var(--crtr-radius-medium);
88
+ }
89
+
90
+ [part="status"] {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ padding: 0 var(--crtr-space-4);
95
+ color: var(--crtr-color-text-muted);
96
+ border: 1px dashed var(--crtr-color-border);
97
+ border-radius: var(--crtr-radius-medium);
98
+ }
99
+
100
+ [part="unavailable"] {
101
+ padding: var(--crtr-space-3);
102
+ color: var(--crtr-color-text-muted);
103
+ background: var(--crtr-color-surface);
104
+ border: 1px solid var(--crtr-color-border);
105
+ border-radius: var(--crtr-radius-medium);
106
+ box-shadow: var(--crtr-shadow);
107
+ }
108
+
109
+ [part="reason"] {
110
+ color: var(--crtr-color-text);
111
+ font-family: var(--crtr-font-mono);
112
+ }
113
+
114
+ .grid {
115
+ stroke: var(--crtr-color-border);
116
+ stroke-width: 1;
117
+ }
118
+
119
+ .tick {
120
+ fill: var(--crtr-color-text-muted);
121
+ font-size: calc(var(--crtr-font-size) * 0.72);
122
+ }
123
+ `;
124
+ function normalizeConfig(raw) {
125
+ const kind = raw.kind;
126
+ if (kind !== 'line' && kind !== 'bar' && kind !== 'area') {
127
+ return { status: 'invalid', reason: 'chart kind must be line, bar or area' };
128
+ }
129
+ const x = raw.x;
130
+ if (typeof x !== 'string' || x === '')
131
+ return { status: 'invalid', reason: 'chart config has no x field' };
132
+ if (!Array.isArray(raw.y) || raw.y.length === 0) {
133
+ return { status: 'invalid', reason: 'chart config has no y series' };
134
+ }
135
+ const y = [];
136
+ for (const [index, entry] of raw.y.entries()) {
137
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
138
+ return { status: 'invalid', reason: `chart series #${index} is not { field, label? }` };
139
+ }
140
+ const definition = entry;
141
+ if (typeof definition.field !== 'string' || definition.field === '') {
142
+ return { status: 'invalid', reason: `chart series #${index} has no field` };
143
+ }
144
+ if (definition.label !== undefined && typeof definition.label !== 'string') {
145
+ return { status: 'invalid', reason: `chart series "${definition.field}" label is not a string` };
146
+ }
147
+ y.push({ field: definition.field, label: definition.label });
148
+ }
149
+ if (raw.title !== undefined && typeof raw.title !== 'string') {
150
+ return { status: 'invalid', reason: 'chart title is not a string' };
151
+ }
152
+ if (raw.source !== undefined && typeof raw.source !== 'string') {
153
+ return { status: 'invalid', reason: 'chart source is not a string' };
154
+ }
155
+ return {
156
+ status: 'ok',
157
+ config: {
158
+ kind,
159
+ x,
160
+ y,
161
+ title: raw.title,
162
+ data: raw.data,
163
+ source: raw.source,
164
+ },
165
+ };
166
+ }
167
+ function isRows(value) {
168
+ return Array.isArray(value) && value.every((row) => row !== null && typeof row === 'object' && !Array.isArray(row));
169
+ }
170
+ function getByPath(row, path) {
171
+ return path.split('.').reduce((current, key) => {
172
+ if (current === null || current === undefined || typeof current !== 'object')
173
+ return undefined;
174
+ return current[key];
175
+ }, row);
176
+ }
177
+ /** Convert a row field name into the human-readable label used by the legend and summary. */
178
+ function titleCaseHeader(field) {
179
+ if (field.includes(' '))
180
+ return field;
181
+ const lastSegment = field.includes('.') ? (field.split('.').pop() ?? field) : field;
182
+ return lastSegment
183
+ .replace(/[_-]+/g, ' ')
184
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
185
+ .split(/\s+/)
186
+ .filter(Boolean)
187
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
188
+ .join(' ');
189
+ }
190
+ /** Coerce a cell into a plottable number, tolerating the string numerics agents emit. */
191
+ function toNumber(value) {
192
+ if (typeof value === 'number')
193
+ return Number.isFinite(value) ? value : null;
194
+ if (typeof value === 'boolean')
195
+ return value ? 1 : 0;
196
+ if (typeof value === 'bigint')
197
+ return Number(value);
198
+ if (typeof value === 'string') {
199
+ const cleaned = value.replace(/[\s,_]/g, '').replace(/^[$£€¥]/, '').replace(/%$/, '');
200
+ if (cleaned === '' || cleaned === '-')
201
+ return null;
202
+ const parsed = Number(cleaned);
203
+ return Number.isFinite(parsed) ? parsed : null;
204
+ }
205
+ return null;
206
+ }
207
+ /** Render an x value as an axis category label. */
208
+ function formatCategory(value) {
209
+ if (value === null || value === undefined)
210
+ return '';
211
+ if (value instanceof Date) {
212
+ return Number.isNaN(value.getTime())
213
+ ? ''
214
+ : value.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
215
+ }
216
+ if (typeof value === 'string') {
217
+ const trimmed = value.trim();
218
+ if (ISO_DATE_RE.test(trimmed)) {
219
+ const parsed = new Date(trimmed);
220
+ if (!Number.isNaN(parsed.getTime())) {
221
+ // A date-only string parses as UTC midnight, so it has to be read back in UTC
222
+ // or every label west of Greenwich names the previous day.
223
+ return parsed.toLocaleDateString(undefined, DATE_ONLY_RE.test(trimmed)
224
+ ? { month: 'short', day: 'numeric', timeZone: 'UTC' }
225
+ : { month: 'short', day: 'numeric' });
226
+ }
227
+ }
228
+ return value;
229
+ }
230
+ if (typeof value === 'object')
231
+ return '';
232
+ return String(value);
233
+ }
234
+ function truncateLabel(label) {
235
+ return label.length > MAX_X_LABEL_CHARS ? `${label.slice(0, MAX_X_LABEL_CHARS - 1)}…` : label;
236
+ }
237
+ /**
238
+ * Axis tick text. `compact` is decided once for the whole axis rather than per
239
+ * value, so one axis never reads `5,000` above `10K`.
240
+ */
241
+ function formatTick(value, step, compact) {
242
+ const decimals = step >= 1 ? 0 : Math.min(4, Math.ceil(-Math.log10(step)));
243
+ return new Intl.NumberFormat(undefined, {
244
+ maximumFractionDigits: decimals,
245
+ notation: compact ? 'compact' : 'standard',
246
+ }).format(value);
247
+ }
248
+ /** Tooltip text — always the full value, never rounded to the axis step. */
249
+ function formatExact(value) {
250
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: 4 }).format(value);
251
+ }
252
+ /** A 1 / 2 / 5 × 10ⁿ tick scale covering [min, max]. */
253
+ function niceScale(min, max, target) {
254
+ let low = min;
255
+ let high = max;
256
+ if (!Number.isFinite(low) || !Number.isFinite(high)) {
257
+ low = 0;
258
+ high = 1;
259
+ }
260
+ if (low === high) {
261
+ const pad = Math.abs(low) > 0 ? Math.abs(low) * 0.1 : 1;
262
+ low -= pad;
263
+ high += pad;
264
+ }
265
+ const rawStep = (high - low) / Math.max(1, target);
266
+ const magnitude = 10 ** Math.floor(Math.log10(rawStep));
267
+ const normalized = rawStep / magnitude;
268
+ const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
269
+ const domainMin = Math.floor(low / step) * step;
270
+ const domainMax = Math.ceil(high / step) * step;
271
+ const ticks = [];
272
+ for (let value = domainMin; value <= domainMax + step / 2; value += step) {
273
+ // Re-round each accumulated step so 0.1 increments do not print as 0.30000000000000004.
274
+ ticks.push(Number(value.toFixed(10)));
275
+ }
276
+ return { ticks, domainMin, domainMax, step };
277
+ }
278
+ /** Split a series into runs of consecutive plottable points, so gaps break the line. */
279
+ function segmentsOf(points) {
280
+ const segments = [];
281
+ let current = [];
282
+ for (const point of points) {
283
+ const previous = current[current.length - 1];
284
+ if (previous === undefined || point.index === previous.index + 1) {
285
+ current.push(point);
286
+ continue;
287
+ }
288
+ segments.push(current);
289
+ current = [point];
290
+ }
291
+ if (current.length > 0)
292
+ segments.push(current);
293
+ return segments;
294
+ }
295
+ function svgNode(name, part) {
296
+ const node = document.createElementNS(SVG_NS, name);
297
+ if (part !== undefined)
298
+ node.setAttribute('part', part);
299
+ return node;
300
+ }
301
+ function setAttributes(node, attributes) {
302
+ for (const [name, value] of Object.entries(attributes))
303
+ node.setAttribute(name, String(value));
304
+ }
305
+ function tooltip(text) {
306
+ const node = svgNode('title');
307
+ node.textContent = text;
308
+ return node;
309
+ }
310
+ class CrtrChartElement extends HTMLElement {
311
+ #container = document.createElement('div');
312
+ #title = document.createElement('div');
313
+ #legend = document.createElement('div');
314
+ #plot = document.createElement('div');
315
+ #config = null;
316
+ #rows = null;
317
+ #observer = null;
318
+ #width = 0;
319
+ #generation = 0;
320
+ constructor() {
321
+ super();
322
+ const root = this.attachShadow({ mode: 'open' });
323
+ const style = document.createElement('style');
324
+ style.textContent = STYLE;
325
+ this.#container.part.add('container');
326
+ this.#title.part.add('title');
327
+ this.#legend.part.add('legend');
328
+ this.#plot.part.add('plot');
329
+ root.append(style, this.#container);
330
+ }
331
+ connectedCallback() {
332
+ const generation = ++this.#generation;
333
+ this.#config = null;
334
+ this.#rows = null;
335
+ const read = readSlotConfig(this);
336
+ if (read.status !== 'ok') {
337
+ this.#renderUnavailable(read.reason);
338
+ return;
339
+ }
340
+ const normalized = normalizeConfig(read.config);
341
+ if (normalized.status !== 'ok') {
342
+ this.#renderUnavailable(normalized.reason);
343
+ return;
344
+ }
345
+ this.#config = normalized.config;
346
+ this.#renderFrame();
347
+ this.#observe();
348
+ const { data, source } = normalized.config;
349
+ if (data !== undefined) {
350
+ if (!isRows(data)) {
351
+ this.#renderUnavailable('chart data is not a list of rows');
352
+ return;
353
+ }
354
+ this.#rows = data;
355
+ this.#renderPlot();
356
+ return;
357
+ }
358
+ if (source === undefined || source === '') {
359
+ this.#rows = [];
360
+ this.#renderPlot();
361
+ return;
362
+ }
363
+ this.#renderStatus('Loading chart data…');
364
+ void this.#resolveSource(source, generation);
365
+ }
366
+ disconnectedCallback() {
367
+ this.#generation += 1;
368
+ this.#observer?.disconnect();
369
+ this.#observer = null;
370
+ }
371
+ /** The host bridge resolves `source`; this element never touches the network. */
372
+ async #resolveSource(source, generation) {
373
+ const result = await artifactData(source);
374
+ if (generation !== this.#generation)
375
+ return;
376
+ if (result.status !== 'ok') {
377
+ this.#renderUnavailable(`chart source "${source}" is ${result.status}: ${result.reason}`);
378
+ return;
379
+ }
380
+ if (!isRows(result.data)) {
381
+ this.#renderUnavailable(`chart source "${source}" is not a list of rows`);
382
+ return;
383
+ }
384
+ this.#rows = result.data;
385
+ this.#renderPlot();
386
+ }
387
+ #observe() {
388
+ this.#observer?.disconnect();
389
+ this.#width = this.#plot.getBoundingClientRect().width;
390
+ const observer = new ResizeObserver((entries) => {
391
+ const next = entries[0]?.contentRect.width ?? 0;
392
+ if (Math.abs(this.#width - next) < 0.5)
393
+ return;
394
+ this.#width = next;
395
+ this.#renderPlot();
396
+ });
397
+ observer.observe(this.#plot);
398
+ this.#observer = observer;
399
+ }
400
+ #renderUnavailable(reason) {
401
+ this.#observer?.disconnect();
402
+ this.#observer = null;
403
+ const box = document.createElement('div');
404
+ box.part.add('unavailable');
405
+ box.setAttribute('role', 'status');
406
+ const detail = document.createElement('span');
407
+ detail.part.add('reason');
408
+ detail.textContent = reason;
409
+ box.append('Chart unavailable: ', detail);
410
+ this.#container.replaceChildren(box);
411
+ }
412
+ /** Holds the plot's height while a source resolves, so the page does not jump. */
413
+ #renderStatus(text) {
414
+ const box = document.createElement('div');
415
+ box.part.add('status');
416
+ box.setAttribute('role', 'status');
417
+ box.style.height = `${HEIGHT}px`;
418
+ box.textContent = text;
419
+ this.#plot.replaceChildren(box);
420
+ }
421
+ /** Title and legend come from the config alone, so they survive a pending source. */
422
+ #renderFrame() {
423
+ const config = this.#config;
424
+ if (config === null)
425
+ return;
426
+ const children = [];
427
+ const title = config.title?.trim();
428
+ if (title !== undefined && title.length > 0) {
429
+ this.#title.textContent = title;
430
+ children.push(this.#title);
431
+ }
432
+ if (config.y.length > 1) {
433
+ this.#legend.replaceChildren(...config.y.map((definition, index) => {
434
+ const item = document.createElement('div');
435
+ item.part.add('legend-item');
436
+ const swatch = document.createElement('span');
437
+ swatch.part.add('legend-swatch');
438
+ swatch.setAttribute('aria-hidden', 'true');
439
+ swatch.style.background = SERIES_COLORS[index % SERIES_COLORS.length] ?? SERIES_COLORS[0];
440
+ const label = document.createElement('span');
441
+ label.part.add('legend-label');
442
+ label.textContent = definition.label ?? titleCaseHeader(definition.field);
443
+ item.append(swatch, label);
444
+ return item;
445
+ }));
446
+ children.push(this.#legend);
447
+ }
448
+ children.push(this.#plot);
449
+ this.#container.replaceChildren(...children);
450
+ }
451
+ #renderPlot() {
452
+ const config = this.#config;
453
+ const rows = this.#rows;
454
+ if (config === null || rows === null)
455
+ return;
456
+ const categories = rows.map((row) => formatCategory(getByPath(row, config.x)));
457
+ const series = config.y.map((definition, seriesIndex) => {
458
+ const points = [];
459
+ rows.forEach((row, index) => {
460
+ const value = toNumber(getByPath(row, definition.field));
461
+ if (value !== null)
462
+ points.push({ index, value });
463
+ });
464
+ return {
465
+ label: definition.label ?? titleCaseHeader(definition.field),
466
+ color: SERIES_COLORS[seriesIndex % SERIES_COLORS.length] ?? SERIES_COLORS[0],
467
+ points,
468
+ };
469
+ });
470
+ // Accumulated rather than spread over `Math.min(...values)`, which blows the call
471
+ // stack on a dataset of a few hundred thousand points.
472
+ let plotted = 0;
473
+ let low = 0;
474
+ let high = 0;
475
+ for (const entry of series) {
476
+ for (const point of entry.points) {
477
+ if (plotted === 0) {
478
+ low = point.value;
479
+ high = point.value;
480
+ }
481
+ else {
482
+ if (point.value < low)
483
+ low = point.value;
484
+ if (point.value > high)
485
+ high = point.value;
486
+ }
487
+ plotted += 1;
488
+ }
489
+ }
490
+ // Bars and areas are read against a baseline, so their domain must contain zero.
491
+ if (config.kind !== 'line') {
492
+ low = Math.min(low, 0);
493
+ high = Math.max(high, 0);
494
+ }
495
+ if (plotted === 0 || rows.length === 0) {
496
+ const empty = document.createElement('div');
497
+ empty.part.add('empty');
498
+ empty.style.height = `${HEIGHT}px`;
499
+ empty.textContent = 'No data to chart';
500
+ this.#plot.replaceChildren(empty);
501
+ return;
502
+ }
503
+ const width = this.#width;
504
+ if (width <= 0) {
505
+ // First paint, before the container has been measured: hold the height so
506
+ // the surrounding document does not reflow when the SVG arrives.
507
+ const spacer = document.createElement('div');
508
+ spacer.style.height = `${HEIGHT}px`;
509
+ this.#plot.replaceChildren(spacer);
510
+ return;
511
+ }
512
+ const { ticks, domainMin, domainMax, step } = niceScale(low, high, Y_TICK_TARGET);
513
+ const compactTicks = Math.max(...ticks.map((tick) => Math.abs(tick))) >= 10_000;
514
+ const tickLabels = ticks.map((tick) => formatTick(tick, step, compactTicks));
515
+ const marginLeft = Math.min(88, Math.max(36, Math.max(...tickLabels.map((label) => label.length)) * 6.4 + 12));
516
+ const plotWidth = Math.max(0, width - marginLeft - MARGIN_RIGHT);
517
+ const plotHeight = Math.max(0, HEIGHT - MARGIN_TOP - MARGIN_BOTTOM);
518
+ const plotBottom = MARGIN_TOP + plotHeight;
519
+ const band = categories.length > 0 ? plotWidth / categories.length : plotWidth;
520
+ const xOf = (index) => marginLeft + band * (index + 0.5);
521
+ const span = domainMax - domainMin || 1;
522
+ const yOf = (value) => plotBottom - ((value - domainMin) / span) * plotHeight;
523
+ const baselineValue = Math.min(Math.max(0, domainMin), domainMax);
524
+ const baselineY = yOf(baselineValue);
525
+ const svg = svgNode('svg', 'chart');
526
+ setAttributes(svg, { role: 'img', width, height: HEIGHT, viewBox: `0 0 ${width} ${HEIGHT}` });
527
+ svg.setAttribute('aria-label', `${config.kind} chart of ${series.map((entry) => entry.label).join(', ')} by ${titleCaseHeader(config.x)}`);
528
+ ticks.forEach((tick, tickIndex) => {
529
+ const line = svgNode('line');
530
+ line.setAttribute('class', 'grid');
531
+ setAttributes(line, {
532
+ x1: marginLeft,
533
+ x2: marginLeft + plotWidth,
534
+ y1: yOf(tick),
535
+ y2: yOf(tick),
536
+ opacity: tick === baselineValue ? 0.9 : 0.45,
537
+ });
538
+ const label = svgNode('text');
539
+ label.setAttribute('class', 'tick');
540
+ setAttributes(label, {
541
+ x: marginLeft - 8,
542
+ y: yOf(tick),
543
+ 'text-anchor': 'end',
544
+ 'dominant-baseline': 'central',
545
+ });
546
+ label.textContent = tickLabels[tickIndex] ?? '';
547
+ svg.append(line, label);
548
+ });
549
+ // Label a whole-number stride of categories so the gaps stay even, then pin the
550
+ // last one — replacing rather than appending when it would crowd its neighbour.
551
+ const xTickIndices = [];
552
+ if (categories.length > 0) {
553
+ const maxTicks = Math.max(2, Math.min(categories.length, Math.floor(plotWidth / X_TICK_SPACING) || 1));
554
+ const stride = Math.max(1, Math.ceil((categories.length - 1) / (maxTicks - 1)));
555
+ for (let index = 0; index < categories.length; index += stride)
556
+ xTickIndices.push(index);
557
+ const last = xTickIndices[xTickIndices.length - 1] ?? 0;
558
+ if (last !== categories.length - 1) {
559
+ if ((categories.length - 1 - last) * band >= X_TICK_SPACING)
560
+ xTickIndices.push(categories.length - 1);
561
+ else
562
+ xTickIndices[xTickIndices.length - 1] = categories.length - 1;
563
+ }
564
+ }
565
+ for (const index of xTickIndices) {
566
+ const text = truncateLabel(categories[index] ?? '');
567
+ const center = xOf(index);
568
+ const half = (text.length * LABEL_CHAR_WIDTH) / 2;
569
+ const anchor = center - half < 0 ? 'start' : center + half > width ? 'end' : 'middle';
570
+ const label = svgNode('text');
571
+ label.setAttribute('class', 'tick');
572
+ setAttributes(label, {
573
+ x: anchor === 'start' ? 0 : anchor === 'end' ? width : center,
574
+ y: plotBottom + 16,
575
+ 'text-anchor': anchor,
576
+ 'dominant-baseline': 'hanging',
577
+ });
578
+ label.textContent = text;
579
+ svg.append(label);
580
+ }
581
+ if (config.kind === 'bar') {
582
+ const groupWidth = band * 0.72;
583
+ const slot = groupWidth / series.length;
584
+ const barWidth = Math.max(1, slot - (slot > 4 ? 1 : 0));
585
+ series.forEach((entry, seriesIndex) => {
586
+ const group = svgNode('g');
587
+ for (const point of entry.points) {
588
+ const y = yOf(point.value);
589
+ const bar = svgNode('rect', 'bar');
590
+ setAttributes(bar, {
591
+ x: xOf(point.index) - groupWidth / 2 + seriesIndex * slot,
592
+ y: Math.min(y, baselineY),
593
+ width: barWidth,
594
+ height: Math.max(1, Math.abs(baselineY - y)),
595
+ rx: Math.min(2, barWidth / 3),
596
+ });
597
+ bar.style.fill = entry.color;
598
+ bar.append(tooltip(`${entry.label} · ${categories[point.index] ?? point.index}: ${formatExact(point.value)}`));
599
+ group.append(bar);
600
+ }
601
+ svg.append(group);
602
+ });
603
+ }
604
+ else {
605
+ const showDots = categories.length <= 40;
606
+ for (const entry of series) {
607
+ const group = svgNode('g');
608
+ const segments = segmentsOf(entry.points);
609
+ for (const segment of segments) {
610
+ const trace = segment.map((point) => `${xOf(point.index)},${yOf(point.value)}`).join(' L ');
611
+ const first = segment[0];
612
+ const last = segment[segment.length - 1];
613
+ if (config.kind === 'area' && first !== undefined && last !== undefined) {
614
+ const area = svgNode('path', 'area');
615
+ setAttributes(area, {
616
+ d: `M ${xOf(first.index)},${baselineY} L ${trace} L ${xOf(last.index)},${baselineY} Z`,
617
+ 'fill-opacity': 0.18,
618
+ });
619
+ area.style.fill = entry.color;
620
+ group.append(area);
621
+ }
622
+ const line = svgNode('path', 'line');
623
+ setAttributes(line, {
624
+ d: `M ${trace}`,
625
+ fill: 'none',
626
+ 'stroke-width': 2,
627
+ 'stroke-linecap': 'round',
628
+ 'stroke-linejoin': 'round',
629
+ });
630
+ line.style.stroke = entry.color;
631
+ group.append(line);
632
+ if (!showDots && segment.length > 1)
633
+ continue;
634
+ for (const point of segment) {
635
+ const dot = svgNode('circle', 'point');
636
+ setAttributes(dot, { cx: xOf(point.index), cy: yOf(point.value), r: 2.5 });
637
+ dot.style.fill = entry.color;
638
+ dot.append(tooltip(`${entry.label} · ${categories[point.index] ?? point.index}: ${formatExact(point.value)}`));
639
+ group.append(dot);
640
+ }
641
+ }
642
+ svg.append(group);
643
+ }
644
+ }
645
+ this.#plot.replaceChildren(svg);
646
+ }
647
+ }
648
+ registerElement('crtr-chart', CrtrChartElement);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `<crtr-options>` — the agent asks a multiple-choice question.
3
+ *
4
+ * A port of the interaction kit's `agent-options` (Northlight
5
+ * `apps/gateway/components/agent-ui/agent-options.tsx` @ `3705fe9e2`) onto a dependency-free
6
+ * custom element, keeping its two load-bearing decisions:
7
+ *
8
+ * - **The control's silhouette tells the truth about how many answers are wanted.**
9
+ * Single-select is a radio group, multi-select is checkbox rows, so the element never has
10
+ * to say "pick one" in prose.
11
+ * - **"This one, but…" is a first-class answer.** Every option carries the comment
12
+ * affordance in place, rather than making the user squeeze a caveat into the freetext box
13
+ * at the bottom. The affordance is quiet until you reach for it — it reveals on hover, on
14
+ * keyboard focus, and on the `j`/`k` cursor — and stays lit once a comment exists, which
15
+ * is the whole "there is a note here" signal at a glance.
16
+ *
17
+ * Selection is kept in the agent's authored order, never click order, so the response reads
18
+ * the way the question did. `freetext` is absent rather than empty: the response says whether
19
+ * the user wrote something, not whether the box existed.
20
+ */
21
+ export {};