@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,906 @@
1
+ /**
2
+ * `<crtr-table>` — a dataset the agent wants the user to pick from.
3
+ *
4
+ * Rows and columns are independently selectable, single or multi, because the two questions
5
+ * an agent asks of a table are "which of these records" and "which of these fields", and they
6
+ * are not the same question. Row selection wears a real checkbox or radio in a leading cell;
7
+ * column selection turns the header into a pressed toggle, since a column has no natural row
8
+ * to hang a checkbox on and the header IS the column.
9
+ *
10
+ * Anchored comments use the same one-per-anchor algebra as every other element: a row anchor
11
+ * in each row's trailing cell, a column anchor beside each header label, and a `whole` anchor
12
+ * in the trailing header cell for the table itself. Triggers stay invisible until the row is
13
+ * hovered or focused, or until a comment is actually there.
14
+ *
15
+ * Ported from the agent interaction kit's `agent-table.tsx` (northlight `3705fe9e2`), with the
16
+ * kit's `classic-list-keys` grammar — j/k move, x selects, c comments — reimplemented here
17
+ * over the shadow root.
18
+ */
19
+ import { registerElement } from '../register.js';
20
+ import { readSlotConfig, readSlotId } from '../slot-config.js';
21
+ import { artifactData, respond } from '../host.js';
22
+ import { anchorLabel, findComment, setComment } from '../comments.js';
23
+ const COMMENT_BUBBLE = 'M3.25 3.5h9.5A1.75 1.75 0 0 1 14.5 5.25v5A1.75 1.75 0 0 1 12.75 12H7.4l-3.15 2.36A.5.5 0 0 1 3.5 14v-2H3.25A1.75 1.75 0 0 1 1.5 10.25v-5A1.75 1.75 0 0 1 3.25 3.5Z';
24
+ const STYLE = `
25
+ :host {
26
+ display: block;
27
+ }
28
+
29
+ [part="container"] {
30
+ position: relative;
31
+ background: var(--crtr-color-background);
32
+ border: 1px solid var(--crtr-color-border);
33
+ border-radius: var(--crtr-radius-medium);
34
+ box-shadow: var(--crtr-shadow);
35
+ overflow: hidden;
36
+ }
37
+
38
+ [part="container"]:focus-visible {
39
+ outline: 2px solid var(--crtr-color-focus);
40
+ outline-offset: 2px;
41
+ }
42
+
43
+ [part="table"] {
44
+ width: 100%;
45
+ border-collapse: collapse;
46
+ font: inherit;
47
+ color: var(--crtr-color-text);
48
+ }
49
+
50
+ [part="caption"] {
51
+ caption-side: top;
52
+ padding: var(--crtr-space-3) var(--crtr-space-3) var(--crtr-space-2);
53
+ text-align: left;
54
+ color: var(--crtr-color-text-muted);
55
+ }
56
+
57
+ th {
58
+ padding: var(--crtr-space-2) var(--crtr-space-3);
59
+ text-align: left;
60
+ font-weight: 600;
61
+ color: var(--crtr-color-text-muted);
62
+ border-bottom: 1px solid var(--crtr-color-border);
63
+ white-space: nowrap;
64
+ }
65
+
66
+ th[data-align="right"] {
67
+ text-align: right;
68
+ }
69
+
70
+ [part="header-cell-inner"] {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: var(--crtr-space-1);
74
+ }
75
+
76
+ th[data-align="right"] [part="header-cell-inner"] {
77
+ flex-direction: row-reverse;
78
+ }
79
+
80
+ [part="column-toggle"] {
81
+ font: inherit;
82
+ font-weight: 600;
83
+ color: inherit;
84
+ background: transparent;
85
+ border: 1px solid transparent;
86
+ border-radius: var(--crtr-radius-small);
87
+ padding: var(--crtr-space-1) var(--crtr-space-2);
88
+ margin: calc(var(--crtr-space-1) * -1) calc(var(--crtr-space-2) * -1);
89
+ cursor: pointer;
90
+ }
91
+
92
+ [part="column-toggle"]:hover {
93
+ background: var(--crtr-color-surface-hover);
94
+ }
95
+
96
+ [part="column-toggle"][aria-pressed="true"] {
97
+ color: var(--crtr-color-text);
98
+ background: color-mix(in srgb, var(--crtr-color-accent) 12%, transparent);
99
+ border-color: color-mix(in srgb, var(--crtr-color-accent) 40%, transparent);
100
+ }
101
+
102
+ [part="column-toggle"]:focus-visible,
103
+ [part="comment-trigger"]:focus-visible,
104
+ [part="row-control"]:focus-visible,
105
+ [part="select-all"]:focus-visible {
106
+ outline: 2px solid var(--crtr-color-focus);
107
+ outline-offset: 1px;
108
+ }
109
+
110
+ [part="row"] {
111
+ border-bottom: 1px solid color-mix(in srgb, var(--crtr-color-border) 60%, transparent);
112
+ }
113
+
114
+ [part="row"]:last-child {
115
+ border-bottom: none;
116
+ }
117
+
118
+ [part="row"]:hover,
119
+ [part="row"][data-active="true"] {
120
+ background: var(--crtr-color-surface-hover);
121
+ }
122
+
123
+ [part="row"][data-state="selected"],
124
+ [part="row"][data-state="selected"]:hover {
125
+ background: color-mix(in srgb, var(--crtr-color-accent) 8%, transparent);
126
+ }
127
+
128
+ [part="row"]:focus-visible {
129
+ outline: 2px solid var(--crtr-color-focus);
130
+ outline-offset: -2px;
131
+ }
132
+
133
+ td {
134
+ padding: var(--crtr-space-2) var(--crtr-space-3);
135
+ vertical-align: middle;
136
+ }
137
+
138
+ td[data-align="right"] {
139
+ text-align: right;
140
+ font-variant-numeric: tabular-nums;
141
+ }
142
+
143
+ td[data-mono="true"] {
144
+ font-family: var(--crtr-font-mono);
145
+ font-size: 0.8125em;
146
+ }
147
+
148
+ [data-column-selected="true"] {
149
+ background: color-mix(in srgb, var(--crtr-color-accent) 6%, transparent);
150
+ }
151
+
152
+ [part="select-cell"],
153
+ [part="select-head"] {
154
+ width: 1px;
155
+ padding-right: 0;
156
+ }
157
+
158
+ [part="comment-cell"],
159
+ [part="comment-head"] {
160
+ width: 1px;
161
+ text-align: right;
162
+ padding-left: 0;
163
+ }
164
+
165
+ [part="row-control"],
166
+ [part="select-all"] {
167
+ accent-color: var(--crtr-color-accent);
168
+ margin: 0;
169
+ cursor: pointer;
170
+ }
171
+
172
+ [part="comment-trigger"] {
173
+ display: inline-flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ width: 1.5rem;
177
+ height: 1.5rem;
178
+ padding: 0;
179
+ color: var(--crtr-color-text-muted);
180
+ background: transparent;
181
+ border: none;
182
+ border-radius: var(--crtr-radius-small);
183
+ cursor: pointer;
184
+ opacity: 0;
185
+ transition: opacity 120ms ease;
186
+ }
187
+
188
+ [part="comment-trigger"] svg {
189
+ width: 0.875rem;
190
+ height: 0.875rem;
191
+ }
192
+
193
+ [part="comment-trigger"][data-has-comment="true"] {
194
+ color: var(--crtr-color-accent);
195
+ opacity: 1;
196
+ }
197
+
198
+ [part="comment-trigger"]:hover {
199
+ background: var(--crtr-color-surface-hover);
200
+ }
201
+
202
+ tr:hover [part="comment-trigger"],
203
+ tr:focus-within [part="comment-trigger"],
204
+ tr[data-active="true"] [part="comment-trigger"],
205
+ [part="comment-trigger"]:focus-visible,
206
+ [part="comment-trigger"][data-open="true"] {
207
+ opacity: 1;
208
+ }
209
+
210
+ [part="comment-editor"] {
211
+ position: absolute;
212
+ z-index: 2;
213
+ width: min(20rem, calc(100% - var(--crtr-space-4)));
214
+ display: grid;
215
+ gap: var(--crtr-space-2);
216
+ padding: var(--crtr-space-3);
217
+ background: var(--crtr-color-surface);
218
+ border: 1px solid var(--crtr-color-border);
219
+ border-radius: var(--crtr-radius-medium);
220
+ box-shadow: var(--crtr-shadow);
221
+ }
222
+
223
+ [part="comment-editor"][hidden] {
224
+ display: none;
225
+ }
226
+
227
+ [part="comment-target"] {
228
+ margin: 0;
229
+ font-family: var(--crtr-font-mono);
230
+ font-size: 0.75em;
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.06em;
233
+ color: var(--crtr-color-text-muted);
234
+ }
235
+
236
+ [part="comment-input"] {
237
+ font: inherit;
238
+ color: var(--crtr-color-text);
239
+ background: var(--crtr-color-background);
240
+ border: 1px solid var(--crtr-color-border);
241
+ border-radius: var(--crtr-radius-small);
242
+ padding: var(--crtr-space-2);
243
+ min-height: 4rem;
244
+ resize: vertical;
245
+ }
246
+
247
+ [part="comment-input"]:focus-visible {
248
+ outline: 2px solid var(--crtr-color-focus);
249
+ outline-offset: -1px;
250
+ }
251
+
252
+ [part="comment-actions"] {
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: space-between;
256
+ gap: var(--crtr-space-2);
257
+ }
258
+
259
+ [part="comment-hint"] {
260
+ font-family: var(--crtr-font-mono);
261
+ font-size: 0.75em;
262
+ text-transform: uppercase;
263
+ letter-spacing: 0.06em;
264
+ color: var(--crtr-color-text-muted);
265
+ }
266
+
267
+ [part="comment-cancel"],
268
+ [part="comment-save"] {
269
+ font: inherit;
270
+ font-size: 0.875em;
271
+ padding: var(--crtr-space-1) var(--crtr-space-2);
272
+ border-radius: var(--crtr-radius-small);
273
+ border: 1px solid var(--crtr-color-border);
274
+ cursor: pointer;
275
+ }
276
+
277
+ [part="comment-cancel"] {
278
+ color: var(--crtr-color-text-muted);
279
+ background: transparent;
280
+ }
281
+
282
+ [part="comment-save"] {
283
+ color: var(--crtr-color-accent-contrast);
284
+ background: var(--crtr-color-accent);
285
+ border-color: var(--crtr-color-accent);
286
+ }
287
+
288
+ [part="comment-cancel"]:focus-visible,
289
+ [part="comment-save"]:focus-visible {
290
+ outline: 2px solid var(--crtr-color-focus);
291
+ outline-offset: 1px;
292
+ }
293
+
294
+ [part="status"],
295
+ [part="unavailable"] {
296
+ padding: var(--crtr-space-3);
297
+ color: var(--crtr-color-text-muted);
298
+ background: var(--crtr-color-surface);
299
+ border: 1px solid var(--crtr-color-border);
300
+ border-radius: var(--crtr-radius-medium);
301
+ box-shadow: var(--crtr-shadow);
302
+ }
303
+
304
+ [part="reason"] {
305
+ color: var(--crtr-color-text);
306
+ font-family: var(--crtr-font-mono);
307
+ }
308
+
309
+ [part="empty"] {
310
+ padding: var(--crtr-space-3);
311
+ color: var(--crtr-color-text-muted);
312
+ text-align: center;
313
+ }
314
+ `;
315
+ /** A cell value the schema allows, as the string a table shows. */
316
+ function cellText(value) {
317
+ if (value === undefined || value === null)
318
+ return '';
319
+ return typeof value === 'string' ? value : String(value);
320
+ }
321
+ function isPlainObject(value) {
322
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
323
+ }
324
+ /**
325
+ * The rows inside whatever the host handed back for a `source`. Accepted shapes are the same
326
+ * `{ id, cells }` rows an inline config carries, either as a bare array or under a `rows` key;
327
+ * anything else is a named unavailable state rather than a guess at the author's intent.
328
+ */
329
+ function readSourceRows(data) {
330
+ const raw = Array.isArray(data) ? data : isPlainObject(data) && Array.isArray(data.rows) ? data.rows : undefined;
331
+ if (raw === undefined)
332
+ return { status: 'invalid', reason: 'source data is not a list of rows' };
333
+ const rows = [];
334
+ for (let index = 0; index < raw.length; index += 1) {
335
+ const row = raw[index];
336
+ if (!isPlainObject(row) || typeof row.id !== 'string' || row.id === '' || !isPlainObject(row.cells)) {
337
+ return { status: 'invalid', reason: `source row #${index} is not { id, cells }` };
338
+ }
339
+ const cells = {};
340
+ for (const [key, value] of Object.entries(row.cells)) {
341
+ if (value === undefined)
342
+ continue;
343
+ if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
344
+ return { status: 'invalid', reason: `source row "${row.id}" cell "${key}" is not a string, number, boolean or null` };
345
+ }
346
+ cells[key] = value;
347
+ }
348
+ rows.push({ id: row.id, cells });
349
+ }
350
+ return { status: 'ok', rows };
351
+ }
352
+ function commentIcon() {
353
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
354
+ svg.setAttribute('viewBox', '0 0 16 16');
355
+ svg.setAttribute('aria-hidden', 'true');
356
+ svg.setAttribute('focusable', 'false');
357
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
358
+ path.setAttribute('d', COMMENT_BUBBLE);
359
+ path.setAttribute('fill', 'none');
360
+ path.setAttribute('stroke', 'currentColor');
361
+ path.setAttribute('stroke-width', '1.25');
362
+ path.setAttribute('stroke-linejoin', 'round');
363
+ svg.append(path);
364
+ return svg;
365
+ }
366
+ function isTextEntry(target) {
367
+ if (!(target instanceof HTMLElement))
368
+ return false;
369
+ return target.closest('input[type="text"], textarea, [contenteditable="true"]') !== null;
370
+ }
371
+ class CrtrTableElement extends HTMLElement {
372
+ #root;
373
+ #slotId;
374
+ #config;
375
+ #rows = [];
376
+ #selectedRowIds = [];
377
+ #selectedColumnIds = [];
378
+ #comments = [];
379
+ #activeRowId = null;
380
+ #editorAnchor = null;
381
+ #editorTrigger;
382
+ #connected = false;
383
+ #container;
384
+ #editor;
385
+ #editorTarget;
386
+ #editorInput;
387
+ #editorHint;
388
+ #selectAll;
389
+ #rowParts = new Map();
390
+ #columnParts = new Map();
391
+ #wholeComment;
392
+ constructor() {
393
+ super();
394
+ this.#root = this.attachShadow({ mode: 'open' });
395
+ }
396
+ connectedCallback() {
397
+ this.#connected = true;
398
+ const read = readSlotConfig(this);
399
+ if (read.status !== 'ok') {
400
+ this.#renderUnavailable(read.reason);
401
+ return;
402
+ }
403
+ const config = read.config;
404
+ if (!Array.isArray(config.columns) || config.columns.length === 0) {
405
+ this.#renderUnavailable('table config has no columns');
406
+ return;
407
+ }
408
+ if (config.rows !== undefined && config.source !== undefined) {
409
+ this.#renderUnavailable('table rows and source are mutually exclusive');
410
+ return;
411
+ }
412
+ this.#config = config;
413
+ this.#slotId = readSlotId(this);
414
+ if (config.source !== undefined) {
415
+ this.#renderStatus('Loading table data…');
416
+ void this.#loadSource(config.source);
417
+ return;
418
+ }
419
+ if (!Array.isArray(config.rows)) {
420
+ this.#renderUnavailable('table config has neither rows nor source');
421
+ return;
422
+ }
423
+ this.#rows = config.rows;
424
+ this.#renderTable();
425
+ }
426
+ disconnectedCallback() {
427
+ this.#connected = false;
428
+ }
429
+ async #loadSource(source) {
430
+ const result = await artifactData(source);
431
+ if (!this.#connected)
432
+ return;
433
+ if (result.status !== 'ok') {
434
+ this.#renderUnavailable(`table source "${source}" is ${result.status}: ${result.reason}`);
435
+ return;
436
+ }
437
+ const rows = readSourceRows(result.data);
438
+ if (rows.status !== 'ok') {
439
+ this.#renderUnavailable(`table source "${source}": ${rows.reason}`);
440
+ return;
441
+ }
442
+ this.#rows = rows.rows;
443
+ this.#renderTable();
444
+ }
445
+ // ── the response ───────────────────────────────────────────────────────────────────────
446
+ get #rowSelect() {
447
+ return this.#config?.rowSelect ?? 'none';
448
+ }
449
+ get #columnSelect() {
450
+ return this.#config?.columnSelect ?? 'none';
451
+ }
452
+ #response() {
453
+ return {
454
+ selectedRowIds: [...this.#selectedRowIds],
455
+ selectedColumnIds: [...this.#selectedColumnIds],
456
+ comments: [...this.#comments],
457
+ };
458
+ }
459
+ /** Every user change hands the host this slot's complete current response, never a delta. */
460
+ #commit() {
461
+ this.#syncState();
462
+ if (this.#slotId !== undefined)
463
+ respond(this.#slotId, this.#response());
464
+ }
465
+ /** Selection stays in the order the agent authored, not the order the user clicked. */
466
+ #orderRows(ids) {
467
+ return this.#rows.map((row) => row.id).filter((id) => ids.includes(id));
468
+ }
469
+ #orderColumns(ids) {
470
+ return (this.#config?.columns ?? []).map((column) => column.id).filter((id) => ids.includes(id));
471
+ }
472
+ #setRowSelected(rowId, selected) {
473
+ const mode = this.#rowSelect;
474
+ if (mode === 'none')
475
+ return;
476
+ if (mode === 'single') {
477
+ this.#selectedRowIds = selected ? [rowId] : [];
478
+ }
479
+ else {
480
+ this.#selectedRowIds = this.#orderRows(selected ? [...this.#selectedRowIds, rowId] : this.#selectedRowIds.filter((id) => id !== rowId));
481
+ }
482
+ this.#commit();
483
+ }
484
+ #toggleRow(rowId) {
485
+ if (this.#rowSelect === 'single') {
486
+ this.#setRowSelected(rowId, true);
487
+ return;
488
+ }
489
+ this.#setRowSelected(rowId, !this.#selectedRowIds.includes(rowId));
490
+ }
491
+ #setColumnSelected(columnId, selected) {
492
+ const mode = this.#columnSelect;
493
+ if (mode === 'none')
494
+ return;
495
+ if (mode === 'single') {
496
+ this.#selectedColumnIds = selected ? [columnId] : [];
497
+ }
498
+ else {
499
+ this.#selectedColumnIds = this.#orderColumns(selected ? [...this.#selectedColumnIds, columnId] : this.#selectedColumnIds.filter((id) => id !== columnId));
500
+ }
501
+ this.#commit();
502
+ }
503
+ #toggleColumn(columnId) {
504
+ this.#setColumnSelected(columnId, !this.#selectedColumnIds.includes(columnId));
505
+ }
506
+ #setAllRows(selected) {
507
+ if (this.#rowSelect !== 'multi')
508
+ return;
509
+ this.#selectedRowIds = selected ? this.#rows.map((row) => row.id) : [];
510
+ this.#commit();
511
+ }
512
+ // ── rendering ──────────────────────────────────────────────────────────────────────────
513
+ #reset() {
514
+ this.#root.replaceChildren();
515
+ this.#rowParts.clear();
516
+ this.#columnParts.clear();
517
+ this.#container = undefined;
518
+ this.#editor = undefined;
519
+ this.#editorTrigger = undefined;
520
+ this.#editorAnchor = null;
521
+ this.#selectAll = undefined;
522
+ this.#wholeComment = undefined;
523
+ const style = document.createElement('style');
524
+ style.textContent = STYLE;
525
+ this.#root.append(style);
526
+ }
527
+ #renderUnavailable(reason) {
528
+ this.#reset();
529
+ const box = document.createElement('div');
530
+ box.part.add('unavailable');
531
+ box.setAttribute('role', 'status');
532
+ const detail = document.createElement('span');
533
+ detail.part.add('reason');
534
+ detail.textContent = reason;
535
+ box.append('Table unavailable: ', detail);
536
+ this.#root.append(box);
537
+ }
538
+ #renderStatus(text) {
539
+ this.#reset();
540
+ const box = document.createElement('div');
541
+ box.part.add('status');
542
+ box.setAttribute('role', 'status');
543
+ box.textContent = text;
544
+ this.#root.append(box);
545
+ }
546
+ #renderTable() {
547
+ const config = this.#config;
548
+ if (config === undefined)
549
+ return;
550
+ this.#reset();
551
+ const container = document.createElement('div');
552
+ container.part.add('container');
553
+ container.tabIndex = 0;
554
+ container.addEventListener('keydown', (event) => this.#onKeyDown(event));
555
+ container.addEventListener('focus', () => {
556
+ if (this.#activeRowId === null && this.#rows.length > 0) {
557
+ this.#activeRowId = this.#rows[0].id;
558
+ this.#syncState();
559
+ }
560
+ });
561
+ this.#container = container;
562
+ const table = document.createElement('table');
563
+ table.part.add('table');
564
+ if (config.caption !== undefined && config.caption !== '') {
565
+ const caption = document.createElement('caption');
566
+ caption.part.add('caption');
567
+ caption.textContent = config.caption;
568
+ table.append(caption);
569
+ }
570
+ table.append(this.#buildHead(config), this.#buildBody(config));
571
+ container.append(table);
572
+ if (this.#rows.length === 0) {
573
+ const empty = document.createElement('div');
574
+ empty.part.add('empty');
575
+ empty.textContent = 'No rows.';
576
+ container.append(empty);
577
+ }
578
+ container.append(this.#buildEditor());
579
+ this.#root.append(container);
580
+ this.#syncState();
581
+ }
582
+ #buildHead(config) {
583
+ const head = document.createElement('thead');
584
+ const tr = document.createElement('tr');
585
+ tr.part.add('header-row');
586
+ if (this.#rowSelect !== 'none') {
587
+ const th = document.createElement('th');
588
+ th.part.add('select-head');
589
+ th.setAttribute('scope', 'col');
590
+ if (this.#rowSelect === 'multi') {
591
+ const all = document.createElement('input');
592
+ all.type = 'checkbox';
593
+ all.part.add('select-all');
594
+ all.setAttribute('aria-label', 'Select every row');
595
+ all.addEventListener('change', () => this.#setAllRows(all.checked));
596
+ this.#selectAll = all;
597
+ th.append(all);
598
+ }
599
+ tr.append(th);
600
+ }
601
+ for (const column of config.columns) {
602
+ const th = document.createElement('th');
603
+ th.part.add('column-header');
604
+ th.setAttribute('scope', 'col');
605
+ th.dataset.columnHeader = column.id;
606
+ if (column.align === 'right')
607
+ th.dataset.align = 'right';
608
+ const inner = document.createElement('span');
609
+ inner.part.add('header-cell-inner');
610
+ let toggle;
611
+ if (this.#columnSelect === 'none') {
612
+ const label = document.createElement('span');
613
+ label.textContent = column.label;
614
+ inner.append(label);
615
+ }
616
+ else {
617
+ toggle = document.createElement('button');
618
+ toggle.type = 'button';
619
+ toggle.part.add('column-toggle');
620
+ toggle.dataset.columnToggle = column.id;
621
+ toggle.setAttribute('aria-pressed', 'false');
622
+ toggle.setAttribute('aria-label', `Select the ${column.label} column`);
623
+ toggle.textContent = column.label;
624
+ toggle.addEventListener('click', () => this.#toggleColumn(column.id));
625
+ inner.append(toggle);
626
+ }
627
+ const comment = this.#buildCommentTrigger({ kind: 'column', columnId: column.id }, `the ${column.label} column`);
628
+ inner.append(comment);
629
+ th.append(inner);
630
+ tr.append(th);
631
+ this.#columnParts.set(column.id, { toggle, comment, cells: [], head: th });
632
+ }
633
+ const trailing = document.createElement('th');
634
+ trailing.part.add('comment-head');
635
+ trailing.setAttribute('scope', 'col');
636
+ this.#wholeComment = this.#buildCommentTrigger({ kind: 'whole' }, 'this table');
637
+ trailing.append(this.#wholeComment);
638
+ tr.append(trailing);
639
+ head.append(tr);
640
+ return head;
641
+ }
642
+ #buildBody(config) {
643
+ const body = document.createElement('tbody');
644
+ for (const row of this.#rows) {
645
+ const tr = document.createElement('tr');
646
+ tr.part.add('row');
647
+ tr.dataset.rowId = row.id;
648
+ tr.tabIndex = -1;
649
+ tr.addEventListener('mouseenter', () => this.#setActiveRow(row.id));
650
+ tr.addEventListener('focusin', () => this.#setActiveRow(row.id));
651
+ let control;
652
+ if (this.#rowSelect !== 'none') {
653
+ const td = document.createElement('td');
654
+ td.part.add('select-cell');
655
+ const input = document.createElement('input');
656
+ input.type = this.#rowSelect === 'single' ? 'radio' : 'checkbox';
657
+ input.part.add('row-control');
658
+ if (this.#rowSelect === 'single')
659
+ input.name = `${this.#slotId ?? 'crtr-table'}-row`;
660
+ input.value = row.id;
661
+ input.setAttribute('aria-label', `Select row ${row.id}`);
662
+ input.addEventListener('change', () => this.#setRowSelected(row.id, input.checked));
663
+ control = input;
664
+ td.append(input);
665
+ tr.append(td);
666
+ }
667
+ for (const column of config.columns) {
668
+ const td = document.createElement('td');
669
+ td.part.add('cell');
670
+ td.dataset.columnId = column.id;
671
+ if (column.align === 'right')
672
+ td.dataset.align = 'right';
673
+ if (column.mono === true)
674
+ td.dataset.mono = 'true';
675
+ td.textContent = cellText(row.cells[column.id]);
676
+ tr.append(td);
677
+ this.#columnParts.get(column.id)?.cells.push(td);
678
+ }
679
+ const commentCell = document.createElement('td');
680
+ commentCell.part.add('comment-cell');
681
+ const comment = this.#buildCommentTrigger({ kind: 'row', rowId: row.id }, `row ${row.id}`);
682
+ commentCell.append(comment);
683
+ tr.append(commentCell);
684
+ body.append(tr);
685
+ this.#rowParts.set(row.id, { tr, ...(control === undefined ? {} : { control }), comment });
686
+ }
687
+ return body;
688
+ }
689
+ #buildCommentTrigger(anchor, target) {
690
+ const button = document.createElement('button');
691
+ button.type = 'button';
692
+ button.part.add('comment-trigger');
693
+ button.setAttribute('aria-label', `Comment on ${target}`);
694
+ button.dataset.commentTarget = target;
695
+ button.append(commentIcon());
696
+ button.addEventListener('click', () => this.#openEditor(anchor, button));
697
+ return button;
698
+ }
699
+ #buildEditor() {
700
+ const editor = document.createElement('div');
701
+ editor.part.add('comment-editor');
702
+ editor.hidden = true;
703
+ editor.setAttribute('role', 'dialog');
704
+ editor.setAttribute('aria-label', 'Comment');
705
+ const target = document.createElement('p');
706
+ target.part.add('comment-target');
707
+ editor.append(target);
708
+ const input = document.createElement('textarea');
709
+ input.part.add('comment-input');
710
+ input.placeholder = 'What should the agent know about this?';
711
+ input.addEventListener('keydown', (event) => {
712
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
713
+ event.preventDefault();
714
+ this.#saveComment();
715
+ }
716
+ else if (event.key === 'Escape') {
717
+ event.preventDefault();
718
+ this.#closeEditor();
719
+ }
720
+ });
721
+ editor.append(input);
722
+ const actions = document.createElement('div');
723
+ actions.part.add('comment-actions');
724
+ const hint = document.createElement('span');
725
+ hint.part.add('comment-hint');
726
+ actions.append(hint);
727
+ const buttons = document.createElement('span');
728
+ const cancel = document.createElement('button');
729
+ cancel.type = 'button';
730
+ cancel.part.add('comment-cancel');
731
+ cancel.textContent = 'Cancel';
732
+ cancel.addEventListener('click', () => this.#closeEditor());
733
+ const save = document.createElement('button');
734
+ save.type = 'button';
735
+ save.part.add('comment-save');
736
+ save.textContent = 'Save';
737
+ save.addEventListener('click', () => this.#saveComment());
738
+ buttons.append(cancel, ' ', save);
739
+ actions.append(buttons);
740
+ editor.append(actions);
741
+ this.#editor = editor;
742
+ this.#editorTarget = target;
743
+ this.#editorInput = input;
744
+ this.#editorHint = hint;
745
+ return editor;
746
+ }
747
+ // ── comments ───────────────────────────────────────────────────────────────────────────
748
+ #openEditor(anchor, trigger) {
749
+ const editor = this.#editor;
750
+ const container = this.#container;
751
+ if (editor === undefined || container === undefined)
752
+ return;
753
+ this.#editorAnchor = anchor;
754
+ this.#editorTrigger = trigger;
755
+ const existing = findComment(this.#comments, anchor);
756
+ // The box is seeded from the saved comment every time it opens, so an abandoned edit
757
+ // never leaks into the next one.
758
+ if (this.#editorInput !== undefined)
759
+ this.#editorInput.value = existing?.text ?? '';
760
+ if (this.#editorTarget !== undefined) {
761
+ this.#editorTarget.textContent = `Comment on ${trigger.dataset.commentTarget ?? anchorLabel(anchor)}`;
762
+ }
763
+ if (this.#editorHint !== undefined)
764
+ this.#editorHint.textContent = existing === undefined ? '⌘↵ to save' : 'empty to delete';
765
+ const bounds = container.getBoundingClientRect();
766
+ const rect = trigger.getBoundingClientRect();
767
+ editor.hidden = false;
768
+ editor.style.top = `${Math.max(0, rect.bottom - bounds.top + 4)}px`;
769
+ editor.style.right = `${Math.max(0, bounds.right - rect.right)}px`;
770
+ this.#syncState();
771
+ this.#editorInput?.focus();
772
+ }
773
+ /**
774
+ * Closing hands focus back to the trigger that opened the editor. Without it the keyboard
775
+ * grammar dies on save: the focused Save button is inside the panel being hidden, so focus
776
+ * falls out of the element entirely and j/k/x/c stop arriving.
777
+ */
778
+ #closeEditor() {
779
+ const trigger = this.#editorTrigger;
780
+ this.#editorAnchor = null;
781
+ this.#editorTrigger = undefined;
782
+ if (this.#editor !== undefined)
783
+ this.#editor.hidden = true;
784
+ this.#syncState();
785
+ trigger?.focus();
786
+ }
787
+ #saveComment() {
788
+ const anchor = this.#editorAnchor;
789
+ if (anchor === null)
790
+ return;
791
+ this.#comments = setComment(this.#comments, anchor, this.#editorInput?.value ?? '');
792
+ this.#closeEditor();
793
+ this.#commit();
794
+ }
795
+ // ── interaction ────────────────────────────────────────────────────────────────────────
796
+ #setActiveRow(rowId) {
797
+ if (this.#activeRowId === rowId)
798
+ return;
799
+ this.#activeRowId = rowId;
800
+ this.#syncState();
801
+ }
802
+ #focusRow(rowId) {
803
+ const parts = this.#rowParts.get(rowId);
804
+ (parts?.control ?? parts?.tr)?.focus();
805
+ }
806
+ /**
807
+ * The kit's classic list grammar: j/k move (wrapping) and carry DOM focus with them, x
808
+ * selects, c comments. A key pressed on a column toggle acts on that column instead, since
809
+ * the header IS the column. Modified keys and anything typed in the comment box pass through.
810
+ */
811
+ #onKeyDown(event) {
812
+ if (event.metaKey || event.ctrlKey || event.altKey)
813
+ return;
814
+ const target = event.composedPath()[0] ?? event.target;
815
+ if (isTextEntry(target))
816
+ return;
817
+ const key = event.key.toLowerCase();
818
+ if (key !== 'j' && key !== 'k' && key !== 'x' && key !== 'c')
819
+ return;
820
+ // Anything focused inside a header cell — the toggle or that column's comment trigger —
821
+ // acts on the column, because the header IS the column.
822
+ const columnId = target instanceof HTMLElement ? target.closest('[data-column-header]')?.dataset.columnHeader : undefined;
823
+ if (columnId !== undefined && (key === 'x' || key === 'c')) {
824
+ event.preventDefault();
825
+ if (key === 'x')
826
+ this.#toggleColumn(columnId);
827
+ else {
828
+ const parts = this.#columnParts.get(columnId);
829
+ if (parts !== undefined)
830
+ this.#openEditor({ kind: 'column', columnId }, parts.comment);
831
+ }
832
+ return;
833
+ }
834
+ const ids = this.#rows.map((row) => row.id);
835
+ if (ids.length === 0)
836
+ return;
837
+ const currentId = this.#activeRowId ?? this.#selectedRowIds[this.#selectedRowIds.length - 1] ?? ids[0];
838
+ if (key === 'j' || key === 'k') {
839
+ event.preventDefault();
840
+ const index = Math.max(0, ids.indexOf(currentId));
841
+ const nextId = ids[(index + (key === 'j' ? 1 : -1) + ids.length) % ids.length];
842
+ this.#setActiveRow(nextId);
843
+ this.#focusRow(nextId);
844
+ return;
845
+ }
846
+ event.preventDefault();
847
+ if (key === 'x') {
848
+ this.#toggleRow(currentId);
849
+ return;
850
+ }
851
+ const parts = this.#rowParts.get(currentId);
852
+ if (parts !== undefined)
853
+ this.#openEditor({ kind: 'row', rowId: currentId }, parts.comment);
854
+ }
855
+ // ── state → DOM ────────────────────────────────────────────────────────────────────────
856
+ #syncCommentTrigger(button, anchor) {
857
+ const comment = findComment(this.#comments, anchor);
858
+ const target = button.dataset.commentTarget ?? anchorLabel(anchor);
859
+ button.dataset.hasComment = comment === undefined ? 'false' : 'true';
860
+ button.setAttribute('aria-label', comment === undefined ? `Comment on ${target}` : `Edit comment on ${target}`);
861
+ button.title = comment?.text ?? '';
862
+ const path = button.querySelector('path');
863
+ path?.setAttribute('fill', comment === undefined ? 'none' : 'currentColor');
864
+ button.dataset.open = this.#isEditorAnchor(anchor) ? 'true' : 'false';
865
+ }
866
+ #isEditorAnchor(anchor) {
867
+ const current = this.#editorAnchor;
868
+ if (current === null || current.kind !== anchor.kind)
869
+ return false;
870
+ if (current.kind === 'row' && anchor.kind === 'row')
871
+ return current.rowId === anchor.rowId;
872
+ if (current.kind === 'column' && anchor.kind === 'column')
873
+ return current.columnId === anchor.columnId;
874
+ return current.kind === 'whole';
875
+ }
876
+ #syncState() {
877
+ for (const [rowId, parts] of this.#rowParts) {
878
+ const selected = this.#selectedRowIds.includes(rowId);
879
+ if (selected)
880
+ parts.tr.dataset.state = 'selected';
881
+ else
882
+ delete parts.tr.dataset.state;
883
+ parts.tr.dataset.active = this.#activeRowId === rowId ? 'true' : 'false';
884
+ parts.tr.setAttribute('aria-selected', selected ? 'true' : 'false');
885
+ if (parts.control !== undefined)
886
+ parts.control.checked = selected;
887
+ this.#syncCommentTrigger(parts.comment, { kind: 'row', rowId });
888
+ }
889
+ for (const [columnId, parts] of this.#columnParts) {
890
+ const selected = this.#selectedColumnIds.includes(columnId);
891
+ parts.toggle?.setAttribute('aria-pressed', selected ? 'true' : 'false');
892
+ parts.head.dataset.columnSelected = selected ? 'true' : 'false';
893
+ for (const cell of parts.cells)
894
+ cell.dataset.columnSelected = selected ? 'true' : 'false';
895
+ this.#syncCommentTrigger(parts.comment, { kind: 'column', columnId });
896
+ }
897
+ if (this.#wholeComment !== undefined)
898
+ this.#syncCommentTrigger(this.#wholeComment, { kind: 'whole' });
899
+ if (this.#selectAll !== undefined) {
900
+ const all = this.#rows.length > 0 && this.#selectedRowIds.length === this.#rows.length;
901
+ this.#selectAll.checked = all;
902
+ this.#selectAll.indeterminate = this.#selectedRowIds.length > 0 && !all;
903
+ }
904
+ }
905
+ }
906
+ registerElement('crtr-table', CrtrTableElement);