@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,604 @@
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
+ import { setComment, findComment } from '../comments.js';
22
+ import { respond } from '../host.js';
23
+ import { registerElement } from '../register.js';
24
+ import { readSlotConfig, readSlotId } from '../slot-config.js';
25
+ const BUBBLE_PLUS = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z"/><path d="M8 5.5v3M6.5 7h3"/></svg>';
26
+ const BUBBLE_TEXT = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z"/><path d="M5 6.25h6M5 8.25h4"/></svg>';
27
+ const STYLE = `
28
+ :host {
29
+ display: block;
30
+ }
31
+
32
+ [part="container"] {
33
+ display: grid;
34
+ gap: var(--crtr-space-3);
35
+ }
36
+
37
+ [part="list"] {
38
+ display: grid;
39
+ gap: var(--crtr-space-2);
40
+ }
41
+
42
+ [part="option"] {
43
+ position: relative;
44
+ display: flex;
45
+ align-items: flex-start;
46
+ gap: var(--crtr-space-3);
47
+ padding: var(--crtr-space-2) var(--crtr-space-3);
48
+ background: var(--crtr-color-surface);
49
+ border: 1px solid var(--crtr-color-border);
50
+ border-radius: var(--crtr-radius-medium);
51
+ transition: background-color 120ms ease, border-color 120ms ease;
52
+ }
53
+
54
+ [part="option"]:hover,
55
+ [part="option"][data-active="true"] {
56
+ background: var(--crtr-color-surface-hover);
57
+ }
58
+
59
+ [part="option"][data-selected="true"] {
60
+ border-color: var(--crtr-color-accent);
61
+ background: color-mix(in srgb, var(--crtr-color-accent) 10%, var(--crtr-color-surface));
62
+ }
63
+
64
+ [part="option-label"] {
65
+ display: flex;
66
+ flex: 1;
67
+ align-items: flex-start;
68
+ gap: var(--crtr-space-3);
69
+ cursor: pointer;
70
+ }
71
+
72
+ [part="option-control"] {
73
+ margin: 0;
74
+ margin-top: 0.15em;
75
+ accent-color: var(--crtr-color-accent);
76
+ }
77
+
78
+ [part="option-control"]:focus-visible {
79
+ outline: 2px solid var(--crtr-color-focus);
80
+ outline-offset: 2px;
81
+ }
82
+
83
+ [part="option-text"] {
84
+ display: grid;
85
+ gap: var(--crtr-space-1);
86
+ }
87
+
88
+ [part="option-title"] {
89
+ font-weight: 500;
90
+ }
91
+
92
+ [part="option-description"] {
93
+ color: var(--crtr-color-text-muted);
94
+ font-size: 0.9em;
95
+ }
96
+
97
+ [part="comment"] {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ flex: none;
102
+ width: 1.5rem;
103
+ height: 1.5rem;
104
+ padding: 0;
105
+ color: var(--crtr-color-text-muted);
106
+ background: transparent;
107
+ border: 1px solid transparent;
108
+ border-radius: var(--crtr-radius-small);
109
+ opacity: 0;
110
+ cursor: pointer;
111
+ transition: opacity 120ms ease, color 120ms ease, background-color 120ms ease;
112
+ }
113
+
114
+ [part="option"]:hover [part="comment"],
115
+ [part="option"][data-active="true"] [part="comment"],
116
+ [part="option"]:focus-within [part="comment"],
117
+ [part="comment"][data-state="commented"],
118
+ [part="comment"][aria-expanded="true"] {
119
+ opacity: 1;
120
+ }
121
+
122
+ [part="comment"]:hover {
123
+ background: var(--crtr-color-surface-hover);
124
+ border-color: var(--crtr-color-border);
125
+ }
126
+
127
+ [part="comment"]:focus-visible {
128
+ outline: 2px solid var(--crtr-color-focus);
129
+ outline-offset: 1px;
130
+ }
131
+
132
+ [part="comment"][data-state="commented"] {
133
+ color: var(--crtr-color-accent);
134
+ }
135
+
136
+ [part="comment"] svg {
137
+ width: 0.9rem;
138
+ height: 0.9rem;
139
+ fill: none;
140
+ stroke: currentColor;
141
+ stroke-width: 1.25;
142
+ stroke-linecap: round;
143
+ stroke-linejoin: round;
144
+ }
145
+
146
+ [part="comment-popover"] {
147
+ position: absolute;
148
+ top: calc(100% + var(--crtr-space-1));
149
+ right: 0;
150
+ z-index: 2;
151
+ display: grid;
152
+ gap: var(--crtr-space-2);
153
+ width: min(20rem, 100%);
154
+ padding: var(--crtr-space-3);
155
+ background: var(--crtr-color-background);
156
+ border: 1px solid var(--crtr-color-border);
157
+ border-radius: var(--crtr-radius-medium);
158
+ box-shadow: var(--crtr-shadow);
159
+ }
160
+
161
+ [part="comment-popover"][hidden] {
162
+ display: none;
163
+ }
164
+
165
+ [part="comment-subject"] {
166
+ margin: 0;
167
+ color: var(--crtr-color-text-muted);
168
+ font-family: var(--crtr-font-mono);
169
+ font-size: 0.75em;
170
+ letter-spacing: 0.06em;
171
+ text-transform: uppercase;
172
+ }
173
+
174
+ [part="comment-input"],
175
+ [part="freetext-input"] {
176
+ width: 100%;
177
+ box-sizing: border-box;
178
+ padding: var(--crtr-space-2);
179
+ color: var(--crtr-color-text);
180
+ font: inherit;
181
+ background: var(--crtr-color-surface);
182
+ border: 1px solid var(--crtr-color-border);
183
+ border-radius: var(--crtr-radius-small);
184
+ resize: vertical;
185
+ }
186
+
187
+ [part="comment-input"] {
188
+ min-height: 4.5rem;
189
+ }
190
+
191
+ [part="freetext-input"] {
192
+ min-height: 4rem;
193
+ }
194
+
195
+ [part="comment-input"]:focus-visible,
196
+ [part="freetext-input"]:focus-visible {
197
+ outline: 2px solid var(--crtr-color-focus);
198
+ outline-offset: -1px;
199
+ }
200
+
201
+ [part="comment-actions"] {
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ gap: var(--crtr-space-2);
206
+ }
207
+
208
+ [part="comment-hint"] {
209
+ color: var(--crtr-color-text-muted);
210
+ font-family: var(--crtr-font-mono);
211
+ font-size: 0.75em;
212
+ letter-spacing: 0.06em;
213
+ text-transform: uppercase;
214
+ }
215
+
216
+ [part="comment-cancel"],
217
+ [part="comment-save"] {
218
+ padding: var(--crtr-space-1) var(--crtr-space-2);
219
+ font: inherit;
220
+ font-size: 0.9em;
221
+ border-radius: var(--crtr-radius-small);
222
+ cursor: pointer;
223
+ }
224
+
225
+ [part="comment-cancel"] {
226
+ color: var(--crtr-color-text-muted);
227
+ background: transparent;
228
+ border: 1px solid transparent;
229
+ }
230
+
231
+ [part="comment-cancel"]:hover {
232
+ color: var(--crtr-color-text);
233
+ background: var(--crtr-color-surface-hover);
234
+ }
235
+
236
+ [part="comment-save"] {
237
+ color: var(--crtr-color-accent-contrast);
238
+ background: var(--crtr-color-accent);
239
+ border: 1px solid var(--crtr-color-accent);
240
+ }
241
+
242
+ [part="comment-cancel"]:focus-visible,
243
+ [part="comment-save"]:focus-visible {
244
+ outline: 2px solid var(--crtr-color-focus);
245
+ outline-offset: 1px;
246
+ }
247
+
248
+ [part="freetext"] {
249
+ display: grid;
250
+ gap: var(--crtr-space-1);
251
+ }
252
+
253
+ [part="freetext-label"] {
254
+ color: var(--crtr-color-text-muted);
255
+ font-size: 0.85em;
256
+ }
257
+
258
+ [part="unavailable"] {
259
+ padding: var(--crtr-space-3);
260
+ color: var(--crtr-color-text-muted);
261
+ background: var(--crtr-color-surface);
262
+ border: 1px solid var(--crtr-color-border);
263
+ border-radius: var(--crtr-radius-medium);
264
+ }
265
+
266
+ [part="reason"] {
267
+ color: var(--crtr-color-text);
268
+ font-family: var(--crtr-font-mono);
269
+ }
270
+ `;
271
+ let groupCounter = 0;
272
+ /**
273
+ * Whether a key belongs to something being written in rather than to the list grammar.
274
+ * The option controls are real radios and checkboxes, so this deliberately admits them —
275
+ * only prose entry (the comment box, the freetext box) swallows `j`/`k`/`x`/`c`.
276
+ */
277
+ function isTextEntry(target) {
278
+ if (target instanceof HTMLTextAreaElement)
279
+ return true;
280
+ if (target instanceof HTMLElement && target.isContentEditable)
281
+ return true;
282
+ if (!(target instanceof HTMLInputElement))
283
+ return false;
284
+ return target.type !== 'checkbox' && target.type !== 'radio';
285
+ }
286
+ class CrtrOptionsElement extends HTMLElement {
287
+ #shadow;
288
+ #config;
289
+ #slotId;
290
+ #rows = new Map();
291
+ #order = [];
292
+ #selected = [];
293
+ #comments = [];
294
+ #freetext;
295
+ #activeId;
296
+ #commentingId;
297
+ #built = false;
298
+ constructor() {
299
+ super();
300
+ this.#shadow = this.attachShadow({ mode: 'open' });
301
+ }
302
+ connectedCallback() {
303
+ if (this.#built)
304
+ return;
305
+ this.#built = true;
306
+ this.#slotId = readSlotId(this);
307
+ const read = readSlotConfig(this);
308
+ if (read.status !== 'ok') {
309
+ this.#renderUnavailable(read.reason);
310
+ return;
311
+ }
312
+ const config = read.config;
313
+ if (!Array.isArray(config.options) || config.options.length === 0) {
314
+ this.#renderUnavailable('options config lists no options');
315
+ return;
316
+ }
317
+ this.#config = config;
318
+ this.#order = config.options.map((option) => option.id);
319
+ this.#render(config);
320
+ this.#sync();
321
+ }
322
+ disconnectedCallback() {
323
+ document.removeEventListener('pointerdown', this.#onDocumentPointerDown, true);
324
+ }
325
+ /** The named state for a config this element cannot render — never a thrown error. */
326
+ #renderUnavailable(reason) {
327
+ const style = document.createElement('style');
328
+ style.textContent = STYLE;
329
+ const container = document.createElement('div');
330
+ container.part.add('unavailable');
331
+ container.setAttribute('role', 'status');
332
+ const detail = document.createElement('span');
333
+ detail.part.add('reason');
334
+ detail.textContent = reason;
335
+ container.append('Options unavailable: ', detail);
336
+ this.#shadow.replaceChildren(style, container);
337
+ }
338
+ #render(config) {
339
+ const style = document.createElement('style');
340
+ style.textContent = STYLE;
341
+ const container = document.createElement('div');
342
+ container.part.add('container');
343
+ const list = document.createElement('div');
344
+ list.part.add('list');
345
+ if (config.mode === 'single')
346
+ list.setAttribute('role', 'radiogroup');
347
+ groupCounter += 1;
348
+ const group = `crtr-options-${groupCounter}`;
349
+ for (const option of config.options)
350
+ list.append(this.#renderOption(config, option, group));
351
+ container.append(list);
352
+ if (config.allowFreetext === true)
353
+ container.append(this.#renderFreetext(config));
354
+ // The grammar lives on the container, so it reaches every row and never escapes the
355
+ // shadow root: `j`/`k` move the cursor, `x` toggles it, `c` comments on it.
356
+ container.addEventListener('keydown', (event) => this.#onKeydown(event));
357
+ this.#shadow.replaceChildren(style, container);
358
+ }
359
+ #renderOption(config, option, group) {
360
+ const root = document.createElement('div');
361
+ root.part.add('option');
362
+ root.dataset.optionId = option.id;
363
+ const label = document.createElement('label');
364
+ label.part.add('option-label');
365
+ const control = document.createElement('input');
366
+ control.part.add('option-control');
367
+ control.type = config.mode === 'single' ? 'radio' : 'checkbox';
368
+ control.name = group;
369
+ control.value = option.id;
370
+ control.addEventListener('change', () => this.#toggle(option.id, control.checked));
371
+ control.addEventListener('focus', () => this.#setActive(option.id));
372
+ const text = document.createElement('span');
373
+ text.part.add('option-text');
374
+ const title = document.createElement('span');
375
+ title.part.add('option-title');
376
+ title.textContent = option.label;
377
+ text.append(title);
378
+ if (option.description !== undefined && option.description !== '') {
379
+ const description = document.createElement('span');
380
+ description.part.add('option-description');
381
+ description.textContent = option.description;
382
+ text.append(description);
383
+ }
384
+ label.append(control, text);
385
+ const comment = document.createElement('button');
386
+ comment.part.add('comment');
387
+ comment.type = 'button';
388
+ comment.setAttribute('aria-haspopup', 'dialog');
389
+ comment.addEventListener('click', () => {
390
+ if (this.#commentingId === option.id)
391
+ this.#closeComment();
392
+ else
393
+ this.#openComment(option.id);
394
+ });
395
+ const popover = document.createElement('div');
396
+ popover.part.add('comment-popover');
397
+ popover.setAttribute('role', 'dialog');
398
+ popover.hidden = true;
399
+ const subject = document.createElement('p');
400
+ subject.part.add('comment-subject');
401
+ subject.textContent = `Comment on ${option.label}`;
402
+ popover.setAttribute('aria-label', subject.textContent);
403
+ const input = document.createElement('textarea');
404
+ input.part.add('comment-input');
405
+ input.placeholder = 'What should the agent know about this?';
406
+ input.addEventListener('keydown', (event) => {
407
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
408
+ event.preventDefault();
409
+ this.#commitComment(option.id);
410
+ return;
411
+ }
412
+ if (event.key === 'Escape') {
413
+ event.preventDefault();
414
+ this.#closeComment();
415
+ }
416
+ });
417
+ const actions = document.createElement('div');
418
+ actions.part.add('comment-actions');
419
+ const hint = document.createElement('span');
420
+ hint.part.add('comment-hint');
421
+ const buttons = document.createElement('span');
422
+ const cancel = document.createElement('button');
423
+ cancel.part.add('comment-cancel');
424
+ cancel.type = 'button';
425
+ cancel.textContent = 'Cancel';
426
+ cancel.addEventListener('click', () => this.#closeComment());
427
+ const save = document.createElement('button');
428
+ save.part.add('comment-save');
429
+ save.type = 'button';
430
+ save.textContent = 'Save';
431
+ save.addEventListener('click', () => this.#commitComment(option.id));
432
+ buttons.append(cancel, save);
433
+ actions.append(hint, buttons);
434
+ popover.append(subject, input, actions);
435
+ root.append(label, comment, popover);
436
+ root.addEventListener('mouseenter', () => this.#setActive(option.id));
437
+ root.addEventListener('mouseleave', () => this.#clearActive(option.id));
438
+ this.#rows.set(option.id, { id: option.id, label: option.label, root, control, comment, popover, input, hint });
439
+ return root;
440
+ }
441
+ #renderFreetext(config) {
442
+ const wrapper = document.createElement('label');
443
+ wrapper.part.add('freetext');
444
+ const label = document.createElement('span');
445
+ label.part.add('freetext-label');
446
+ label.textContent = config.freetextLabel ?? 'Something else';
447
+ const input = document.createElement('textarea');
448
+ input.part.add('freetext-input');
449
+ input.placeholder = config.freetextPlaceholder ?? 'Answer in your own words…';
450
+ input.addEventListener('input', () => {
451
+ // Absent rather than empty — see the note on `freetext` in the response type.
452
+ this.#freetext = input.value === '' ? undefined : input.value;
453
+ this.#emit();
454
+ });
455
+ wrapper.append(label, input);
456
+ return wrapper;
457
+ }
458
+ #onKeydown(event) {
459
+ if (event.metaKey || event.ctrlKey || event.altKey)
460
+ return;
461
+ if (isTextEntry(event.target))
462
+ return;
463
+ const key = event.key.toLowerCase();
464
+ const current = this.#activeId ?? this.#selected.at(-1) ?? this.#order[0];
465
+ if (current === undefined)
466
+ return;
467
+ if (key === 'j' || key === 'k') {
468
+ event.preventDefault();
469
+ const index = Math.max(0, this.#order.indexOf(current));
470
+ const step = key === 'j' ? 1 : -1;
471
+ const next = this.#order[(index + step + this.#order.length) % this.#order.length];
472
+ this.#setActive(next);
473
+ this.#rows.get(next)?.control.focus();
474
+ return;
475
+ }
476
+ if (key === 'x') {
477
+ event.preventDefault();
478
+ const selected = this.#selected.includes(current);
479
+ this.#toggle(current, this.#config?.mode === 'single' ? true : !selected);
480
+ return;
481
+ }
482
+ if (key === 'c') {
483
+ event.preventDefault();
484
+ this.#openComment(current);
485
+ }
486
+ }
487
+ #setActive(optionId) {
488
+ if (this.#activeId === optionId)
489
+ return;
490
+ this.#activeId = optionId;
491
+ this.#sync();
492
+ }
493
+ #clearActive(optionId) {
494
+ if (this.#activeId !== optionId)
495
+ return;
496
+ this.#activeId = undefined;
497
+ this.#sync();
498
+ }
499
+ #toggle(optionId, selected) {
500
+ if (this.#config === undefined)
501
+ return;
502
+ if (this.#config.mode === 'single') {
503
+ this.#selected = [optionId];
504
+ }
505
+ else {
506
+ const next = selected
507
+ ? [...this.#selected, optionId]
508
+ : this.#selected.filter((id) => id !== optionId);
509
+ // Kept in the agent's authored order, so the response reads the way the question did
510
+ // rather than in click order.
511
+ this.#selected = this.#order.filter((id) => next.includes(id));
512
+ }
513
+ this.#setActive(optionId);
514
+ this.#sync();
515
+ this.#emit();
516
+ }
517
+ /**
518
+ * A press anywhere but the open box and its trigger closes the box without committing,
519
+ * matching the kit's popover. It listens on the document rather than on this element,
520
+ * because "anywhere else" includes the rest of the page: `pointerdown` is composed, so the
521
+ * path still names the shadow nodes an inside press went through.
522
+ */
523
+ #onDocumentPointerDown = (event) => {
524
+ if (this.#commentingId === undefined)
525
+ return;
526
+ const row = this.#rows.get(this.#commentingId);
527
+ if (row === undefined)
528
+ return;
529
+ const path = event.composedPath();
530
+ if (path.includes(row.popover) || path.includes(row.comment))
531
+ return;
532
+ // No focus restore: the press is on its way somewhere else, and pulling focus back to
533
+ // the trigger would fight it.
534
+ this.#closeComment(false);
535
+ };
536
+ #openComment(optionId) {
537
+ const row = this.#rows.get(optionId);
538
+ if (row === undefined)
539
+ return;
540
+ if (this.#commentingId === undefined) {
541
+ document.addEventListener('pointerdown', this.#onDocumentPointerDown, true);
542
+ }
543
+ this.#commentingId = optionId;
544
+ this.#activeId = optionId;
545
+ // Seeded from the saved comment every time it opens.
546
+ row.input.value = findComment(this.#comments, anchorFor(optionId))?.text ?? '';
547
+ this.#sync();
548
+ row.input.focus();
549
+ }
550
+ #closeComment(restoreFocus = true) {
551
+ if (this.#commentingId === undefined)
552
+ return;
553
+ const row = this.#rows.get(this.#commentingId);
554
+ this.#commentingId = undefined;
555
+ document.removeEventListener('pointerdown', this.#onDocumentPointerDown, true);
556
+ this.#sync();
557
+ if (restoreFocus)
558
+ row?.comment.focus();
559
+ }
560
+ #commitComment(optionId) {
561
+ const row = this.#rows.get(optionId);
562
+ if (row === undefined)
563
+ return;
564
+ // Clearing the box deletes the comment — `setComment` treats empty text as removal, so
565
+ // there is no separate destructive control.
566
+ this.#comments = setComment(this.#comments, anchorFor(optionId), row.input.value);
567
+ this.#closeComment();
568
+ this.#emit();
569
+ }
570
+ /** Paint every row from local state; local state is authoritative for rendering. */
571
+ #sync() {
572
+ for (const row of this.#rows.values()) {
573
+ const selected = this.#selected.includes(row.id);
574
+ row.control.checked = selected;
575
+ row.root.dataset.selected = String(selected);
576
+ row.root.dataset.active = String(this.#activeId === row.id);
577
+ const comment = findComment(this.#comments, anchorFor(row.id));
578
+ row.comment.dataset.state = comment === undefined ? 'empty' : 'commented';
579
+ row.comment.innerHTML = comment === undefined ? BUBBLE_PLUS : BUBBLE_TEXT;
580
+ row.comment.setAttribute('aria-label', comment === undefined ? `Comment on ${row.label}` : `Edit comment on ${row.label}`);
581
+ const open = this.#commentingId === row.id;
582
+ row.comment.setAttribute('aria-expanded', String(open));
583
+ row.popover.hidden = !open;
584
+ row.hint.textContent = comment === undefined ? '\u2318\u21b5 to save' : 'empty to delete';
585
+ }
586
+ }
587
+ /** The complete current response, on every user change — never a delta. */
588
+ #emit() {
589
+ if (this.#slotId === undefined)
590
+ return;
591
+ const response = {
592
+ selectedOptionIds: [...this.#selected],
593
+ comments: this.#comments.map((comment) => ({ ...comment })),
594
+ };
595
+ if (this.#config?.allowFreetext === true && this.#freetext !== undefined) {
596
+ response.freetext = this.#freetext;
597
+ }
598
+ respond(this.#slotId, response);
599
+ }
600
+ }
601
+ function anchorFor(optionId) {
602
+ return { kind: 'option', optionId };
603
+ }
604
+ registerElement('crtr-options', CrtrOptionsElement);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `<crtr-pages>` / `<crtr-page>` — the page's own pager, and the only thing that submits.
3
+ *
4
+ * A response-bearing page wraps its slots in exactly one `<crtr-pages>` containing one or
5
+ * more `<crtr-page>` steps. This element shows one step at a time, carries the step
6
+ * progress and the forward/back affordances, and renders the submit affordance on the
7
+ * final step — the in-document chrome, and nothing else. Dismiss and save are host chrome
8
+ * (a frame around the page, not markup inside it) and never appear here.
9
+ *
10
+ * **Flipping steps is the same motion as flipping tickets, one level down.** The inbox is
11
+ * a pager over pending pages; this is a pager over the steps of one page. So the grammar is
12
+ * deliberately the narrow one both levels share — go to the next unit of work, go back to
13
+ * the previous one, submit — with no third way to move. `n` / `p` are the keys, matching the
14
+ * terminal page panel's within-page motion (`src/clients/inbox/tui/input.ts`), because the
15
+ * kit's `j` / `k` / `x` / `c` belong to the items *inside* a slot and a pager that stole them
16
+ * would make one keystroke mean two levels at once. Submit is `⌘`/`Ctrl` + `Enter` — the
17
+ * inbox's own submit chord, unchanged, because submitting an answer is literally the same
18
+ * act at both levels. The step dots are an indicator, not a control: a page is answered by
19
+ * walking it, not by jumping around it.
20
+ *
21
+ * A single-step page renders no pager chrome at all — no progress, no forward/back — just
22
+ * the submit affordance, because there is nothing to flip.
23
+ */
24
+ export {};