@north-light/crouter 0.3.198 → 0.3.199
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 +18 -18
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +81 -34
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +6 -6
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +3 -3
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +17 -6
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +21 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +39 -115
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +19 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -90,6 +90,7 @@ export function applySnapshot(snapshot, previous) {
|
|
|
90
90
|
const toolGroupSummaries = summaryTexts(toolGroupSummaryDetails);
|
|
91
91
|
const isStreaming = snapshot.state?.isStreaming === true;
|
|
92
92
|
const workingActivity = snapshotWorkingActivity(snapshot);
|
|
93
|
+
const queued = snapshotQueued(snapshot);
|
|
93
94
|
if (!isStreaming) {
|
|
94
95
|
return {
|
|
95
96
|
messages,
|
|
@@ -99,7 +100,7 @@ export function applySnapshot(snapshot, previous) {
|
|
|
99
100
|
isStreaming: false,
|
|
100
101
|
activity: null,
|
|
101
102
|
workingActivity,
|
|
102
|
-
queued
|
|
103
|
+
queued,
|
|
103
104
|
toolGroupSummaries,
|
|
104
105
|
toolGroupSummaryDetails,
|
|
105
106
|
};
|
|
@@ -115,7 +116,7 @@ export function applySnapshot(snapshot, previous) {
|
|
|
115
116
|
isStreaming: true,
|
|
116
117
|
activity: workingActivity,
|
|
117
118
|
workingActivity,
|
|
118
|
-
queued
|
|
119
|
+
queued,
|
|
119
120
|
toolGroupSummaries,
|
|
120
121
|
toolGroupSummaryDetails,
|
|
121
122
|
};
|
|
@@ -133,7 +134,7 @@ export function applySnapshot(snapshot, previous) {
|
|
|
133
134
|
isStreaming: true,
|
|
134
135
|
activity: workingActivity,
|
|
135
136
|
workingActivity,
|
|
136
|
-
queued
|
|
137
|
+
queued,
|
|
137
138
|
toolGroupSummaries,
|
|
138
139
|
toolGroupSummaryDetails,
|
|
139
140
|
};
|
|
@@ -142,6 +143,20 @@ function snapshotWorkingActivity(snapshot) {
|
|
|
142
143
|
const activity = snapshot.workingActivity;
|
|
143
144
|
return typeof activity === 'string' && activity.trim() !== '' ? activity : DEFAULT_WORKING_ACTIVITY;
|
|
144
145
|
}
|
|
146
|
+
/** The queue a `welcome` reports, in the same order `queue_update` composes it.
|
|
147
|
+
* Absent on a snapshot from a broker generation older than the field, and on
|
|
148
|
+
* an offline node (which has no engine and so no queue) — both mean nothing is
|
|
149
|
+
* waiting to run, which is exactly an empty queue. */
|
|
150
|
+
function snapshotQueued(snapshot) {
|
|
151
|
+
const queued = snapshot.queued;
|
|
152
|
+
if (typeof queued !== 'object' || queued === null)
|
|
153
|
+
return [];
|
|
154
|
+
const { steering, followUp } = queued;
|
|
155
|
+
return [...texts(steering), ...texts(followUp)];
|
|
156
|
+
}
|
|
157
|
+
function texts(value) {
|
|
158
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
159
|
+
}
|
|
145
160
|
/** Apply the broker's random choice for the newly received user message. */
|
|
146
161
|
export function applyWorkingActivity(state, activity) {
|
|
147
162
|
const next = activity.trim() === '' ? DEFAULT_WORKING_ACTIVITY : activity;
|
|
@@ -38,11 +38,14 @@ async function startDaemon() {
|
|
|
38
38
|
return server;
|
|
39
39
|
}
|
|
40
40
|
const source = (name, body) => { const path = join(sources, name); writeFileSync(path, body); return path; };
|
|
41
|
-
const ask = source('ask.
|
|
42
|
-
const
|
|
41
|
+
const ask = source('ask.tsx', 'export default () => (\n <Page title="Restored page" subtitle="Pick one">\n <UserQuestion id="choice" label="Restore?" body="Pick one to continue." mode="single" options={[{ id: "yes", label: "Yes" }, { id: "no", label: "No" }]} />\n </Page>\n);\n');
|
|
42
|
+
const rich = source('rich.tsx', 'export default () => (\n <Page title="Rich page" subtitle="Browser content">\n <Chart kind="bar" x="day" y={[{ field: "count" }]} />\n </Page>\n);\n');
|
|
43
|
+
// A ticket reaches the inbox and settles only with a response lifecycle; a page
|
|
44
|
+
// carrying neither is a passive display that never opens here.
|
|
45
|
+
const inboxDelivery = { placement: 'inline', inbox: true, reply: true };
|
|
43
46
|
const server = await startDaemon();
|
|
44
47
|
try {
|
|
45
|
-
const ticket = submitPage({ dir: ticketDir('page-one'), sourceFile: ask,
|
|
48
|
+
const ticket = submitPage({ dir: ticketDir('page-one'), sourceFile: ask, delivery: inboxDelivery });
|
|
46
49
|
writePageProgress(ticket.dir, { choice: { selectedOptionIds: ['yes'], comments: [] } });
|
|
47
50
|
const active = controller({ cols: 100, rows: 24 });
|
|
48
51
|
active.activate();
|
|
@@ -52,11 +55,11 @@ try {
|
|
|
52
55
|
await waitFor(() => existsSync(join(ticket.dir, 'response.json')), 'restored page responses cross the daemon seam');
|
|
53
56
|
assert.deepEqual(received, { choice: { selectedOptionIds: ['yes'], comments: [] } }, 'progress.json is restored into the response map');
|
|
54
57
|
active.close();
|
|
55
|
-
submitPage({ dir: ticketDir('rich-page'), sourceFile:
|
|
58
|
+
submitPage({ dir: ticketDir('rich-page'), sourceFile: rich, delivery: inboxDelivery });
|
|
56
59
|
const preview = controller({ cols: 100, rows: 24 });
|
|
57
|
-
assert.match(preview.render().join('\n'), /
|
|
60
|
+
assert.match(preview.render().join('\n'), /Rich page/, 'a pending page is listed before it is opened');
|
|
58
61
|
preview.activate();
|
|
59
|
-
assert.match(preview.render().join('\n'), /chart — bar: day vs count/, '
|
|
62
|
+
assert.match(preview.render().join('\n'), /chart — bar: day vs count/, 'a page uses the manifest-backed terminal projection');
|
|
60
63
|
preview.close();
|
|
61
64
|
}
|
|
62
65
|
finally {
|
|
@@ -2,12 +2,11 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import { mountPanel } from '../../tui/panel.js';
|
|
3
3
|
function key(partial = {}) { return { ctrl: false, meta: false, upArrow: false, downArrow: false, leftArrow: false, rightArrow: false, wordLeft: false, wordRight: false, home: false, end: false, pageUp: false, pageDown: false, del: false, return: false, newline: false, escape: false, tab: false, backspace: false, ...partial }; }
|
|
4
4
|
const enter = key({ return: true });
|
|
5
|
-
const manifest = (slots, extra = {}) => ({ schema: 'crtr.page/
|
|
6
|
-
const projection = (slots, prose = []) => prose.length > 0 ? prose : slots.map((slotIndex) => ({ kind: 'slot', slotIndex }));
|
|
5
|
+
const manifest = (slots, extra = {}) => ({ schema: 'crtr.page/v2', dialect: 'jsx', title: 'Test page', delivery: { placement: 'panel', inbox: true, reply: true }, document: 'page.tsx', steps: 1, slots, ...extra });
|
|
7
6
|
// Option comment is anchored and survives the subsequent single-select completion.
|
|
8
7
|
const options = manifest([{ id: 'choice', kind: 'options', step: 0, config: { mode: 'single', options: [{ id: 'yes', label: 'Yes' }, { id: 'no', label: 'No' }] } }]);
|
|
9
8
|
let optionResponses = {};
|
|
10
|
-
const optionsPanel = mountPanel({ manifest: options,
|
|
9
|
+
const optionsPanel = mountPanel({ manifest: options, cols: 80, rows: 24, onComplete: (r) => { optionResponses = r; } });
|
|
11
10
|
optionsPanel.handleKey('c', key());
|
|
12
11
|
for (const ch of 'ship it')
|
|
13
12
|
optionsPanel.handleKey(ch, key());
|
|
@@ -18,7 +17,7 @@ optionsPanel.unmount();
|
|
|
18
17
|
// Multi-select empty Enter remains inert; selection confirmation lands on Summary before submit.
|
|
19
18
|
const multi = manifest([{ id: 'toppings', kind: 'options', step: 0, config: { mode: 'multi', options: [{ id: 'mush', label: 'Mushroom' }, { id: 'onion', label: 'Onion' }] } }]);
|
|
20
19
|
let multiResponses;
|
|
21
|
-
const multiPanel = mountPanel({ manifest: multi,
|
|
20
|
+
const multiPanel = mountPanel({ manifest: multi, cols: 80, rows: 24, onComplete: (r) => { multiResponses = r; } });
|
|
22
21
|
multiPanel.handleKey('', enter);
|
|
23
22
|
assert.equal(multiResponses, undefined);
|
|
24
23
|
assert.match(multiPanel.render().join('\n'), /select at least one/i);
|
|
@@ -32,7 +31,7 @@ multiPanel.unmount();
|
|
|
32
31
|
// Table selection uses row ids and permits an empty committed selection by schema.
|
|
33
32
|
const table = manifest([{ id: 'rows', kind: 'table', step: 0, config: { columns: [{ id: 'name', label: 'Name' }], rows: [{ id: 'a', cells: { name: 'Alpha' } }, { id: 'b', cells: { name: 'Beta' } }], rowSelect: 'multi', columnSelect: 'none' } }]);
|
|
34
33
|
let tableResponses = {};
|
|
35
|
-
const tablePanel = mountPanel({ manifest: table,
|
|
34
|
+
const tablePanel = mountPanel({ manifest: table, cols: 80, rows: 24, onComplete: (r) => { tableResponses = r; } });
|
|
36
35
|
assert.match(tablePanel.render().join('\n'), /Alpha/);
|
|
37
36
|
tablePanel.handleKey(' ', key());
|
|
38
37
|
tablePanel.handleKey('', enter);
|
|
@@ -41,19 +40,19 @@ assert.deepEqual(tableResponses, { rows: { selectedRowIds: ['a'], selectedColumn
|
|
|
41
40
|
tablePanel.unmount();
|
|
42
41
|
// A display-only page is dismissed with q and completes with an empty response map.
|
|
43
42
|
let displayResponses;
|
|
44
|
-
const displayPanel = mountPanel({ manifest: manifest([]),
|
|
43
|
+
const displayPanel = mountPanel({ manifest: manifest([]), cols: 80, rows: 24, onComplete: (r) => { displayResponses = r; } });
|
|
45
44
|
displayPanel.handleKey('q', key());
|
|
46
45
|
assert.deepEqual(displayResponses, {});
|
|
47
46
|
displayPanel.unmount();
|
|
48
|
-
// A single-slot page opens in item-review
|
|
49
|
-
const lone = manifest([{ id: 'only', kind: 'options', step: 0, config: { mode: 'single', options: [{ id: 'a', label: 'Alpha' }] } }]);
|
|
50
|
-
const lonePanel = mountPanel({ manifest: lone,
|
|
51
|
-
assert.match(lonePanel.render().join('\n'), /
|
|
47
|
+
// A single-slot page opens straight in item-review.
|
|
48
|
+
const lone = manifest([{ id: 'only', kind: 'options', step: 0, config: { label: 'Pick one', mode: 'single', options: [{ id: 'a', label: 'Alpha' }] } }]);
|
|
49
|
+
const lonePanel = mountPanel({ manifest: lone, cols: 80, rows: 24 });
|
|
50
|
+
assert.match(lonePanel.render().join('\n'), /Pick one/);
|
|
52
51
|
lonePanel.unmount();
|
|
53
52
|
// An unvalidated slot takes no terminal input and keeps the page unsubmittable.
|
|
54
53
|
const unvalidated = manifest([{ id: 'widget', kind: 'acme.widget', step: 0, config: {}, unvalidated: true }]);
|
|
55
54
|
let unvalidatedResponses;
|
|
56
|
-
const unvalidatedPanel = mountPanel({ manifest: unvalidated,
|
|
55
|
+
const unvalidatedPanel = mountPanel({ manifest: unvalidated, cols: 80, rows: 24, productKinds: [{ kind: 'acme.widget' }], onComplete: (r) => { unvalidatedResponses = r; } });
|
|
57
56
|
unvalidatedPanel.handleKey('c', key());
|
|
58
57
|
assert.equal(unvalidatedPanel.getInputBuffer(), undefined);
|
|
59
58
|
unvalidatedPanel.handleKey('n', key());
|
|
@@ -65,17 +64,10 @@ unvalidatedPanel.unmount();
|
|
|
65
64
|
const stepped = manifest([
|
|
66
65
|
{ id: 'one', kind: 'options', step: 0, config: { mode: 'single', options: [{ id: 'a', label: 'Alpha' }] } },
|
|
67
66
|
{ id: 'two', kind: 'options', step: 1, config: { mode: 'single', options: [{ id: 'b', label: 'Beta' }] } },
|
|
68
|
-
], {
|
|
69
|
-
const steppedPanel = mountPanel({ manifest: stepped,
|
|
70
|
-
{ kind: 'prose', markdown: '<h2>Read <em>this</em></h2><p>Use <span>semantic prose</span>.</p><div class="custom-chrome">Widget</div>' },
|
|
71
|
-
{ kind: 'slot', slotIndex: 0 },
|
|
72
|
-
{ kind: 'slot', slotIndex: 1 },
|
|
73
|
-
]), cols: 80, rows: 24 });
|
|
67
|
+
], { steps: 2 });
|
|
68
|
+
const steppedPanel = mountPanel({ manifest: stepped, cols: 80, rows: 24 });
|
|
74
69
|
const overview = steppedPanel.render().join('\n');
|
|
75
|
-
assert.match(overview, /
|
|
76
|
-
assert.match(overview, /semantic prose/);
|
|
77
|
-
assert.match(overview, /rich page — open in Northlight/);
|
|
78
|
-
assert.doesNotMatch(overview, /<span/);
|
|
70
|
+
assert.match(overview, /Test page/);
|
|
79
71
|
assert.match(overview, /Step 1\/2/);
|
|
80
72
|
assert.match(overview, /Step 2\/2/);
|
|
81
73
|
steppedPanel.unmount();
|
|
@@ -4,8 +4,7 @@ import { BOLD, CYAN, DIM, GRAY, RESET, YELLOW, clipLine } from './tui/ansi.js';
|
|
|
4
4
|
import { buildInboxLines } from './tui.js';
|
|
5
5
|
import { inboxLayout } from './layout.js';
|
|
6
6
|
import { scanInbox } from '../../core/human/scan.js';
|
|
7
|
-
import {
|
|
8
|
-
import { parsePageSource } from '../../core/human/page.js';
|
|
7
|
+
import { resolvePageComponents } from '../../core/config.js';
|
|
9
8
|
import { watchInboxActivity } from '../../core/human/root.js';
|
|
10
9
|
import { claimTicket, heartbeatClaim, releaseClaim } from '../../core/human/claim.js';
|
|
11
10
|
import { readPageTicket, readTicketResult } from '../../core/human/tickets.js';
|
|
@@ -13,7 +12,7 @@ import { clearProgress } from '../../core/human/convention.js';
|
|
|
13
12
|
import { PageAdapter } from './page-adapter.js';
|
|
14
13
|
import { ReviewAdapter } from './review-adapter.js';
|
|
15
14
|
import { editBufferInEditor } from './review/roundtrip.js';
|
|
16
|
-
import { cancelTicketViaDaemon, daemonErrorMessage,
|
|
15
|
+
import { cancelTicketViaDaemon, daemonErrorMessage, ticketIdFor, resolveFailureKind, resolveTicketPage, } from './resolve.js';
|
|
17
16
|
/** Host-agnostic model for scanning, stable selection, claims, and rendering. */
|
|
18
17
|
export class InboxController {
|
|
19
18
|
options;
|
|
@@ -40,7 +39,7 @@ export class InboxController {
|
|
|
40
39
|
constructor(options) {
|
|
41
40
|
this.options = options;
|
|
42
41
|
this.host = options.host;
|
|
43
|
-
this.scan = options.scan ?? (() => scanInbox(
|
|
42
|
+
this.scan = options.scan ?? (() => scanInbox());
|
|
44
43
|
this.cols = options.cols ?? 80;
|
|
45
44
|
this.rows = options.rows ?? 24;
|
|
46
45
|
this.rescan();
|
|
@@ -207,9 +206,7 @@ export class InboxController {
|
|
|
207
206
|
this.claim = { dir: item.dir, token: claim.token };
|
|
208
207
|
let loaded;
|
|
209
208
|
try {
|
|
210
|
-
|
|
211
|
-
const ticket = readPageTicket(item.dir, productKinds);
|
|
212
|
-
loaded = { ticket, projection: parsePageSource(ticket.document, ticket.manifest, productKinds) };
|
|
209
|
+
loaded = readPageTicket(item.dir);
|
|
213
210
|
}
|
|
214
211
|
catch {
|
|
215
212
|
releaseClaim(item.dir, claim.token);
|
|
@@ -218,12 +215,11 @@ export class InboxController {
|
|
|
218
215
|
this.invalidate();
|
|
219
216
|
return;
|
|
220
217
|
}
|
|
221
|
-
const productKinds =
|
|
218
|
+
const productKinds = resolvePageComponents();
|
|
222
219
|
this.screen = 'detail';
|
|
223
220
|
this.adapter = new PageAdapter({
|
|
224
221
|
dir: item.dir,
|
|
225
|
-
ticket: loaded
|
|
226
|
-
projection: loaded.projection,
|
|
222
|
+
ticket: loaded,
|
|
227
223
|
productKinds,
|
|
228
224
|
cols: this.detailSize().cols,
|
|
229
225
|
rows: this.detailSize().rows,
|
|
@@ -257,9 +253,7 @@ export class InboxController {
|
|
|
257
253
|
return;
|
|
258
254
|
}
|
|
259
255
|
try {
|
|
260
|
-
|
|
261
|
-
const ticket = readPageTicket(this.claim.dir, productKinds);
|
|
262
|
-
this.adapter.reload(ticket, parsePageSource(ticket.document, ticket.manifest, productKinds));
|
|
256
|
+
this.adapter.reload(readPageTicket(this.claim.dir));
|
|
263
257
|
}
|
|
264
258
|
catch {
|
|
265
259
|
this.status = 'page could not be read';
|
|
@@ -372,7 +366,7 @@ export class InboxController {
|
|
|
372
366
|
return;
|
|
373
367
|
this.focusingSource = true;
|
|
374
368
|
try {
|
|
375
|
-
const focused = await this.host.focusNode(
|
|
369
|
+
const focused = await this.host.focusNode(ticketIdFor(item.dir));
|
|
376
370
|
if (focused)
|
|
377
371
|
this.close();
|
|
378
372
|
else {
|
|
@@ -392,11 +386,11 @@ export class InboxController {
|
|
|
392
386
|
for (const rendered of renderMarkdown(selected.subtitle, Math.max(1, width - 2)))
|
|
393
387
|
lines.push(` ${rendered}`);
|
|
394
388
|
const count = selected.kind === 'page'
|
|
395
|
-
? `${selected.slotKinds.length}
|
|
389
|
+
? `${selected.slotKinds.length} component${selected.slotKinds.length === 1 ? '' : 's'}`
|
|
396
390
|
: '1 file to review';
|
|
397
391
|
lines.push('', ` ${DIM}${count}${RESET}`);
|
|
398
392
|
if (selected.kind === 'page' && selected.dialect === 'html')
|
|
399
|
-
lines.push(` ${DIM}
|
|
393
|
+
lines.push(` ${DIM}HTML pages render in a browser surface${RESET}`);
|
|
400
394
|
return lines.map((line) => clipLine(line, width));
|
|
401
395
|
}
|
|
402
396
|
withStatus(lines) {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { ProductPageComponents } from '../../types.js';
|
|
2
2
|
import type { PageResponses } from '../../core/human/page-schema.js';
|
|
3
|
-
import type { PageSourceProjection } from '../../core/human/page.js';
|
|
4
3
|
import type { PageTicket } from '../../core/human/tickets.js';
|
|
5
4
|
import type { Key } from './tui/keys.js';
|
|
6
5
|
export interface PageAdapterOptions {
|
|
7
6
|
dir: string;
|
|
8
7
|
ticket: PageTicket;
|
|
9
|
-
projection: PageSourceProjection;
|
|
10
8
|
productKinds: ProductPageComponents;
|
|
11
9
|
cols: number;
|
|
12
10
|
rows: number;
|
|
@@ -29,6 +27,6 @@ export declare class PageAdapter {
|
|
|
29
27
|
setInputBuffer(text: string): void;
|
|
30
28
|
canAcceptHostKeys(): boolean;
|
|
31
29
|
handleKey(input: string, key: Key): void;
|
|
32
|
-
reload(ticket: PageTicket
|
|
30
|
+
reload(ticket: PageTicket): void;
|
|
33
31
|
close(): void;
|
|
34
32
|
}
|
|
@@ -12,14 +12,13 @@ export class PageAdapter {
|
|
|
12
12
|
this.manifest = opts.ticket.manifest;
|
|
13
13
|
this.panel = mountPanel({
|
|
14
14
|
manifest: opts.ticket.manifest,
|
|
15
|
-
projection: opts.projection,
|
|
16
15
|
productKinds: opts.productKinds,
|
|
17
16
|
initialResponses: opts.ticket.progress,
|
|
18
17
|
cols: opts.cols,
|
|
19
18
|
rows: opts.rows,
|
|
20
19
|
onProgress: (responses) => {
|
|
21
20
|
try {
|
|
22
|
-
writePageProgress(opts.dir, responses
|
|
21
|
+
writePageProgress(opts.dir, responses);
|
|
23
22
|
}
|
|
24
23
|
catch { /* partial in-flight input may not validate yet */ }
|
|
25
24
|
opts.onProgress?.(responses);
|
|
@@ -45,9 +44,9 @@ export class PageAdapter {
|
|
|
45
44
|
}
|
|
46
45
|
this.panel.handleKey(input, key);
|
|
47
46
|
}
|
|
48
|
-
reload(ticket
|
|
47
|
+
reload(ticket) {
|
|
49
48
|
this.manifest = ticket.manifest;
|
|
50
|
-
this.panel.loadPage(ticket.manifest,
|
|
49
|
+
this.panel.loadPage(ticket.manifest, { initialResponses: ticket.progress, productKinds: this.opts.productKinds });
|
|
51
50
|
this.opts.onDirty();
|
|
52
51
|
}
|
|
53
52
|
close() { this.panel.unmount(); }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PageResponses } from '../../core/human/page-schema.js';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function ticketIdFor(dir: string): string;
|
|
3
3
|
export declare function resolveTicketPage(dir: string, responses: PageResponses): Promise<void>;
|
|
4
4
|
export declare function cancelTicketViaDaemon(dir: string, opts: {
|
|
5
5
|
reason: string;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { ApiError } from '../../api/errors.js';
|
|
3
3
|
import { cliClient } from '../../commands/api-client.js';
|
|
4
|
-
export function
|
|
4
|
+
export function ticketIdFor(dir) {
|
|
5
5
|
return basename(dir);
|
|
6
6
|
}
|
|
7
7
|
export async function resolveTicketPage(dir, responses) {
|
|
8
|
-
await cliClient().resolveHumanTicket(
|
|
8
|
+
await cliClient().resolveHumanTicket(ticketIdFor(dir), { responses });
|
|
9
9
|
}
|
|
10
10
|
export async function cancelTicketViaDaemon(dir, opts) {
|
|
11
|
-
await cliClient().cancelHumanTicket(
|
|
11
|
+
await cliClient().cancelHumanTicket(ticketIdFor(dir), opts);
|
|
12
12
|
}
|
|
13
13
|
function isUnreachable(err) {
|
|
14
14
|
if (err instanceof ApiError)
|
|
@@ -846,8 +846,10 @@ export class ReviewDocumentSurface {
|
|
|
846
846
|
* active. There is deliberately no synthetic fallback row: a marker is
|
|
847
847
|
* only valid when it belongs to a rendered anchor or the compose/list cursor. */
|
|
848
848
|
markCursorLocation(lines, state) {
|
|
849
|
+
// The compose caret is reverse video painted over the character it sits on,
|
|
850
|
+
// so its start sequence is the cell the hardware cursor belongs in.
|
|
849
851
|
if (state.mode === 'compose')
|
|
850
|
-
return markAtToken(lines,
|
|
852
|
+
return markAtToken(lines, REVERSE);
|
|
851
853
|
if (state.mode === 'list') {
|
|
852
854
|
const markedListCursor = markAtToken(lines, `${CYAN}▸${RESET}`);
|
|
853
855
|
if (markedListCursor !== lines)
|
|
@@ -132,7 +132,8 @@ export declare function textVertical(buffer: string, cursor: number, delta: numb
|
|
|
132
132
|
* a long soft-wrapped comment has many rows even though it is one logical
|
|
133
133
|
* line. Shares `verticalCursor` with the frame renderer's own wrap. */
|
|
134
134
|
export declare function textVerticalWrapped(buffer: string, cursor: number, delta: number, width: number): number;
|
|
135
|
-
/** Start of the word before the cursor (readline alt-b / alt+left).
|
|
135
|
+
/** Start of the word before the cursor (readline alt-b / alt+left). Shares one
|
|
136
|
+
* word-motion implementation with the page compose box. */
|
|
136
137
|
export declare function textWordLeft(buffer: string, cursor: number): number;
|
|
137
138
|
/** Just past the end of the word after the cursor (readline alt-f / alt+right). */
|
|
138
139
|
export declare function textWordRight(buffer: string, cursor: number): number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { verticalCursor } from '../tui/render.js';
|
|
1
|
+
import { verticalCursor, wordLeftIndex, wordRightIndex } from '../tui/render.js';
|
|
2
2
|
import { unitIndexForLine, unitRangeForSpan } from './anchors.js';
|
|
3
3
|
/** Best unit for a source line: the NARROWEST unit containing it — code/table
|
|
4
4
|
* chrome units carry their whole block's range and sit before the per-line
|
|
@@ -251,25 +251,14 @@ export function textVertical(buffer, cursor, delta) {
|
|
|
251
251
|
export function textVerticalWrapped(buffer, cursor, delta, width) {
|
|
252
252
|
return verticalCursor(buffer, cursor, delta, width);
|
|
253
253
|
}
|
|
254
|
-
/** Start of the word before the cursor (readline alt-b / alt+left).
|
|
254
|
+
/** Start of the word before the cursor (readline alt-b / alt+left). Shares one
|
|
255
|
+
* word-motion implementation with the page compose box. */
|
|
255
256
|
export function textWordLeft(buffer, cursor) {
|
|
256
|
-
|
|
257
|
-
let i = Math.max(0, Math.min(cursor, chars.length));
|
|
258
|
-
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
259
|
-
i--;
|
|
260
|
-
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
261
|
-
i--;
|
|
262
|
-
return i;
|
|
257
|
+
return wordLeftIndex(buffer, cursor);
|
|
263
258
|
}
|
|
264
259
|
/** Just past the end of the word after the cursor (readline alt-f / alt+right). */
|
|
265
260
|
export function textWordRight(buffer, cursor) {
|
|
266
|
-
|
|
267
|
-
let i = Math.max(0, Math.min(cursor, chars.length));
|
|
268
|
-
while (i < chars.length && /\s/.test(chars[i]))
|
|
269
|
-
i++;
|
|
270
|
-
while (i < chars.length && !/\s/.test(chars[i]))
|
|
271
|
-
i++;
|
|
272
|
-
return i;
|
|
261
|
+
return wordRightIndex(buffer, cursor);
|
|
273
262
|
}
|
|
274
263
|
/** Delete back to the start of the previous word (alt+backspace / ctrl+w). */
|
|
275
264
|
export function textWordBackspace(buffer, cursor) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
2
|
-
import { panItemReview, verticalCursor } from './render.js';
|
|
3
|
-
import { commentIdFor, pageComplete, slotActions, slotAnswered, terminalAnswerable } from './slots.js';
|
|
2
|
+
import { panItemReview, verticalCursor, wordLeftIndex, wordRightIndex } from './render.js';
|
|
3
|
+
import { commentAnchors, commentIdFor, pageComplete, slotActions, slotAnswered, terminalAnswerable } from './slots.js';
|
|
4
4
|
/** Only a single-mode options/cards pick jumps ahead; a table pick leaves the user on the table. */
|
|
5
5
|
const autoAdvances = (slot, mode) => mode === 'single' && (slot.kind === 'options' || slot.kind === 'cards');
|
|
6
6
|
export function handleKeypress(input, key, state, render, exit) {
|
|
@@ -84,7 +84,7 @@ function handleItemReview(input, key, state, render, exit) {
|
|
|
84
84
|
render();
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
if (input === 'c' &&
|
|
87
|
+
if (input === 'c' && commentAnchors(slot).length > 0) {
|
|
88
88
|
startComment(state, slot, actions);
|
|
89
89
|
render();
|
|
90
90
|
return;
|
|
@@ -118,7 +118,7 @@ function handleItemReview(input, key, state, render, exit) {
|
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
if (input === 'a' && actions.some((a) => a.kind === 'accept')) {
|
|
121
|
-
setResponse(state, slot, { text: slot.config.initialText, edited: false
|
|
121
|
+
setResponse(state, slot, { text: slot.config.initialText, edited: false });
|
|
122
122
|
advanceToNextIncomplete(state);
|
|
123
123
|
render();
|
|
124
124
|
return;
|
|
@@ -138,7 +138,7 @@ function handleItemReview(input, key, state, render, exit) {
|
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
if (action?.kind === 'accept') {
|
|
141
|
-
setResponse(state, slot, { text: slot.config.initialText, edited: false
|
|
141
|
+
setResponse(state, slot, { text: slot.config.initialText, edited: false });
|
|
142
142
|
advanceToNextIncomplete(state);
|
|
143
143
|
render();
|
|
144
144
|
return;
|
|
@@ -207,11 +207,11 @@ function responseFor(slot, existing) {
|
|
|
207
207
|
if (slot.kind === 'options')
|
|
208
208
|
return { selectedOptionIds: [], comments: [] };
|
|
209
209
|
if (slot.kind === 'cards')
|
|
210
|
-
return { selectedCardIds: []
|
|
210
|
+
return { selectedCardIds: [] };
|
|
211
211
|
if (slot.kind === 'table')
|
|
212
212
|
return { selectedRowIds: [], selectedColumnIds: [], comments: [] };
|
|
213
213
|
if (slot.kind === 'text')
|
|
214
|
-
return { text: slot.config.initialText, edited: false
|
|
214
|
+
return { text: slot.config.initialText, edited: false };
|
|
215
215
|
return {};
|
|
216
216
|
}
|
|
217
217
|
function commentsFor(response) { return [...((response?.comments) ?? [])]; }
|
|
@@ -242,17 +242,15 @@ function toggleIds(ids, id, mode) { if (mode === 'single')
|
|
|
242
242
|
return [id]; return ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]; }
|
|
243
243
|
function anchorFor(action) {
|
|
244
244
|
if (action?.kind !== 'select')
|
|
245
|
-
return
|
|
245
|
+
return undefined;
|
|
246
246
|
if (action.group === 'option')
|
|
247
247
|
return { kind: 'option', optionId: action.id };
|
|
248
|
-
if (action.group === 'card')
|
|
249
|
-
return { kind: 'card', cardId: action.id };
|
|
250
248
|
if (action.group === 'row')
|
|
251
249
|
return { kind: 'row', rowId: action.id };
|
|
252
|
-
return
|
|
250
|
+
return undefined;
|
|
253
251
|
}
|
|
254
252
|
function startComment(state, slot, actions) {
|
|
255
|
-
const anchor = anchorFor(actions[state.selectedAction]);
|
|
253
|
+
const anchor = anchorFor(actions[state.selectedAction]) ?? commentAnchors(slot)[0];
|
|
256
254
|
const comments = commentsFor(slot.id === undefined ? undefined : state.responses.get(slot.id));
|
|
257
255
|
const prior = comments.find((c) => c.id === commentIdFor(anchor))?.text ?? '';
|
|
258
256
|
state.inputMode = { kind: 'comment', buffer: prior, cursor: [...prior].length, anchor };
|
|
@@ -261,8 +259,10 @@ function cycleCommentAnchor(state) {
|
|
|
261
259
|
if (state.inputMode?.kind !== 'comment')
|
|
262
260
|
return;
|
|
263
261
|
const slot = state.slots[state.currentIndex];
|
|
264
|
-
const anchors =
|
|
265
|
-
|
|
262
|
+
const anchors = commentAnchors(slot);
|
|
263
|
+
if (anchors.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
const current = commentIdFor(state.inputMode.anchor);
|
|
266
266
|
const index = anchors.findIndex((a) => commentIdFor(a) === current);
|
|
267
267
|
const anchor = anchors[(index + 1) % anchors.length];
|
|
268
268
|
const prior = commentsFor(slot.id === undefined ? undefined : state.responses.get(slot.id)).find((c) => c.id === commentIdFor(anchor))?.text ?? '';
|
|
@@ -290,7 +290,11 @@ function handleInputMode(input, key, state, render) {
|
|
|
290
290
|
render();
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
|
-
if (key.
|
|
293
|
+
if (key.wordLeft)
|
|
294
|
+
mode.cursor = wordLeftIndex(mode.buffer, mode.cursor);
|
|
295
|
+
else if (key.wordRight)
|
|
296
|
+
mode.cursor = wordRightIndex(mode.buffer, mode.cursor);
|
|
297
|
+
else if (key.leftArrow)
|
|
294
298
|
mode.cursor = Math.max(0, mode.cursor - 1);
|
|
295
299
|
else if (key.rightArrow)
|
|
296
300
|
mode.cursor = Math.min([...mode.buffer].length, mode.cursor + 1);
|
|
@@ -302,12 +306,16 @@ function handleInputMode(input, key, state, render) {
|
|
|
302
306
|
mode.cursor = lineStart(mode.buffer, mode.cursor);
|
|
303
307
|
else if (key.end || (key.ctrl && input === 'e'))
|
|
304
308
|
mode.cursor = lineEnd(mode.buffer, mode.cursor);
|
|
309
|
+
// Alt+Backspace (and its ctrl+w form) erase a word; both arrive with the meta
|
|
310
|
+
// flag set, so this precedes the single-character backspace below.
|
|
311
|
+
else if (key.meta && key.backspace)
|
|
312
|
+
wordBackspace(mode);
|
|
305
313
|
else if (key.backspace || input === '\x7f')
|
|
306
314
|
backspace(mode);
|
|
307
315
|
else if (key.del || (key.ctrl && input === 'd'))
|
|
308
316
|
del(mode);
|
|
309
|
-
else if (
|
|
310
|
-
|
|
317
|
+
else if (!key.ctrl && !key.meta)
|
|
318
|
+
insertTyped(mode, input);
|
|
311
319
|
render();
|
|
312
320
|
}
|
|
313
321
|
function commitInput(state) {
|
|
@@ -315,7 +323,7 @@ function commitInput(state) {
|
|
|
315
323
|
const slot = state.slots[state.currentIndex];
|
|
316
324
|
const response = responseFor(slot, slot.id === undefined ? undefined : state.responses.get(slot.id));
|
|
317
325
|
if (mode.kind === 'comment') {
|
|
318
|
-
const anchor = mode.anchor
|
|
326
|
+
const anchor = mode.anchor;
|
|
319
327
|
const id = commentIdFor(anchor);
|
|
320
328
|
const comments = commentsFor(response).filter((c) => c.id !== id);
|
|
321
329
|
if (mode.buffer.trim() !== '')
|
|
@@ -327,7 +335,7 @@ function commitInput(state) {
|
|
|
327
335
|
advanceToNextIncomplete(state);
|
|
328
336
|
}
|
|
329
337
|
else {
|
|
330
|
-
setResponse(state, slot, { text: mode.buffer, edited: mode.buffer !== slot.config.initialText
|
|
338
|
+
setResponse(state, slot, { text: mode.buffer, edited: mode.buffer !== slot.config.initialText });
|
|
331
339
|
advanceToNextIncomplete(state);
|
|
332
340
|
}
|
|
333
341
|
state.inputMode = null;
|
|
@@ -335,6 +343,16 @@ function commitInput(state) {
|
|
|
335
343
|
function insert(mode, text) { const chars = [...mode.buffer]; chars.splice(mode.cursor, 0, ...text); mode.buffer = chars.join(''); mode.cursor += [...text].length; }
|
|
336
344
|
function backspace(mode) { if (mode.cursor === 0)
|
|
337
345
|
return; const chars = [...mode.buffer]; chars.splice(mode.cursor - 1, 1); mode.buffer = chars.join(''); mode.cursor--; }
|
|
346
|
+
function wordBackspace(mode) { const to = wordLeftIndex(mode.buffer, mode.cursor); if (to === mode.cursor)
|
|
347
|
+
return; const chars = [...mode.buffer]; chars.splice(to, mode.cursor - to); mode.buffer = chars.join(''); mode.cursor = to; }
|
|
348
|
+
/** Typed and pasted text only: an unrecognized escape sequence reaches this
|
|
349
|
+
* handler as raw bytes, and inserting those would put control characters into
|
|
350
|
+
* the answer. Newlines survive because a paste may legitimately carry them. */
|
|
351
|
+
function insertTyped(mode, input) {
|
|
352
|
+
const clean = input.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\r\n?/g, '\n').replace(/[\x00-\x09\x0B-\x1F\x7F]/g, '');
|
|
353
|
+
if (clean.length > 0)
|
|
354
|
+
insert(mode, clean);
|
|
355
|
+
}
|
|
338
356
|
function del(mode) { const chars = [...mode.buffer]; if (mode.cursor >= chars.length)
|
|
339
357
|
return; chars.splice(mode.cursor, 1); mode.buffer = chars.join(''); }
|
|
340
358
|
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; }
|
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
import { handleKeypress } from './input.js';
|
|
2
2
|
import { clampItemReviewScroll, renderFinal, renderItemReview, renderOverview } from './render.js';
|
|
3
3
|
import { pageComplete, slotAnswered } from './slots.js';
|
|
4
|
-
function
|
|
5
|
-
const overviewProse = [];
|
|
6
|
-
const slotProse = new Map();
|
|
7
|
-
let pending = [];
|
|
8
|
-
let seenSlot = false;
|
|
9
|
-
for (const block of projection) {
|
|
10
|
-
if (block.kind === 'prose') {
|
|
11
|
-
if (block.markdown.trim() !== '')
|
|
12
|
-
pending.push(block.markdown);
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (!seenSlot)
|
|
16
|
-
overviewProse.push(...pending);
|
|
17
|
-
else
|
|
18
|
-
slotProse.set(block.slotIndex, pending);
|
|
19
|
-
pending = [];
|
|
20
|
-
seenSlot = true;
|
|
21
|
-
}
|
|
22
|
-
// A page with no slots has no final phase to reach — its prose is the overview.
|
|
23
|
-
if (!seenSlot) {
|
|
24
|
-
overviewProse.push(...pending);
|
|
25
|
-
return { overviewProse, slotProse, finalProse: [] };
|
|
26
|
-
}
|
|
27
|
-
return { overviewProse, slotProse, finalProse: pending };
|
|
28
|
-
}
|
|
29
|
-
function buildInitialState(manifest, projection, initialResponses, productKinds, editorAvailable, wrapWidth) {
|
|
30
|
-
const prose = projectProse(projection);
|
|
4
|
+
function buildInitialState(manifest, initialResponses, productKinds, editorAvailable, wrapWidth) {
|
|
31
5
|
const responses = new Map();
|
|
32
6
|
const validIds = new Set(manifest.slots.flatMap((slot) => slot.id === undefined ? [] : [slot.id]));
|
|
33
7
|
for (const [id, response] of Object.entries(initialResponses ?? {}))
|
|
34
8
|
if (validIds.has(id))
|
|
35
9
|
responses.set(id, response);
|
|
36
|
-
const state = { phase: manifest.slots.length === 1 ? 'item-review' : 'overview', currentIndex: 0, manifest, slots: manifest.slots,
|
|
10
|
+
const state = { phase: manifest.slots.length === 1 ? 'item-review' : 'overview', currentIndex: 0, manifest, slots: manifest.slots, responses, productKinds, inputMode: null, selectedAction: 0, bodyMode: 'question', scrollOffset: 0, bodyScrollOffsets: { question: 0 }, hscrollOffset: 0, hscrollMax: 0, wrapWidth, editorAvailable };
|
|
37
11
|
const first = manifest.slots.findIndex((slot) => slot.id !== undefined && !slotAnswered(state, slot));
|
|
38
12
|
if (first >= 0)
|
|
39
13
|
state.currentIndex = first;
|
|
@@ -42,7 +16,7 @@ function buildInitialState(manifest, projection, initialResponses, productKinds,
|
|
|
42
16
|
export function collectResponses(state) { return Object.fromEntries(state.responses); }
|
|
43
17
|
function rebindPersist(internals) { internals.state.persist = () => internals.callbacks.onProgress?.(collectResponses(internals.state)); }
|
|
44
18
|
export function mountPanel(opts) {
|
|
45
|
-
const internals = { state: buildInitialState(opts.manifest, opts.
|
|
19
|
+
const internals = { state: buildInitialState(opts.manifest, opts.initialResponses, opts.productKinds ?? [], opts.onEditorRequest !== undefined, opts.cols), cols: opts.cols, rows: opts.rows, mounted: true, callbacks: { onProgress: opts.onProgress, onComplete: opts.onComplete, onExit: opts.onExit, onDirty: opts.onDirty, onEditorRequest: opts.onEditorRequest } };
|
|
46
20
|
rebindPersist(internals);
|
|
47
21
|
const renderLines = () => {
|
|
48
22
|
if (internals.state.phase === 'overview')
|
|
@@ -72,12 +46,12 @@ export function mountPanel(opts) {
|
|
|
72
46
|
render: () => internals.mounted ? renderLines() : [],
|
|
73
47
|
handleResize(cols, rows) { internals.cols = cols; internals.rows = rows; internals.state.wrapWidth = cols; return renderLines(); },
|
|
74
48
|
unmount() { internals.mounted = false; internals.state.persist = undefined; },
|
|
75
|
-
loadPage(manifest,
|
|
49
|
+
loadPage(manifest, loadOpts) {
|
|
76
50
|
if (!internals.mounted)
|
|
77
51
|
return;
|
|
78
52
|
const prior = collectResponses(internals.state);
|
|
79
53
|
const merged = { ...(loadOpts?.initialResponses ?? {}), ...prior };
|
|
80
|
-
internals.state = buildInitialState(manifest,
|
|
54
|
+
internals.state = buildInitialState(manifest, merged, loadOpts?.productKinds ?? internals.state.productKinds, internals.callbacks.onEditorRequest !== undefined, internals.cols);
|
|
81
55
|
rebindPersist(internals);
|
|
82
56
|
},
|
|
83
57
|
canAcceptHostKeys: () => internals.mounted && internals.state.inputMode === null,
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { TuiState } from './types.js';
|
|
2
2
|
export declare function renderOverview(state: TuiState, cols: number, rows: number): string[];
|
|
3
|
+
/** Wrap an input buffer and paint the cursor the way a terminal does: the
|
|
4
|
+
* character under it is drawn in reverse video, occupying its own cell rather
|
|
5
|
+
* than pushing the rest of the line across by one. Only past the end of a row
|
|
6
|
+
* does the cursor need a cell of its own, and there it is a reversed space.
|
|
7
|
+
* Wrapping runs over the plain buffer, so the rows here are exactly the rows
|
|
8
|
+
* `verticalCursor` moves between. */
|
|
3
9
|
export declare function renderInputBuffer(buffer: string, cursor: number, maxWidth: number): string[];
|
|
10
|
+
/** Start of the word before the cursor (alt+left / readline alt-b). The one
|
|
11
|
+
* implementation of word motion for every inbox input buffer. */
|
|
12
|
+
export declare function wordLeftIndex(buffer: string, cursor: number): number;
|
|
13
|
+
/** Just past the end of the word after the cursor (alt+right / readline alt-f). */
|
|
14
|
+
export declare function wordRightIndex(buffer: string, cursor: number): number;
|
|
4
15
|
export declare function wrappedRowBounds(buffer: string, width: number): {
|
|
5
16
|
start: number;
|
|
6
17
|
end: number;
|