@north-light/crouter 0.3.197 → 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 +19 -19
- 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 +126 -33
- 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 +8 -8
- 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 +3 -3
- 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/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- 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/render/tool-calls.js +16 -2
- 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 -982
- 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 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- 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 +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- 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/{doc.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 +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- 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__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- 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 +41 -45
- 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/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- 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/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +33 -0
- package/dist/core/canvas/canvas.js +92 -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 +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- 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 +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- 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 +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- 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 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- 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 +12 -4
- 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/boot-root.js +6 -1
- 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 +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- 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/spawn.js +4 -4
- 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 +36 -2
- package/dist/core/user-settings.js +56 -14
- 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 +48 -109
- 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 +29 -17
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -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/daemon/review/sweep.js +12 -1
- 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.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- 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 +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- 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/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- 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__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
import stringWidth from 'string-width';
|
|
2
2
|
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
3
|
+
import { answerCommentAnchorLabel, describePageAnswer } from '../../../core/human/answer.js';
|
|
3
4
|
import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
|
|
4
|
-
import {
|
|
5
|
-
import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline,
|
|
5
|
+
import { commentAnchors, describeSlot, pageComplete, slotActions, slotAnswered, tableMarkdown, terminalAnswerable } from './slots.js';
|
|
6
|
+
import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, wrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
|
|
6
7
|
const PAN_STEP = 8;
|
|
7
8
|
const mdLines = (md, maxW, paneW) => renderMarkdownBlockAwareLines(md, maxW, paneW);
|
|
8
|
-
function
|
|
9
|
-
return
|
|
9
|
+
function pageAnswer(state) {
|
|
10
|
+
return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: '' });
|
|
11
|
+
}
|
|
12
|
+
function answerSlot(answer, slot) {
|
|
13
|
+
return slot.id === undefined ? undefined : answer.slots.find((candidate) => candidate.id === slot.id);
|
|
14
|
+
}
|
|
15
|
+
function answerSummary(slot) {
|
|
16
|
+
if (slot === undefined || slot.state === 'unanswered')
|
|
17
|
+
return 'unanswered';
|
|
18
|
+
if (slot.raw !== undefined)
|
|
19
|
+
return 'custom response';
|
|
20
|
+
const groups = slot.text !== undefined ? [slot.text.edited ? 'edited' : 'accepted'] : slot.groups.map((group) => group.picked.length === 0 ? 'none' : group.picked.map((item) => item.label).join(', '));
|
|
21
|
+
if (slot.freetext !== undefined)
|
|
22
|
+
groups.push(slot.freetext);
|
|
23
|
+
if (slot.comments.length > 0)
|
|
24
|
+
groups.push(`${slot.comments.length} comment${slot.comments.length === 1 ? '' : 's'}`);
|
|
25
|
+
return groups.join('; ') || 'answered';
|
|
26
|
+
}
|
|
27
|
+
function commentLabel(slot, anchor) {
|
|
28
|
+
return answerCommentAnchorLabel(slot, anchor);
|
|
10
29
|
}
|
|
11
30
|
export function renderOverview(state, cols, rows) {
|
|
12
31
|
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
13
32
|
const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)}${RESET}`];
|
|
14
33
|
if (state.manifest.subtitle)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
lines.push(` ${line}`);
|
|
34
|
+
for (const line of wrap(sanitize(state.manifest.subtitle), maxW))
|
|
35
|
+
lines.push(` ${DIM}${line}${RESET}`);
|
|
36
|
+
const hasBody = state.slots.length > 0;
|
|
37
|
+
if (hasBody)
|
|
38
|
+
lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
|
|
39
|
+
const answer = pageAnswer(state);
|
|
22
40
|
let priorStep = -1;
|
|
23
41
|
for (let i = 0; i < state.slots.length; i++) {
|
|
24
42
|
const slot = state.slots[i];
|
|
@@ -33,20 +51,66 @@ export function renderOverview(state, cols, rows) {
|
|
|
33
51
|
const cursor = i === state.currentIndex ? `${CYAN}▸${RESET} ` : ' ';
|
|
34
52
|
lines.push(` ${cursor}${icon} ${truncate(singleLine(describeSlot(slot)), Math.max(10, cols - 16))}`);
|
|
35
53
|
if (response !== undefined)
|
|
36
|
-
lines.push(` ${DIM}${truncate(singleLine(
|
|
54
|
+
lines.push(` ${DIM}${truncate(singleLine(answerSummary(answerSlot(answer, slot))), Math.max(10, cols - 10))}${RESET}`);
|
|
37
55
|
}
|
|
38
|
-
if (
|
|
39
|
-
lines.push(
|
|
40
|
-
lines.push(
|
|
56
|
+
if (hasBody)
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(` ${DIM}${hline(maxW)}${RESET}`);
|
|
41
59
|
const display = !state.manifest.slots.some(isResponseBearingSlot);
|
|
42
60
|
lines.push(display ? ` ${DIM}q${RESET} dismiss` : ` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} close`);
|
|
43
61
|
return centeredWindow(lines, state, cols, rows, maxW + 2);
|
|
44
62
|
}
|
|
63
|
+
/** Wrap an input buffer and paint the cursor the way a terminal does: the
|
|
64
|
+
* character under it is drawn in reverse video, occupying its own cell rather
|
|
65
|
+
* than pushing the rest of the line across by one. Only past the end of a row
|
|
66
|
+
* does the cursor need a cell of its own, and there it is a reversed space.
|
|
67
|
+
* Wrapping runs over the plain buffer, so the rows here are exactly the rows
|
|
68
|
+
* `verticalCursor` moves between. */
|
|
45
69
|
export function renderInputBuffer(buffer, cursor, maxWidth) {
|
|
46
70
|
const chars = [...buffer];
|
|
47
71
|
const at = Math.max(0, Math.min(cursor, chars.length));
|
|
48
|
-
const
|
|
49
|
-
|
|
72
|
+
const rows = wrappedRowBounds(buffer, maxWidth);
|
|
73
|
+
let cursorRow = 0;
|
|
74
|
+
for (let i = 0; i < rows.length; i++)
|
|
75
|
+
if (rows[i].start <= at)
|
|
76
|
+
cursorRow = i;
|
|
77
|
+
const lines = rows.map((row, index) => {
|
|
78
|
+
if (index !== cursorRow)
|
|
79
|
+
return chars.slice(row.start, row.end).join('');
|
|
80
|
+
const head = chars.slice(row.start, at).join('');
|
|
81
|
+
const under = at < row.end ? chars[at] : ' ';
|
|
82
|
+
const tail = at < row.end ? chars.slice(at + 1, row.end).join('') : '';
|
|
83
|
+
return `${head}${REVERSE}${under}${RESET}${tail}`;
|
|
84
|
+
});
|
|
85
|
+
// A cursor past the end of an already-full row has no cell left on it, so it
|
|
86
|
+
// takes the first cell of the next row instead of overflowing the wrap width.
|
|
87
|
+
const last = rows[cursorRow];
|
|
88
|
+
if (at === last.end && stringWidth(chars.slice(last.start, last.end).join('')) >= maxWidth) {
|
|
89
|
+
lines[cursorRow] = chars.slice(last.start, last.end).join('');
|
|
90
|
+
lines.splice(cursorRow + 1, 0, `${REVERSE} ${RESET}`);
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
/** Start of the word before the cursor (alt+left / readline alt-b). The one
|
|
95
|
+
* implementation of word motion for every inbox input buffer. */
|
|
96
|
+
export function wordLeftIndex(buffer, cursor) {
|
|
97
|
+
const chars = [...buffer];
|
|
98
|
+
let i = Math.max(0, Math.min(cursor, chars.length));
|
|
99
|
+
while (i > 0 && /\s/.test(chars[i - 1]))
|
|
100
|
+
i--;
|
|
101
|
+
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
102
|
+
i--;
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
/** Just past the end of the word after the cursor (alt+right / readline alt-f). */
|
|
106
|
+
export function wordRightIndex(buffer, cursor) {
|
|
107
|
+
const chars = [...buffer];
|
|
108
|
+
let i = Math.max(0, Math.min(cursor, chars.length));
|
|
109
|
+
while (i < chars.length && /\s/.test(chars[i]))
|
|
110
|
+
i++;
|
|
111
|
+
while (i < chars.length && !/\s/.test(chars[i]))
|
|
112
|
+
i++;
|
|
113
|
+
return i;
|
|
50
114
|
}
|
|
51
115
|
export function wrappedRowBounds(buffer, width) {
|
|
52
116
|
const chars = [...buffer];
|
|
@@ -107,13 +171,12 @@ function itemLines(state, cols) {
|
|
|
107
171
|
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
108
172
|
const paneW = Math.max(20, cols - 4);
|
|
109
173
|
const lines = ['', ` ${BOLD}${CYAN}[${state.currentIndex + 1}/${state.slots.length}]${RESET} ${DIM}${sanitize(state.manifest.title)}${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, '', ` ${BOLD}${sanitize(describeSlot(slot))}${RESET}`];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else
|
|
115
|
-
for (const line of proseLines(prose, maxW, paneW))
|
|
174
|
+
const body = slot.config.body;
|
|
175
|
+
if (typeof body === 'string' && body.length > 0) {
|
|
176
|
+
lines.push('');
|
|
177
|
+
for (const line of mdLines(body, maxW, paneW))
|
|
116
178
|
lines.push(` ${line}`);
|
|
179
|
+
}
|
|
117
180
|
if (!terminalAnswerable(slot))
|
|
118
181
|
lines.push('', ` ${DIM}${sanitize(slot.kind)} — cannot be answered in the terminal; open in Northlight${RESET}`);
|
|
119
182
|
else if (slot.kind === 'chart') {
|
|
@@ -125,9 +188,9 @@ function itemLines(state, cols) {
|
|
|
125
188
|
lines.push('');
|
|
126
189
|
for (const line of mdLines(tableMarkdown(slot.config), maxW, paneW))
|
|
127
190
|
lines.push(` ${line}`);
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
lines.push(` ${DIM}${sanitize(
|
|
191
|
+
const label = slot.config.label;
|
|
192
|
+
if (label)
|
|
193
|
+
lines.push(` ${DIM}${sanitize(label)}${RESET}`);
|
|
131
194
|
}
|
|
132
195
|
else if (slot.kind === 'text') {
|
|
133
196
|
const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
|
|
@@ -138,7 +201,7 @@ function itemLines(state, cols) {
|
|
|
138
201
|
}
|
|
139
202
|
lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
|
|
140
203
|
if (state.inputMode !== null) {
|
|
141
|
-
const label = state.inputMode.kind === 'comment' ? `Comment on ${
|
|
204
|
+
const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor)}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
|
|
142
205
|
lines.push(` ${BOLD}${label}${RESET}`);
|
|
143
206
|
for (const line of renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, Math.max(10, maxW - 4)))
|
|
144
207
|
lines.push(` ${line}`);
|
|
@@ -152,7 +215,8 @@ function itemLines(state, cols) {
|
|
|
152
215
|
const mark = selected(state, slot, action.group, action.id) ? `${GREEN}✓${RESET}` : ' ';
|
|
153
216
|
lines.push(` ${cursor} [${mark}] ${action.shortcut ? `[${action.shortcut}] ` : ''}${sanitize(action.label)}`);
|
|
154
217
|
if (action.description)
|
|
155
|
-
|
|
218
|
+
for (const line of wrap(sanitize(action.description), Math.max(10, maxW - 6)))
|
|
219
|
+
lines.push(` ${DIM}${line}${RESET}`);
|
|
156
220
|
}
|
|
157
221
|
else {
|
|
158
222
|
const key = action.kind === 'freetext' ? 'r' : action.kind === 'edit' ? 'e' : 'a';
|
|
@@ -160,10 +224,11 @@ function itemLines(state, cols) {
|
|
|
160
224
|
}
|
|
161
225
|
});
|
|
162
226
|
for (const comment of responseComments(state, slot))
|
|
163
|
-
|
|
227
|
+
for (const line of wrap(`attached: ${commentLabel(slot, comment.anchor)} — ${sanitize(comment.text)}`, Math.max(10, maxW - 2)))
|
|
228
|
+
lines.push(` ${DIM}${line}${RESET}`);
|
|
164
229
|
if (state.hint)
|
|
165
230
|
lines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
|
|
166
|
-
lines.push(` ${DIM}n/p next/previous j/k actions c comment u/d scroll h/l pan q close${RESET}`);
|
|
231
|
+
lines.push(` ${DIM}n/p next/previous j/k actions${commentAnchors(slot).length > 0 ? ' c comment' : ''} u/d scroll h/l pan q close${RESET}`);
|
|
167
232
|
}
|
|
168
233
|
const maxLine = Math.max(0, ...lines.map(visibleWidth));
|
|
169
234
|
return { lines, maxW, maxH: Math.max(0, maxLine - Math.max(1, cols)) };
|
|
@@ -195,13 +260,13 @@ export function renderItemReview(state, cols, rows) {
|
|
|
195
260
|
export function renderFinal(state, cols, rows) {
|
|
196
261
|
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
197
262
|
const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)} — Summary${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, ''];
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
263
|
+
const answer = pageAnswer(state);
|
|
264
|
+
for (const slot of answer.slots) {
|
|
265
|
+
const manifestSlot = state.manifest.slots.find((candidate) => candidate.id === slot.id);
|
|
266
|
+
const answered = manifestSlot !== undefined && slotAnswered(state, manifestSlot);
|
|
267
|
+
const mark = answered ? `${GREEN}✓${RESET}` : `${YELLOW}○${RESET}`;
|
|
268
|
+
lines.push(` ${mark} ${BOLD}${sanitize(manifestSlot === undefined ? slot.kind : describeSlot(manifestSlot))}${RESET} ${DIM}${sanitize(answerSummary(slot))}${RESET}`);
|
|
201
269
|
}
|
|
202
|
-
if (state.manifest.dialect !== 'html')
|
|
203
|
-
for (const line of proseLines(state.finalProse, maxW, Math.max(20, cols - 4)))
|
|
204
|
-
lines.push(` ${line}`);
|
|
205
270
|
const complete = pageComplete(state);
|
|
206
271
|
if (!complete) {
|
|
207
272
|
lines.push('', ` ${YELLOW}Cannot submit yet.${RESET}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommentAnchor, PageManifest, PageSlot,
|
|
1
|
+
import type { CommentAnchor, PageManifest, PageSlot, TableConfig } from '../../../core/human/page-schema.js';
|
|
2
2
|
import type { TuiState } from './types.js';
|
|
3
3
|
export type SelectGroup = 'option' | 'card' | 'row' | 'column';
|
|
4
4
|
export type ActionRow = {
|
|
@@ -20,13 +20,13 @@ export type ActionRow = {
|
|
|
20
20
|
label: string;
|
|
21
21
|
};
|
|
22
22
|
export declare function describeSlot(slot: PageSlot): string;
|
|
23
|
-
/** False when
|
|
23
|
+
/** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
|
|
24
24
|
export declare function terminalAnswerable(slot: PageSlot): boolean;
|
|
25
25
|
export declare function slotActions(slot: PageSlot): ActionRow[];
|
|
26
26
|
export declare function tableMarkdown(config: TableConfig): string;
|
|
27
|
-
export declare function summarizeSlotResponse(slot: PageSlot, response: SlotResponse | undefined): string;
|
|
28
27
|
export declare function slotAnswered(state: TuiState, slot: PageSlot): boolean;
|
|
29
28
|
export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'responses'>): boolean;
|
|
30
29
|
export declare function commentIdFor(anchor: CommentAnchor): string;
|
|
31
|
-
|
|
30
|
+
/** The response-comment anchors this component may author: options take per-option, tables take rows only. */
|
|
31
|
+
export declare function commentAnchors(slot: PageSlot): CommentAnchor[];
|
|
32
32
|
export declare function awaitsResponse(manifest: PageManifest): boolean;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from '../../../core/human/page-schema.js';
|
|
2
2
|
const RESERVED = new Set(['c', 'r', 'e', 'a', 'n', 'p', 'q', 'j', 'k', 'u', 'd', ' ']);
|
|
3
3
|
export function describeSlot(slot) {
|
|
4
|
+
const label = slot.config.label;
|
|
5
|
+
if (typeof label === 'string' && label !== '')
|
|
6
|
+
return label;
|
|
4
7
|
if (slot.unvalidated === true)
|
|
5
8
|
return slot.kind;
|
|
6
9
|
if (slot.kind === 'options')
|
|
@@ -8,14 +11,14 @@ export function describeSlot(slot) {
|
|
|
8
11
|
if (slot.kind === 'text')
|
|
9
12
|
return 'Text';
|
|
10
13
|
if (slot.kind === 'table')
|
|
11
|
-
return
|
|
14
|
+
return 'Table';
|
|
12
15
|
if (slot.kind === 'cards')
|
|
13
16
|
return 'Cards';
|
|
14
17
|
if (slot.kind === 'chart')
|
|
15
18
|
return 'Chart';
|
|
16
19
|
return slot.kind;
|
|
17
20
|
}
|
|
18
|
-
/** False when
|
|
21
|
+
/** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
|
|
19
22
|
export function terminalAnswerable(slot) {
|
|
20
23
|
if (slot.unvalidated === true)
|
|
21
24
|
return false;
|
|
@@ -31,15 +34,16 @@ export function slotActions(slot) {
|
|
|
31
34
|
const config = slot.config;
|
|
32
35
|
rows = config.options.map((option) => ({ kind: 'select', group: 'option', id: option.id, label: option.label, ...(option.description === undefined ? {} : { description: option.description }), mode: config.mode }));
|
|
33
36
|
if (config.allowFreetext === true)
|
|
34
|
-
rows.push({ kind: 'freetext', label: config.freetextLabel ?? '
|
|
37
|
+
rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Something else' });
|
|
35
38
|
}
|
|
36
39
|
else if (slot.kind === 'cards') {
|
|
37
40
|
const config = slot.config;
|
|
38
41
|
rows = (config.cards ?? []).map((card) => ({ kind: 'select', group: 'card', id: card.id, label: card.subtitle === undefined ? card.title : `${card.title} — ${card.subtitle}`, ...(card.body === undefined ? {} : { description: card.body }), mode: config.mode }));
|
|
39
42
|
}
|
|
40
43
|
else if (slot.kind === 'text') {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
// Text is always a writing surface — the same contract the page kit presents. Accepting
|
|
45
|
+
// the draft as written is the other half of that answer, not a separate read-only mode.
|
|
46
|
+
rows.push({ kind: 'edit', label: 'Edit' });
|
|
43
47
|
rows.push({ kind: 'accept', label: 'Accept as-is' });
|
|
44
48
|
}
|
|
45
49
|
else if (slot.kind === 'table') {
|
|
@@ -84,28 +88,6 @@ export function tableMarkdown(config) {
|
|
|
84
88
|
const divider = `| ${config.columns.map((column) => column.align === 'right' ? '---:' : '---').join(' | ')} |`;
|
|
85
89
|
return [header, divider, ...(config.rows ?? []).map((row) => `| ${config.columns.map((column) => escapeCell(row.cells[column.id], column.mono === true)).join(' | ')} |`)].join('\n');
|
|
86
90
|
}
|
|
87
|
-
export function summarizeSlotResponse(slot, response) {
|
|
88
|
-
if (response === undefined)
|
|
89
|
-
return isResponseBearingSlot(slot) ? 'unanswered' : 'display';
|
|
90
|
-
if (slot.kind === 'options') {
|
|
91
|
-
const r = response;
|
|
92
|
-
const labels = slot.config.options.filter((o) => r.selectedOptionIds.includes(o.id)).map((o) => o.label);
|
|
93
|
-
if ((r.freetext?.trim() ?? '') !== '')
|
|
94
|
-
labels.push(r.freetext.trim());
|
|
95
|
-
return labels.join(', ') || (r.comments.length > 0 ? `${r.comments.length} comment${r.comments.length === 1 ? '' : 's'}` : 'answered');
|
|
96
|
-
}
|
|
97
|
-
if (slot.kind === 'cards') {
|
|
98
|
-
const r = response;
|
|
99
|
-
return slot.config.cards?.filter((c) => r.selectedCardIds.includes(c.id)).map((c) => c.title).join(', ') || 'answered';
|
|
100
|
-
}
|
|
101
|
-
if (slot.kind === 'table') {
|
|
102
|
-
const r = response;
|
|
103
|
-
return `${r.selectedRowIds.length} row${r.selectedRowIds.length === 1 ? '' : 's'}, ${r.selectedColumnIds.length} column${r.selectedColumnIds.length === 1 ? '' : 's'}`;
|
|
104
|
-
}
|
|
105
|
-
if (slot.kind === 'text')
|
|
106
|
-
return response.edited ? 'edited' : 'accepted';
|
|
107
|
-
return 'answered';
|
|
108
|
-
}
|
|
109
91
|
export function slotAnswered(state, slot) {
|
|
110
92
|
if (!terminalAnswerable(slot) || slot.id === undefined || !state.responses.has(slot.id))
|
|
111
93
|
return false;
|
|
@@ -130,27 +112,20 @@ export function pageComplete(state) {
|
|
|
130
112
|
}
|
|
131
113
|
export function commentIdFor(anchor) {
|
|
132
114
|
switch (anchor.kind) {
|
|
133
|
-
case 'whole': return 'whole';
|
|
134
115
|
case 'option': return `option:${anchor.optionId}`;
|
|
135
|
-
case 'card': return `card:${anchor.cardId}`;
|
|
136
116
|
case 'row': return `row:${anchor.rowId}`;
|
|
137
|
-
case 'column': return `column:${anchor.columnId}`;
|
|
138
|
-
case 'range': return `range:${anchor.start}:${anchor.end}`;
|
|
139
117
|
}
|
|
140
118
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (anchor.kind === 'column')
|
|
151
|
-
return slot.config.columns.find((x) => x.id === anchor.columnId)?.label ?? anchor.columnId;
|
|
152
|
-
return anchor.quote;
|
|
119
|
+
/** The response-comment anchors this component may author: options take per-option, tables take rows only. */
|
|
120
|
+
export function commentAnchors(slot) {
|
|
121
|
+
if (!terminalAnswerable(slot))
|
|
122
|
+
return [];
|
|
123
|
+
if (slot.kind === 'options')
|
|
124
|
+
return slot.config.options.map((option) => ({ kind: 'option', optionId: option.id }));
|
|
125
|
+
if (slot.kind === 'table')
|
|
126
|
+
return (slot.config.rows ?? []).map((row) => ({ kind: 'row', rowId: row.id }));
|
|
127
|
+
return [];
|
|
153
128
|
}
|
|
154
129
|
export function awaitsResponse(manifest) {
|
|
155
|
-
return manifest.
|
|
130
|
+
return manifest.delivery.reply;
|
|
156
131
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Key } from './keys.js';
|
|
2
|
+
import type { ProductPageComponents } from '../../../types.js';
|
|
2
3
|
import type { CommentAnchor, PageManifest, PageResponses, PageSlot, SlotResponse } from '../../../core/human/page-schema.js';
|
|
3
|
-
import type { PageSourceProjection } from '../../../core/human/page.js';
|
|
4
4
|
export type Phase = 'overview' | 'item-review' | 'final';
|
|
5
5
|
export type InputMode = null | {
|
|
6
6
|
kind: 'comment';
|
|
7
7
|
buffer: string;
|
|
8
8
|
cursor: number;
|
|
9
|
-
anchor
|
|
9
|
+
anchor: CommentAnchor;
|
|
10
10
|
} | {
|
|
11
11
|
kind: 'freetext';
|
|
12
12
|
buffer: string;
|
|
@@ -21,11 +21,8 @@ export interface TuiState {
|
|
|
21
21
|
currentIndex: number;
|
|
22
22
|
manifest: PageManifest;
|
|
23
23
|
slots: PageSlot[];
|
|
24
|
-
overviewProse: string[];
|
|
25
|
-
slotProse: Map<number, string[]>;
|
|
26
|
-
finalProse: string[];
|
|
27
24
|
responses: Map<string, SlotResponse>;
|
|
28
|
-
productKinds:
|
|
25
|
+
productKinds: ProductPageComponents;
|
|
29
26
|
inputMode: InputMode;
|
|
30
27
|
selectedAction: number;
|
|
31
28
|
bodyMode: 'question';
|
|
@@ -42,8 +39,7 @@ export interface TuiState {
|
|
|
42
39
|
}
|
|
43
40
|
export interface MountedPanelOpts {
|
|
44
41
|
manifest: PageManifest;
|
|
45
|
-
|
|
46
|
-
productKinds?: string[];
|
|
42
|
+
productKinds?: ProductPageComponents;
|
|
47
43
|
initialResponses?: PageResponses | null;
|
|
48
44
|
cols: number;
|
|
49
45
|
rows: number;
|
|
@@ -58,9 +54,9 @@ export interface MountedPanel {
|
|
|
58
54
|
render(): string[];
|
|
59
55
|
handleResize(cols: number, rows: number): string[];
|
|
60
56
|
unmount(): void;
|
|
61
|
-
loadPage(manifest: PageManifest,
|
|
57
|
+
loadPage(manifest: PageManifest, opts?: {
|
|
62
58
|
initialResponses?: PageResponses | null;
|
|
63
|
-
productKinds?:
|
|
59
|
+
productKinds?: ProductPageComponents;
|
|
64
60
|
}): void;
|
|
65
61
|
canAcceptHostKeys(): boolean;
|
|
66
62
|
atPageTop(): boolean;
|
|
@@ -36,12 +36,12 @@ export function buildInboxLines(items, width, selectedIndex, height = Number.MAX
|
|
|
36
36
|
}
|
|
37
37
|
function buildItemLines(item, index, selectedIndex, contentWidth) {
|
|
38
38
|
const kind = item.kind === 'page'
|
|
39
|
-
? item.awaitsResponse === false ? 'display' :
|
|
39
|
+
? item.awaitsResponse === false ? 'display' : 'rich'
|
|
40
40
|
: item.kind;
|
|
41
41
|
const icon = KIND_ICON[kind] ?? '·';
|
|
42
42
|
// The inbox is a title-first triage surface. Origin details remain available
|
|
43
43
|
// after opening a ticket, rather than consuming the list's scarce row width.
|
|
44
|
-
const age = formatTimeAgo(item.blockedSince);
|
|
44
|
+
const age = formatTimeAgo(item.kind === 'page' ? item.emittedAt : item.blockedSince);
|
|
45
45
|
const cursor = index === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
|
|
46
46
|
const titleWidth = Math.max(1, contentWidth - age.length - 6);
|
|
47
47
|
const row = `${cursor}${ansiColor(icon, KIND_COLOR[kind] ?? 'cyan')} ${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
// Regression: a caller passing BOTH a positional AND piped stdin (e.g.
|
|
2
|
-
// `crtr node new --kind x "title" < prompt.md`, or `crtr human ask -
|
|
3
|
-
// <<EOF`) used to have the positional silently win, with the entire piped
|
|
4
|
-
// body dropped on the floor. `-` must always mean "read stdin", and any
|
|
5
|
-
// OTHER positional alongside genuinely piped, non-empty stdin must error
|
|
6
|
-
// loudly rather than silently pick one.
|
|
7
|
-
//
|
|
8
1
|
// Run with: node --import tsx/esm --test 'src/commands/__tests__/human.test.ts'
|
|
9
2
|
import { test, describe } from 'node:test';
|
|
10
3
|
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
11
7
|
import { Readable } from 'node:stream';
|
|
12
8
|
import { parseArgv } from '../../core/command.js';
|
|
13
|
-
import {
|
|
9
|
+
import { renderLeafArgv } from '../../core/help.js';
|
|
10
|
+
import { componentDoc, renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
|
|
11
|
+
import { humanComponents } from '../human/components.js';
|
|
12
|
+
import { humanSend } from '../human/prompts.js';
|
|
13
|
+
import { registerHuman } from '../human.js';
|
|
14
|
+
import { pageHelpText } from '../human/shared.js';
|
|
15
|
+
const nlThreadRegistration = {
|
|
16
|
+
kind: 'nl-thread',
|
|
17
|
+
description: 'message conversation with participants and metadata',
|
|
18
|
+
useWhen: 'use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide',
|
|
19
|
+
display: true,
|
|
20
|
+
doc: '`nl-thread` renders a conversation as bubbles down one column. Display only: it contributes nothing to the response.',
|
|
21
|
+
};
|
|
14
22
|
async function withPipedStdin(body, fn) {
|
|
15
23
|
const original = process.stdin;
|
|
16
24
|
const stdin = Readable.from([Buffer.from(body)]);
|
|
@@ -23,25 +31,111 @@ async function withPipedStdin(body, fn) {
|
|
|
23
31
|
Object.defineProperty(process, 'stdin', { value: original, configurable: true });
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
function withPageSurface(pageSurface, run) {
|
|
35
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-human-help-'));
|
|
36
|
+
const originalHome = process.env['HOME'];
|
|
37
|
+
process.env['HOME'] = home;
|
|
38
|
+
mkdirSync(join(home, '.crouter'), { recursive: true });
|
|
39
|
+
writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({ page_surface: pageSurface }));
|
|
40
|
+
try {
|
|
41
|
+
return run();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (originalHome === undefined)
|
|
45
|
+
delete process.env['HOME'];
|
|
46
|
+
else
|
|
47
|
+
process.env['HOME'] = originalHome;
|
|
48
|
+
rmSync(home, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
describe('human page input', () => {
|
|
52
|
+
test('stdin supplies the authored page document', async () => {
|
|
53
|
+
const document = 'export default () => <Page title="Question" />;\n';
|
|
54
|
+
const result = await withPipedStdin(document, () => parseArgv(humanSend.help.params ?? [], []));
|
|
55
|
+
assert.equal(result['document'], document);
|
|
56
|
+
});
|
|
57
|
+
test('empty stdin remains empty alongside --page', async () => {
|
|
58
|
+
const result = await withPipedStdin('', () => parseArgv(humanSend.help.params ?? [], ['--page', 'page.tsx']));
|
|
59
|
+
assert.equal(result['page'], 'page.tsx');
|
|
60
|
+
assert.equal(result['document'], '');
|
|
61
|
+
});
|
|
62
|
+
test('a positional document is rejected', async () => {
|
|
63
|
+
await assert.rejects(() => parseArgv(humanSend.help.params ?? [], ['page.tsx']), (err) => {
|
|
34
64
|
assert.equal(err.code, 'bad_invocation');
|
|
35
|
-
assert.match(err.message, /positional/);
|
|
36
|
-
assert.match(err.
|
|
37
|
-
assert.match(err.message, /Should we ship today\?/);
|
|
38
|
-
assert.equal(err.details?.field, 'question');
|
|
39
|
-
assert.match(err.details?.next ?? '', /exactly one/);
|
|
65
|
+
assert.match(err.message, /takes no positional arguments/);
|
|
66
|
+
assert.match(err.details?.next ?? '', /schema/);
|
|
40
67
|
return true;
|
|
41
68
|
});
|
|
42
69
|
});
|
|
43
|
-
test('
|
|
44
|
-
const
|
|
45
|
-
assert.
|
|
70
|
+
test('send help keeps page authoring at the human branch and makes reply routing explicit', () => {
|
|
71
|
+
const rendered = renderLeafArgv(humanSend.help);
|
|
72
|
+
assert.match(rendered, /^human send: send a durable TSX or HTML page\./);
|
|
73
|
+
assert.match(rendered, /Page authoring: `crtr human -h`\./);
|
|
74
|
+
assert.doesNotMatch(rendered, /--no-reply|--reply/);
|
|
75
|
+
assert.match(rendered, /page_id/);
|
|
76
|
+
assert.match(rendered, /reply/);
|
|
77
|
+
assert.doesNotMatch(rendered, /human notify|run\.json|job_id/);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
test('human help carries the page authoring model and follows the page-surface setting', () => {
|
|
81
|
+
withPageSurface(false, () => {
|
|
82
|
+
const human = registerHuman();
|
|
83
|
+
const help = pageHelpText(false);
|
|
84
|
+
assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
|
|
85
|
+
assert.ok(human.help.model?.includes(help.authoring));
|
|
86
|
+
assert.ok(human.help.model?.includes('`crtr human components`'));
|
|
87
|
+
assert.doesNotMatch(human.help.model ?? '', /doc check/);
|
|
88
|
+
assert.match(human.help.model ?? '', /\.html/);
|
|
89
|
+
});
|
|
90
|
+
withPageSurface(true, () => {
|
|
91
|
+
const human = registerHuman();
|
|
92
|
+
const help = pageHelpText(true);
|
|
93
|
+
assert.notEqual(help.authoring, pageHelpText(false).authoring);
|
|
94
|
+
assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
|
|
95
|
+
assert.ok(human.help.model?.includes(help.authoring));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// The gate itself, not its prose: a page-surface host must not carry the two
|
|
99
|
+
// terminal-only surfaces, because a review ticket filed there reaches a person
|
|
100
|
+
// who has no editor to open it in.
|
|
101
|
+
test('page_surface removes the terminal-only human surfaces from the tree', () => {
|
|
102
|
+
const names = () => registerHuman().children.map((child) => child.name);
|
|
103
|
+
withPageSurface(false, () => {
|
|
104
|
+
assert.deepEqual(names(), ['components', 'send', 'review', 'feedback', 'show', 'cancel', 'list', 'resolve']);
|
|
105
|
+
});
|
|
106
|
+
withPageSurface(true, () => {
|
|
107
|
+
const children = names();
|
|
108
|
+
assert.deepEqual(children, ['components', 'send', 'feedback', 'cancel', 'list', 'resolve']);
|
|
109
|
+
const human = registerHuman();
|
|
110
|
+
assert.doesNotMatch(human.help.model ?? '', /`review`|`show`/);
|
|
111
|
+
assert.doesNotMatch(human.rootEntry?.useWhen ?? '', /review a document|live display/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
test('human components catalog and contracts teach product registrations', () => {
|
|
115
|
+
const legacyRegistration = { kind: 'product-card' };
|
|
116
|
+
const catalog = renderComponentCatalog([nlThreadRegistration, legacyRegistration]);
|
|
117
|
+
assert.match(catalog, /^<components count="\d+">\nStructural — /);
|
|
118
|
+
assert.match(catalog, /\n`Page` — /);
|
|
119
|
+
assert.match(catalog, /\n`NlThread` \(kind `nl-thread`\) — message conversation with participants and metadata\. Use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide\. Display-only\n/);
|
|
120
|
+
assert.match(catalog, /\n`ProductCard` \(kind `product-card`\) — product-registered\n<\/components>$/);
|
|
121
|
+
assert.doesNotMatch(catalog, /Props\n|Response rules/);
|
|
122
|
+
// A product component is addressed by the JSX tag page-catalog derives from its registered
|
|
123
|
+
// kind; the contract heading names the wire kind that tag maps to.
|
|
124
|
+
assert.equal(renderComponentContract('nl-thread', [nlThreadRegistration]), `\`<NlThread>\` — product-registered, wire kind \`nl-thread\`\n\n${nlThreadRegistration.doc}`);
|
|
125
|
+
assert.equal(renderComponentContract('ProductCard', [legacyRegistration]), "`<ProductCard>` — product-registered, wire kind `product-card`\n\nproduct-registered; config is passed through unvalidated (unvalidated: true) — see the product's own docs");
|
|
126
|
+
});
|
|
127
|
+
test('human components show resolves every registry name, families included', async () => {
|
|
128
|
+
const show = humanComponents.children.find((child) => child.name === 'show');
|
|
129
|
+
assert.ok(show !== undefined && show.kind === 'leaf');
|
|
130
|
+
const question = await show.run({ component: 'UserQuestion' });
|
|
131
|
+
assert.ok(question !== undefined);
|
|
132
|
+
assert.equal(question['contract'], componentDoc('UserQuestion')?.contract);
|
|
133
|
+
assert.match(String(question['contract']), /wire kind `options`/);
|
|
134
|
+
// A shadcn family member resolves to the family contract that documents it.
|
|
135
|
+
const cardHeader = await show.run({ component: 'CardHeader' });
|
|
136
|
+
assert.equal(cardHeader?.['contract'], componentDoc('Card')?.contract);
|
|
137
|
+
await assert.rejects(() => show.run({ component: 'crtr-options' }), (err) => {
|
|
138
|
+
assert.equal(err.code, 'unknown_component');
|
|
139
|
+
return true;
|
|
46
140
|
});
|
|
47
141
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { renderLeafArgv } from '../../core/help.js';
|
|
4
|
+
import { nodeMessage } from '../node/message.js';
|
|
5
|
+
test('node message send help documents the finalized-node re-task gate', () => {
|
|
6
|
+
const send = nodeMessage.children.find((child) => child.kind === 'leaf' && child.name === 'send');
|
|
7
|
+
if (send === undefined || send.kind !== 'leaf')
|
|
8
|
+
assert.fail('node message send leaf is missing');
|
|
9
|
+
const rendered = renderLeafArgv(send.help);
|
|
10
|
+
assert.match(rendered, /--reopen/);
|
|
11
|
+
assert.match(rendered, /Required to re-task a finalized target/);
|
|
12
|
+
assert.match(rendered, /clears a finalized target's finalization latch before delivery/);
|
|
13
|
+
assert.match(rendered, /Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted/);
|
|
14
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `crtr canvas attention` — aggregate pending human tickets across the canvas.
|
|
2
2
|
//
|
|
3
|
-
// A pending "ticket" is any humanloop interaction awaiting
|
|
3
|
+
// A pending "ticket" is any humanloop interaction awaiting the user: a page or
|
|
4
4
|
// an anchored review. Each lives in its human bridge node
|
|
5
5
|
// directory and is attributed through that bridge's parent relation. This
|
|
6
6
|
// subtree surfaces a direct node projection; cwd is display metadata only.
|
|
@@ -93,7 +93,7 @@ const attentionCount = defineLeaf({
|
|
|
93
93
|
const attentionList = defineLeaf({
|
|
94
94
|
name: 'list',
|
|
95
95
|
description: 'itemised list of cwds with pending human tickets',
|
|
96
|
-
whenToUse: 'finding which agents are blocked waiting on
|
|
96
|
+
whenToUse: 'finding which agents are blocked waiting on the user — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
|
|
97
97
|
help: {
|
|
98
98
|
name: 'canvas attention list',
|
|
99
99
|
summary: 'list nodes that asked pending human tickets',
|
|
@@ -213,11 +213,11 @@ const attentionMap = defineLeaf({
|
|
|
213
213
|
export const attentionBranch = defineBranch({
|
|
214
214
|
name: 'attention',
|
|
215
215
|
description: 'count/list pending human tickets across the graph',
|
|
216
|
-
whenToUse: 'checking whether any agent on the canvas is blocked waiting on
|
|
216
|
+
whenToUse: 'checking whether any agent on the canvas is blocked waiting on the user, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
|
|
217
217
|
help: {
|
|
218
218
|
name: 'canvas attention',
|
|
219
219
|
summary: 'aggregate pending human tickets across the canvas',
|
|
220
|
-
model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting
|
|
220
|
+
model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting the user: an ask, a review, or a notification.',
|
|
221
221
|
},
|
|
222
222
|
children: [attentionCount, attentionList, attentionMap],
|
|
223
223
|
});
|