@north-light/crouter 0.3.191 → 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.
- package/dist/api/client.d.ts +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +22 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +15 -28
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +32 -0
- package/dist/core/bash-jobs.js +72 -1
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +89 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +13 -5
- package/dist/pi-extensions/canvas-structured-output.js +7 -3
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -1,130 +1,50 @@
|
|
|
1
|
+
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
1
2
|
import { panItemReview, verticalCursor } from './render.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const used = new Set(it.options.map((o) => o.shortcut).filter((s) => s !== undefined));
|
|
6
|
-
for (const opt of it.options) {
|
|
7
|
-
if (opt.shortcut !== undefined)
|
|
8
|
-
continue;
|
|
9
|
-
const letters = [...opt.label.toLowerCase()].filter((c) => /[a-z]/.test(c));
|
|
10
|
-
let chosen;
|
|
11
|
-
for (const letter of letters) {
|
|
12
|
-
if (!used.has(letter) && !RESERVED.has(letter)) {
|
|
13
|
-
chosen = letter;
|
|
14
|
-
break;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
if (chosen === undefined) {
|
|
18
|
-
for (let d = 1; d <= 9; d++) {
|
|
19
|
-
const s = String(d);
|
|
20
|
-
if (!used.has(s)) {
|
|
21
|
-
chosen = s;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (chosen !== undefined) {
|
|
27
|
-
opt.shortcut = chosen;
|
|
28
|
-
used.add(chosen);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
3
|
+
import { commentIdFor, pageComplete, slotActions, slotAnswered, terminalAnswerable } from './slots.js';
|
|
4
|
+
/** Only a single-mode options/cards pick jumps ahead; a table pick leaves the user on the table. */
|
|
5
|
+
const autoAdvances = (slot, mode) => mode === 'single' && (slot.kind === 'options' || slot.kind === 'cards');
|
|
33
6
|
export function handleKeypress(input, key, state, render, exit) {
|
|
34
7
|
if (key.ctrl && input === 'c') {
|
|
35
|
-
exit();
|
|
8
|
+
exit(false);
|
|
36
9
|
return;
|
|
37
10
|
}
|
|
38
|
-
// Clear any transient hint from the previous keypress. Handlers below may set
|
|
39
|
-
// a fresh one (e.g. an empty multi-select Enter), so it survives exactly one
|
|
40
|
-
// render cycle.
|
|
41
11
|
state.hint = undefined;
|
|
42
|
-
if (state.inputMode) {
|
|
12
|
+
if (state.inputMode !== null) {
|
|
43
13
|
handleInputMode(input, key, state, render);
|
|
44
|
-
checkAutoExit(state, exit);
|
|
45
14
|
return;
|
|
46
15
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
checkAutoExit(state, exit);
|
|
54
|
-
break;
|
|
55
|
-
case 'final':
|
|
56
|
-
handleFinal(input, key, state, render, exit);
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function checkAutoExit(state, exit) {
|
|
61
|
-
if (state.phase !== 'final')
|
|
62
|
-
return;
|
|
63
|
-
if (state.responses.size < state.interactions.length)
|
|
64
|
-
return;
|
|
65
|
-
// Multi-select commits route THROUGH the Summary/confirm screen instead of
|
|
66
|
-
// auto-exiting on the first Enter: the commit advances the deck to `final`,
|
|
67
|
-
// but the human must press Enter again (handleFinal) to actually submit.
|
|
68
|
-
// The interaction that pushed us into `final` is still at currentIndex — the
|
|
69
|
-
// advance helpers leave it there when they fall through to `final` — so a
|
|
70
|
-
// multi-select there means "just confirmed a set, await deliberate submit".
|
|
71
|
-
// Single-select keeps its submit-on-pick fast path (auto-exit below).
|
|
72
|
-
const justCommitted = state.interactions[state.currentIndex];
|
|
73
|
-
if (justCommitted?.multiSelect === true)
|
|
74
|
-
return;
|
|
75
|
-
exit();
|
|
16
|
+
if (state.phase === 'overview')
|
|
17
|
+
handleOverview(input, key, state, render, exit);
|
|
18
|
+
else if (state.phase === 'item-review')
|
|
19
|
+
handleItemReview(input, key, state, render, exit);
|
|
20
|
+
else
|
|
21
|
+
handleFinal(input, key, state, render, exit);
|
|
76
22
|
}
|
|
77
|
-
// ── Overview ─────────────────────────────────────────────────────────────────
|
|
78
23
|
function handleOverview(input, key, state, render, exit) {
|
|
79
|
-
if (input === 'j' || key.downArrow)
|
|
80
|
-
state.currentIndex = Math.min(state.currentIndex + 1, state.
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (input === 'k' || key.upArrow) {
|
|
24
|
+
if (input === 'j' || key.downArrow)
|
|
25
|
+
state.currentIndex = Math.min(state.currentIndex + 1, Math.max(0, state.slots.length - 1));
|
|
26
|
+
else if (input === 'k' || key.upArrow)
|
|
85
27
|
state.currentIndex = Math.max(state.currentIndex - 1, 0);
|
|
86
|
-
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (key.return || input === ' ') {
|
|
28
|
+
else if ((key.return || input === ' ') && state.slots.length > 0) {
|
|
90
29
|
state.phase = 'item-review';
|
|
91
|
-
state
|
|
92
|
-
state.bodyMode = 'question';
|
|
93
|
-
state.scrollOffset = state.bodyScrollOffsets.question;
|
|
94
|
-
render();
|
|
95
|
-
return;
|
|
30
|
+
resetItem(state);
|
|
96
31
|
}
|
|
97
|
-
if (input === 'q') {
|
|
98
|
-
|
|
99
|
-
exit();
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
state.phase = 'final';
|
|
103
|
-
render();
|
|
104
|
-
}
|
|
32
|
+
else if (input === 'q' || key.escape) {
|
|
33
|
+
exit(!state.manifest.slots.some(isResponseBearingSlot));
|
|
105
34
|
return;
|
|
106
35
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (interaction !== undefined) {
|
|
111
|
-
const matched = interaction.options.find((o) => o.shortcut === input);
|
|
112
|
-
if (matched !== undefined) {
|
|
113
|
-
if (interaction.multiSelect) {
|
|
114
|
-
toggleMulti(state, interaction, matched.id);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
submitOption(state, interaction, matched.id, undefined);
|
|
118
|
-
}
|
|
119
|
-
// Don't auto-advance the cursor — users may want to re-answer the same
|
|
120
|
-
// question. The response icon flips ✓ and they can j/k away when ready.
|
|
121
|
-
render();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
36
|
+
else
|
|
37
|
+
return;
|
|
38
|
+
render();
|
|
124
39
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
40
|
+
function handleItemReview(input, key, state, render, exit) {
|
|
41
|
+
const slot = state.slots[state.currentIndex];
|
|
42
|
+
if (slot === undefined) {
|
|
43
|
+
state.phase = 'overview';
|
|
44
|
+
render();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const actions = slotActions(slot);
|
|
128
48
|
if (input === 'n') {
|
|
129
49
|
advanceItem(state, 1);
|
|
130
50
|
render();
|
|
@@ -135,519 +55,288 @@ function handleItemReview(input, key, state, render) {
|
|
|
135
55
|
render();
|
|
136
56
|
return;
|
|
137
57
|
}
|
|
138
|
-
// q / Esc step back to the deck overview (one level up from a card).
|
|
139
58
|
if (input === 'q' || key.escape) {
|
|
140
|
-
|
|
141
|
-
state.phase = 'overview';
|
|
142
|
-
render();
|
|
59
|
+
exit(false);
|
|
143
60
|
return;
|
|
144
61
|
}
|
|
145
|
-
// Space toggles the focused option for multi-select.
|
|
146
|
-
if (input === ' ' && interaction.multiSelect
|
|
147
|
-
&& state.selectedAction < interaction.options.length) {
|
|
148
|
-
toggleMulti(state, interaction, interaction.options[state.selectedAction].id);
|
|
149
|
-
render();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// Body scroll: u/d, PageUp/PageDown, or Ctrl+D / Ctrl+U (half-page), Ctrl+E / Ctrl+Y (line).
|
|
153
|
-
// Plain u/d exists because tmux configs commonly bind C-d/C-u for pane scroll
|
|
154
|
-
// and intercept them before they reach the app. Render clamps state.scrollOffset,
|
|
155
|
-
// so over-scroll past the bottom is harmless.
|
|
156
62
|
if (input === 'd' || key.pageDown || (key.ctrl && (input === 'd' || input === 'e'))) {
|
|
157
|
-
state.scrollOffset
|
|
63
|
+
state.scrollOffset += input === 'e' ? 1 : 10;
|
|
158
64
|
render();
|
|
159
65
|
return;
|
|
160
66
|
}
|
|
161
67
|
if (input === 'u' || key.pageUp || (key.ctrl && (input === 'u' || input === 'y'))) {
|
|
162
|
-
state.scrollOffset = Math.max(0,
|
|
68
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - (input === 'y' ? 1 : 10));
|
|
163
69
|
render();
|
|
164
70
|
return;
|
|
165
71
|
}
|
|
166
|
-
|
|
167
|
-
// actually overflows its rectangle (a wide diagram) AND no option owns that
|
|
168
|
-
// shortcut. Otherwise they fall through to the option/action handling below.
|
|
169
|
-
if ((input === 'h' || input === 'l' || key.leftArrow || key.rightArrow)
|
|
170
|
-
&& (state.hscrollMax ?? 0) > 0
|
|
171
|
-
&& !interaction.options.some((o) => o.shortcut === input)) {
|
|
72
|
+
if ((input === 'h' || input === 'l' || key.leftArrow || key.rightArrow) && state.hscrollMax > 0 && !actions.some((a) => a.kind === 'select' && a.shortcut === input)) {
|
|
172
73
|
panItemReview(state, input === 'h' || key.leftArrow ? -1 : 1);
|
|
173
74
|
render();
|
|
174
75
|
return;
|
|
175
76
|
}
|
|
176
77
|
if (input === 'j' || key.downArrow) {
|
|
177
|
-
|
|
178
|
-
state.selectedAction = Math.min(state.selectedAction + 1, max);
|
|
78
|
+
state.selectedAction = Math.min(state.selectedAction + 1, Math.max(0, actions.length - 1));
|
|
179
79
|
render();
|
|
180
80
|
return;
|
|
181
81
|
}
|
|
182
82
|
if (input === 'k' || key.upArrow) {
|
|
183
|
-
state.selectedAction = Math.max(state.selectedAction - 1
|
|
184
|
-
render();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
handleInteractionAction(input, key, state, interaction, render);
|
|
188
|
-
}
|
|
189
|
-
function handleInteractionAction(input, key, state, interaction, render) {
|
|
190
|
-
const opts = interaction.options;
|
|
191
|
-
// Match by shortcut. Multi-select toggles (stay put); single-select submits.
|
|
192
|
-
const matched = opts.find((o) => o.shortcut === input);
|
|
193
|
-
if (matched !== undefined) {
|
|
194
|
-
if (interaction.multiSelect) {
|
|
195
|
-
toggleMulti(state, interaction, matched.id);
|
|
196
|
-
render();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
submitOption(state, interaction, matched.id, undefined);
|
|
200
|
-
advanceToNextUnanswered(state);
|
|
83
|
+
state.selectedAction = Math.max(0, state.selectedAction - 1);
|
|
201
84
|
render();
|
|
202
85
|
return;
|
|
203
86
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// Single-select: pre-attach the focused option (the comment qualifies that
|
|
207
|
-
// pick). Multi-select: also pre-attach the focused option — the comment is
|
|
208
|
-
// saved under `optionComments[id]` (per-option note) and submitting
|
|
209
|
-
// auto-checks the option. From the [c] freetext row in multi-select,
|
|
210
|
-
// start with no attachment so the comment becomes the overall freetext.
|
|
211
|
-
if (input === 'c' && interaction.allowFreetext && opts.length > 0) {
|
|
212
|
-
const onOption = state.selectedAction < opts.length;
|
|
213
|
-
if (onOption) {
|
|
214
|
-
const optId = opts[state.selectedAction].id;
|
|
215
|
-
let prefill = '';
|
|
216
|
-
if (interaction.multiSelect) {
|
|
217
|
-
const existing = state.responses.get(interaction.id);
|
|
218
|
-
const prior = existing?.optionComments?.[optId];
|
|
219
|
-
if (typeof prior === 'string')
|
|
220
|
-
prefill = prior;
|
|
221
|
-
}
|
|
222
|
-
state.inputMode = { kind: 'comment', buffer: prefill, cursor: [...prefill].length, selectedOptionId: optId };
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
// On the [c] row in multi-select: pre-fill from existing overall freetext.
|
|
226
|
-
let prefill = '';
|
|
227
|
-
if (interaction.multiSelect) {
|
|
228
|
-
const existing = state.responses.get(interaction.id);
|
|
229
|
-
if (existing !== undefined && typeof existing.freetext === 'string') {
|
|
230
|
-
prefill = existing.freetext;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
state.inputMode = { kind: 'comment', buffer: prefill, cursor: [...prefill].length };
|
|
234
|
-
}
|
|
87
|
+
if (input === 'c' && terminalAnswerable(slot)) {
|
|
88
|
+
startComment(state, slot, actions);
|
|
235
89
|
render();
|
|
236
90
|
return;
|
|
237
91
|
}
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
const existing = state.responses.get(interaction.id);
|
|
242
|
-
const prefill = existing !== undefined && existing.freetext !== undefined ? existing.freetext : '';
|
|
243
|
-
state.inputMode = { kind: 'freetext', buffer: prefill, cursor: [...prefill].length };
|
|
244
|
-
render();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Enter on selected option row. Multi-select confirms the accumulated set
|
|
249
|
-
// and advances; single-select picks that one option.
|
|
250
|
-
if (key.return && state.selectedAction < opts.length) {
|
|
251
|
-
if (interaction.multiSelect) {
|
|
252
|
-
const checked = state.responses.get(interaction.id)?.selectedOptionIds ?? [];
|
|
253
|
-
if (checked.length === 0) {
|
|
254
|
-
// Accidental Enter with nothing toggled is a no-op: don't finalize or
|
|
255
|
-
// advance. A deliberate empty finish is still reachable via `q` →
|
|
256
|
-
// overview → finish (partial); freetext-only via the [c] row.
|
|
257
|
-
state.hint = 'Select at least one option (space to toggle), or q to skip';
|
|
258
|
-
render();
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
commitMulti(state, interaction);
|
|
262
|
-
advanceToNextUnanswered(state);
|
|
263
|
-
render();
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const o = opts[state.selectedAction];
|
|
267
|
-
submitOption(state, interaction, o.id, undefined);
|
|
268
|
-
advanceToNextUnanswered(state);
|
|
92
|
+
const shortcut = actions.find((a) => a.kind === 'select' && a.shortcut === input);
|
|
93
|
+
if (shortcut?.kind === 'select') {
|
|
94
|
+
commitSelection(state, slot, shortcut, autoAdvances(slot, shortcut.mode));
|
|
269
95
|
render();
|
|
96
|
+
if (autoAdvances(slot, shortcut.mode) && state.phase === 'final' && pageComplete(state))
|
|
97
|
+
exit(true);
|
|
270
98
|
return;
|
|
271
99
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
state.inputMode = { kind: 'comment', buffer: '', cursor: 0 };
|
|
100
|
+
if (input === ' ' && actions[state.selectedAction]?.kind === 'select') {
|
|
101
|
+
const action = actions[state.selectedAction];
|
|
102
|
+
commitSelection(state, slot, action, autoAdvances(slot, action.mode));
|
|
276
103
|
render();
|
|
104
|
+
if (autoAdvances(slot, action.mode) && state.phase === 'final' && pageComplete(state))
|
|
105
|
+
exit(true);
|
|
277
106
|
return;
|
|
278
107
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const mode = state.inputMode;
|
|
283
|
-
if (key.escape) {
|
|
284
|
-
state.inputMode = null;
|
|
108
|
+
if (input === 'r' && actions.some((a) => a.kind === 'freetext')) {
|
|
109
|
+
const r = responseFor(slot, state.responses.get(slot.id));
|
|
110
|
+
state.inputMode = { kind: 'freetext', buffer: r.freetext ?? '', cursor: [...(r.freetext ?? '')].length };
|
|
285
111
|
render();
|
|
286
112
|
return;
|
|
287
113
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const opts = interaction.options;
|
|
292
|
-
if (opts.length > 0) {
|
|
293
|
-
const cur = mode.selectedOptionId;
|
|
294
|
-
const curIdx = cur === undefined ? -1 : opts.findIndex((o) => o.id === cur);
|
|
295
|
-
const nextIdx = curIdx + 1; // -1 → 0, last → length (which we map to none)
|
|
296
|
-
if (nextIdx >= opts.length) {
|
|
297
|
-
delete mode.selectedOptionId;
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
mode.selectedOptionId = opts[nextIdx].id;
|
|
301
|
-
}
|
|
302
|
-
render();
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (key.pageDown) {
|
|
307
|
-
state.scrollOffset = (state.scrollOffset ?? 0) + 10;
|
|
114
|
+
if (input === 'e' && actions.some((a) => a.kind === 'edit')) {
|
|
115
|
+
const r = responseFor(slot, state.responses.get(slot.id));
|
|
116
|
+
state.inputMode = { kind: 'text', buffer: r.text, cursor: [...r.text].length };
|
|
308
117
|
render();
|
|
309
118
|
return;
|
|
310
119
|
}
|
|
311
|
-
if (
|
|
312
|
-
state
|
|
313
|
-
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (key.downArrow) {
|
|
317
|
-
mode.cursor = verticalCursor(mode.buffer, mode.cursor, 1, inputWrapWidth(state));
|
|
318
|
-
render();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (key.upArrow) {
|
|
322
|
-
mode.cursor = verticalCursor(mode.buffer, mode.cursor, -1, inputWrapWidth(state));
|
|
120
|
+
if (input === 'a' && actions.some((a) => a.kind === 'accept')) {
|
|
121
|
+
setResponse(state, slot, { text: slot.config.initialText, edited: false, comments: commentsFor(state.responses.get(slot.id)) });
|
|
122
|
+
advanceToNextIncomplete(state);
|
|
323
123
|
render();
|
|
324
124
|
return;
|
|
325
125
|
}
|
|
326
126
|
if (key.return) {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (perOption) {
|
|
332
|
-
// Per-option comment: save under optionComments[id], auto-check the
|
|
333
|
-
// option (idempotent), stay on this interaction so the human can comment
|
|
334
|
-
// on another option.
|
|
335
|
-
setOptionComment(state, interaction, mode.selectedOptionId, mode.buffer);
|
|
336
|
-
state.inputMode = null;
|
|
127
|
+
const action = actions[state.selectedAction];
|
|
128
|
+
if (action?.kind === 'freetext') {
|
|
129
|
+
const r = responseFor(slot, state.responses.get(slot.id));
|
|
130
|
+
state.inputMode = { kind: 'freetext', buffer: r.freetext ?? '', cursor: [...(r.freetext ?? '')].length };
|
|
337
131
|
render();
|
|
338
132
|
return;
|
|
339
133
|
}
|
|
340
|
-
if (
|
|
341
|
-
|
|
134
|
+
if (action?.kind === 'edit') {
|
|
135
|
+
const r = responseFor(slot, state.responses.get(slot.id));
|
|
136
|
+
state.inputMode = { kind: 'text', buffer: r.text, cursor: [...r.text].length };
|
|
137
|
+
render();
|
|
138
|
+
return;
|
|
342
139
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
140
|
+
if (action?.kind === 'accept') {
|
|
141
|
+
setResponse(state, slot, { text: slot.config.initialText, edited: false, comments: commentsFor(state.responses.get(slot.id)) });
|
|
142
|
+
advanceToNextIncomplete(state);
|
|
143
|
+
render();
|
|
144
|
+
return;
|
|
346
145
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (key.rightArrow) {
|
|
366
|
-
mode.cursor = Math.min([...mode.buffer].length, mode.cursor + 1);
|
|
367
|
-
render();
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (key.wordLeft) {
|
|
371
|
-
mode.cursor = wordLeftIndex(mode.buffer, mode.cursor);
|
|
372
|
-
render();
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (key.wordRight) {
|
|
376
|
-
mode.cursor = wordRightIndex(mode.buffer, mode.cursor);
|
|
377
|
-
render();
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (key.home || (key.ctrl && input === 'a')) {
|
|
381
|
-
mode.cursor = lineStart(mode.buffer, mode.cursor);
|
|
382
|
-
render();
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (key.end || (key.ctrl && input === 'e')) {
|
|
386
|
-
mode.cursor = lineEnd(mode.buffer, mode.cursor);
|
|
387
|
-
render();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
// ── Edits ───────────────────────────────────────────────────────────────
|
|
391
|
-
if (key.backspace && key.meta) {
|
|
392
|
-
deleteWordAtCursor(mode);
|
|
146
|
+
if (slot.kind === 'table' && terminalAnswerable(slot)) {
|
|
147
|
+
const r = responseFor(slot, state.responses.get(slot.id));
|
|
148
|
+
setResponse(state, slot, r);
|
|
149
|
+
advanceToNextIncomplete(state);
|
|
150
|
+
render();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (slotAnswered(state, slot)) {
|
|
154
|
+
advanceToNextIncomplete(state);
|
|
155
|
+
render();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isResponseBearingSlot(slot)) {
|
|
159
|
+
state.hint = terminalAnswerable(slot) ? 'Select at least one option (space to toggle), or q to go back' : 'This must be answered in Northlight';
|
|
160
|
+
render();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
advanceItem(state, 1);
|
|
393
164
|
render();
|
|
394
|
-
return;
|
|
395
165
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
166
|
+
}
|
|
167
|
+
function handleFinal(input, key, state, render, exit) {
|
|
168
|
+
if (input === 'q' || key.escape) {
|
|
169
|
+
state.phase = state.slots.length === 1 ? 'item-review' : 'overview';
|
|
170
|
+
state.scrollOffset = 0;
|
|
400
171
|
render();
|
|
401
172
|
return;
|
|
402
173
|
}
|
|
403
|
-
if (key.
|
|
404
|
-
|
|
405
|
-
|
|
174
|
+
if (key.return && (pageComplete(state) || !state.manifest.slots.some(isResponseBearingSlot)))
|
|
175
|
+
exit(true);
|
|
176
|
+
}
|
|
177
|
+
function resetItem(state) { state.selectedAction = 0; state.scrollOffset = 0; state.hscrollOffset = 0; state.hscrollMax = 0; }
|
|
178
|
+
function advanceItem(state, delta) {
|
|
179
|
+
const next = state.currentIndex + delta;
|
|
180
|
+
if (next < 0) {
|
|
181
|
+
state.phase = state.slots.length > 1 ? 'overview' : 'item-review';
|
|
406
182
|
return;
|
|
407
183
|
}
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
184
|
+
if (next >= state.slots.length) {
|
|
185
|
+
state.phase = 'final';
|
|
186
|
+
state.scrollOffset = 0;
|
|
411
187
|
return;
|
|
412
188
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// newlines in the buffer instead of vanishing (CRLF is normalized to LF).
|
|
416
|
-
const cleaned = input
|
|
417
|
-
.replace(/\x1b\[20[01]~/g, '')
|
|
418
|
-
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
419
|
-
.replace(/\r\n?/g, '\n')
|
|
420
|
-
.replace(/[\x00-\x09\x0B-\x1F\x7F]/g, '');
|
|
421
|
-
if (cleaned.length > 0) {
|
|
422
|
-
insertAtCursor(mode, cleaned);
|
|
423
|
-
render();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
function insertAtCursor(mode, text) {
|
|
427
|
-
const chars = [...mode.buffer];
|
|
428
|
-
const insert = [...text];
|
|
429
|
-
chars.splice(mode.cursor, 0, ...insert);
|
|
430
|
-
mode.buffer = chars.join('');
|
|
431
|
-
mode.cursor += insert.length;
|
|
432
|
-
}
|
|
433
|
-
function backspaceAtCursor(mode) {
|
|
434
|
-
if (mode.cursor <= 0)
|
|
435
|
-
return;
|
|
436
|
-
const chars = [...mode.buffer];
|
|
437
|
-
chars.splice(mode.cursor - 1, 1);
|
|
438
|
-
mode.buffer = chars.join('');
|
|
439
|
-
mode.cursor -= 1;
|
|
440
|
-
}
|
|
441
|
-
function deleteAtCursor(mode) {
|
|
442
|
-
const chars = [...mode.buffer];
|
|
443
|
-
if (mode.cursor >= chars.length)
|
|
444
|
-
return;
|
|
445
|
-
chars.splice(mode.cursor, 1);
|
|
446
|
-
mode.buffer = chars.join('');
|
|
447
|
-
}
|
|
448
|
-
function deleteWordAtCursor(mode) {
|
|
449
|
-
const start = wordLeftIndex(mode.buffer, mode.cursor);
|
|
450
|
-
const chars = [...mode.buffer];
|
|
451
|
-
chars.splice(start, mode.cursor - start);
|
|
452
|
-
mode.buffer = chars.join('');
|
|
453
|
-
mode.cursor = start;
|
|
189
|
+
state.currentIndex = next;
|
|
190
|
+
resetItem(state);
|
|
454
191
|
}
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
192
|
+
function advanceToNextIncomplete(state) {
|
|
193
|
+
for (let i = state.currentIndex + 1; i < state.slots.length; i++) {
|
|
194
|
+
const slot = state.slots[i];
|
|
195
|
+
if (isResponseBearingSlot(slot) && !slotAnswered(state, slot)) {
|
|
196
|
+
state.currentIndex = i;
|
|
197
|
+
resetItem(state);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
state.phase = 'final';
|
|
202
|
+
state.scrollOffset = 0;
|
|
461
203
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
204
|
+
function responseFor(slot, existing) {
|
|
205
|
+
if (existing !== undefined)
|
|
206
|
+
return existing;
|
|
207
|
+
if (slot.kind === 'options')
|
|
208
|
+
return { selectedOptionIds: [], comments: [] };
|
|
209
|
+
if (slot.kind === 'cards')
|
|
210
|
+
return { selectedCardIds: [], comments: [] };
|
|
211
|
+
if (slot.kind === 'table')
|
|
212
|
+
return { selectedRowIds: [], selectedColumnIds: [], comments: [] };
|
|
213
|
+
if (slot.kind === 'text')
|
|
214
|
+
return { text: slot.config.initialText, edited: false, comments: [] };
|
|
215
|
+
return {};
|
|
472
216
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
217
|
+
function commentsFor(response) { return [...((response?.comments) ?? [])]; }
|
|
218
|
+
function setResponse(state, slot, response) { if (slot.id === undefined)
|
|
219
|
+
return; state.responses.set(slot.id, response); state.persist?.(); }
|
|
220
|
+
function commitSelection(state, slot, action, advance) {
|
|
221
|
+
const response = responseFor(slot, state.responses.get(slot.id));
|
|
222
|
+
if (slot.kind === 'options') {
|
|
223
|
+
const r = response;
|
|
224
|
+
const ids = toggleIds(r.selectedOptionIds, action.id, action.mode);
|
|
225
|
+
setResponse(state, slot, { ...r, selectedOptionIds: ids });
|
|
226
|
+
}
|
|
227
|
+
else if (slot.kind === 'cards') {
|
|
228
|
+
const r = response;
|
|
229
|
+
setResponse(state, slot, { ...r, selectedCardIds: toggleIds(r.selectedCardIds, action.id, action.mode) });
|
|
230
|
+
}
|
|
231
|
+
else if (slot.kind === 'table') {
|
|
232
|
+
const r = response;
|
|
233
|
+
if (action.group === 'row')
|
|
234
|
+
setResponse(state, slot, { ...r, selectedRowIds: toggleIds(r.selectedRowIds, action.id, action.mode) });
|
|
235
|
+
else
|
|
236
|
+
setResponse(state, slot, { ...r, selectedColumnIds: toggleIds(r.selectedColumnIds, action.id, action.mode) });
|
|
237
|
+
}
|
|
238
|
+
if (advance)
|
|
239
|
+
advanceToNextIncomplete(state);
|
|
483
240
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
241
|
+
function toggleIds(ids, id, mode) { if (mode === 'single')
|
|
242
|
+
return [id]; return ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]; }
|
|
243
|
+
function anchorFor(action) {
|
|
244
|
+
if (action?.kind !== 'select')
|
|
245
|
+
return { kind: 'whole' };
|
|
246
|
+
if (action.group === 'option')
|
|
247
|
+
return { kind: 'option', optionId: action.id };
|
|
248
|
+
if (action.group === 'card')
|
|
249
|
+
return { kind: 'card', cardId: action.id };
|
|
250
|
+
if (action.group === 'row')
|
|
251
|
+
return { kind: 'row', rowId: action.id };
|
|
252
|
+
return { kind: 'column', columnId: action.id };
|
|
492
253
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
while (i < chars.length && chars[i] !== '\n')
|
|
499
|
-
i++;
|
|
500
|
-
return i;
|
|
254
|
+
function startComment(state, slot, actions) {
|
|
255
|
+
const anchor = anchorFor(actions[state.selectedAction]);
|
|
256
|
+
const comments = commentsFor(slot.id === undefined ? undefined : state.responses.get(slot.id));
|
|
257
|
+
const prior = comments.find((c) => c.id === commentIdFor(anchor))?.text ?? '';
|
|
258
|
+
state.inputMode = { kind: 'comment', buffer: prior, cursor: [...prior].length, anchor };
|
|
501
259
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
260
|
+
function cycleCommentAnchor(state) {
|
|
261
|
+
if (state.inputMode?.kind !== 'comment')
|
|
262
|
+
return;
|
|
263
|
+
const slot = state.slots[state.currentIndex];
|
|
264
|
+
const anchors = [{ kind: 'whole' }, ...slotActions(slot).filter((a) => a.kind === 'select').map(anchorFor)];
|
|
265
|
+
const current = commentIdFor(state.inputMode.anchor ?? { kind: 'whole' });
|
|
266
|
+
const index = anchors.findIndex((a) => commentIdFor(a) === current);
|
|
267
|
+
const anchor = anchors[(index + 1) % anchors.length];
|
|
268
|
+
const prior = commentsFor(slot.id === undefined ? undefined : state.responses.get(slot.id)).find((c) => c.id === commentIdFor(anchor))?.text ?? '';
|
|
269
|
+
state.inputMode = { kind: 'comment', anchor, buffer: prior, cursor: [...prior].length };
|
|
507
270
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (key.
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
else if (key.escape) {
|
|
514
|
-
state.phase = 'overview';
|
|
271
|
+
function handleInputMode(input, key, state, render) {
|
|
272
|
+
const mode = state.inputMode;
|
|
273
|
+
if (key.escape) {
|
|
274
|
+
state.inputMode = null;
|
|
515
275
|
render();
|
|
276
|
+
return;
|
|
516
277
|
}
|
|
517
|
-
|
|
518
|
-
state
|
|
519
|
-
state.currentIndex = state.interactions.length - 1;
|
|
278
|
+
if (key.tab && mode.kind === 'comment') {
|
|
279
|
+
cycleCommentAnchor(state);
|
|
520
280
|
render();
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
524
|
-
function advanceItem(state, direction) {
|
|
525
|
-
const next = state.currentIndex + direction;
|
|
526
|
-
if (next < 0)
|
|
527
281
|
return;
|
|
528
|
-
|
|
529
|
-
|
|
282
|
+
}
|
|
283
|
+
if (key.return) {
|
|
284
|
+
commitInput(state);
|
|
285
|
+
render();
|
|
530
286
|
return;
|
|
531
287
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
state.scrollOffset = 0;
|
|
536
|
-
state.bodyScrollOffsets = { question: 0 };
|
|
537
|
-
state.hscrollOffset = 0;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Move to the next interaction WITHOUT a response, falling through to the
|
|
541
|
-
* final phase if every following interaction is already answered (whether
|
|
542
|
-
* user-answered or `preAnswered`-seeded). Used by all post-submit advance
|
|
543
|
-
* sites so the human flies through pre-approved items by hitting Enter; raw
|
|
544
|
-
* `n`/`p` still step one at a time via `advanceItem`.
|
|
545
|
-
*/
|
|
546
|
-
function advanceToNextUnanswered(state) {
|
|
547
|
-
let next = state.currentIndex + 1;
|
|
548
|
-
while (next < state.interactions.length && state.responses.has(state.interactions[next].id)) {
|
|
549
|
-
next++;
|
|
550
|
-
}
|
|
551
|
-
if (next >= state.interactions.length) {
|
|
552
|
-
state.phase = 'final';
|
|
288
|
+
if (key.newline) {
|
|
289
|
+
insert(mode, '\n');
|
|
290
|
+
render();
|
|
553
291
|
return;
|
|
554
292
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
state.preAnsweredIds.delete(interaction.id);
|
|
575
|
-
state.persist?.();
|
|
576
|
-
}
|
|
577
|
-
// ── Multi-select ─────────────────────────────────────────────────────────────
|
|
578
|
-
// Toggle/commit write progressively into state.responses (mirrors single-select
|
|
579
|
-
// submitOption immediacy); Enter just confirms the accumulated set + advances.
|
|
580
|
-
function toggleMulti(state, interaction, optionId) {
|
|
581
|
-
const existing = state.responses.get(interaction.id);
|
|
582
|
-
const priorIds = existing !== undefined && existing.selectedOptionIds !== undefined
|
|
583
|
-
? existing.selectedOptionIds
|
|
584
|
-
: [];
|
|
585
|
-
const set = new Set(priorIds);
|
|
586
|
-
if (set.has(optionId))
|
|
587
|
-
set.delete(optionId);
|
|
588
|
-
else
|
|
589
|
-
set.add(optionId);
|
|
590
|
-
const response = { id: interaction.id, selectedOptionIds: [...set] };
|
|
591
|
-
if (existing !== undefined && existing.freetext !== undefined)
|
|
592
|
-
response.freetext = existing.freetext;
|
|
593
|
-
if (existing !== undefined && existing.optionComments !== undefined) {
|
|
594
|
-
response.optionComments = { ...existing.optionComments };
|
|
595
|
-
}
|
|
596
|
-
state.responses.set(interaction.id, response);
|
|
597
|
-
// User edited the checked set — no longer a passive carry-over.
|
|
598
|
-
state.preAnsweredIds.delete(interaction.id);
|
|
599
|
-
state.persist?.();
|
|
600
|
-
}
|
|
601
|
-
/** Ensure a (possibly empty) response exists so the interaction counts as
|
|
602
|
-
* answered, optionally setting/replacing freetext. */
|
|
603
|
-
function commitMulti(state, interaction, freetext) {
|
|
604
|
-
const existing = state.responses.get(interaction.id);
|
|
605
|
-
const priorIds = existing !== undefined && existing.selectedOptionIds !== undefined
|
|
606
|
-
? existing.selectedOptionIds
|
|
607
|
-
: [];
|
|
608
|
-
const response = {
|
|
609
|
-
id: interaction.id,
|
|
610
|
-
selectedOptionIds: [...priorIds],
|
|
611
|
-
};
|
|
612
|
-
let ft;
|
|
613
|
-
if (freetext !== undefined)
|
|
614
|
-
ft = freetext;
|
|
615
|
-
else if (existing !== undefined)
|
|
616
|
-
ft = existing.freetext;
|
|
617
|
-
if (ft !== undefined)
|
|
618
|
-
response.freetext = ft;
|
|
619
|
-
if (existing !== undefined && existing.optionComments !== undefined) {
|
|
620
|
-
response.optionComments = { ...existing.optionComments };
|
|
621
|
-
}
|
|
622
|
-
state.responses.set(interaction.id, response);
|
|
623
|
-
// Explicit confirm overrides any preAnswered seed.
|
|
624
|
-
state.preAnsweredIds.delete(interaction.id);
|
|
625
|
-
state.persist?.();
|
|
293
|
+
if (key.leftArrow)
|
|
294
|
+
mode.cursor = Math.max(0, mode.cursor - 1);
|
|
295
|
+
else if (key.rightArrow)
|
|
296
|
+
mode.cursor = Math.min([...mode.buffer].length, mode.cursor + 1);
|
|
297
|
+
else if (key.upArrow)
|
|
298
|
+
mode.cursor = verticalCursor(mode.buffer, mode.cursor, -1, inputWrapWidth(state));
|
|
299
|
+
else if (key.downArrow)
|
|
300
|
+
mode.cursor = verticalCursor(mode.buffer, mode.cursor, 1, inputWrapWidth(state));
|
|
301
|
+
else if (key.home || (key.ctrl && input === 'a'))
|
|
302
|
+
mode.cursor = lineStart(mode.buffer, mode.cursor);
|
|
303
|
+
else if (key.end || (key.ctrl && input === 'e'))
|
|
304
|
+
mode.cursor = lineEnd(mode.buffer, mode.cursor);
|
|
305
|
+
else if (key.backspace || input === '\x7f')
|
|
306
|
+
backspace(mode);
|
|
307
|
+
else if (key.del || (key.ctrl && input === 'd'))
|
|
308
|
+
del(mode);
|
|
309
|
+
else if (input.length > 0 && !key.ctrl && !key.meta)
|
|
310
|
+
insert(mode, input);
|
|
311
|
+
render();
|
|
626
312
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (existing !== undefined && existing.freetext !== undefined)
|
|
649
|
-
response.freetext = existing.freetext;
|
|
650
|
-
state.responses.set(interaction.id, response);
|
|
651
|
-
state.preAnsweredIds.delete(interaction.id);
|
|
652
|
-
state.persist?.();
|
|
313
|
+
function commitInput(state) {
|
|
314
|
+
const mode = state.inputMode;
|
|
315
|
+
const slot = state.slots[state.currentIndex];
|
|
316
|
+
const response = responseFor(slot, slot.id === undefined ? undefined : state.responses.get(slot.id));
|
|
317
|
+
if (mode.kind === 'comment') {
|
|
318
|
+
const anchor = mode.anchor ?? { kind: 'whole' };
|
|
319
|
+
const id = commentIdFor(anchor);
|
|
320
|
+
const comments = commentsFor(response).filter((c) => c.id !== id);
|
|
321
|
+
if (mode.buffer.trim() !== '')
|
|
322
|
+
comments.push({ id, anchor, text: mode.buffer });
|
|
323
|
+
setResponse(state, slot, { ...response, comments });
|
|
324
|
+
}
|
|
325
|
+
else if (mode.kind === 'freetext') {
|
|
326
|
+
setResponse(state, slot, { ...response, freetext: mode.buffer });
|
|
327
|
+
advanceToNextIncomplete(state);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
setResponse(state, slot, { text: mode.buffer, edited: mode.buffer !== slot.config.initialText, comments: commentsFor(response) });
|
|
331
|
+
advanceToNextIncomplete(state);
|
|
332
|
+
}
|
|
333
|
+
state.inputMode = null;
|
|
653
334
|
}
|
|
335
|
+
function insert(mode, text) { const chars = [...mode.buffer]; chars.splice(mode.cursor, 0, ...text); mode.buffer = chars.join(''); mode.cursor += [...text].length; }
|
|
336
|
+
function backspace(mode) { if (mode.cursor === 0)
|
|
337
|
+
return; const chars = [...mode.buffer]; chars.splice(mode.cursor - 1, 1); mode.buffer = chars.join(''); mode.cursor--; }
|
|
338
|
+
function del(mode) { const chars = [...mode.buffer]; if (mode.cursor >= chars.length)
|
|
339
|
+
return; chars.splice(mode.cursor, 1); mode.buffer = chars.join(''); }
|
|
340
|
+
function lineStart(buffer, cursor) { const before = [...buffer].slice(0, cursor).join(''); const i = before.lastIndexOf('\n'); return i < 0 ? 0 : [...before.slice(0, i + 1)].length; }
|
|
341
|
+
function lineEnd(buffer, cursor) { const chars = [...buffer]; const i = chars.slice(cursor).indexOf('\n'); return i < 0 ? chars.length : cursor + i; }
|
|
342
|
+
function inputWrapWidth(state) { return Math.max(10, Math.min(state.wrapWidth - 8, 116)); }
|