@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
|
@@ -2,17 +2,12 @@ import stringWidth from 'string-width';
|
|
|
2
2
|
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
3
3
|
import { answerCommentAnchorLabel, describePageAnswer } from '../../../core/human/answer.js';
|
|
4
4
|
import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, hardWrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
|
|
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';
|
|
8
7
|
const PAN_STEP = 8;
|
|
9
8
|
const mdLines = (md, maxW, paneW) => renderMarkdownBlockAwareLines(md, maxW, paneW);
|
|
10
|
-
function proseLines(blocks, maxW, paneW, dialect) {
|
|
11
|
-
const rendered = blocks.map((block) => dialect === 'html' ? renderHtmlProseAsMarkdown(block) : block).filter((block) => block.trim() !== '');
|
|
12
|
-
return rendered.flatMap((block, index) => [...(index > 0 ? [''] : []), ...mdLines(block, maxW, paneW)]);
|
|
13
|
-
}
|
|
14
9
|
function pageAnswer(state) {
|
|
15
|
-
return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: ''
|
|
10
|
+
return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: '' });
|
|
16
11
|
}
|
|
17
12
|
function answerSlot(answer, slot) {
|
|
18
13
|
return slot.id === undefined ? undefined : answer.slots.find((candidate) => candidate.id === slot.id);
|
|
@@ -36,10 +31,11 @@ export function renderOverview(state, cols, rows) {
|
|
|
36
31
|
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
37
32
|
const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)}${RESET}`];
|
|
38
33
|
if (state.manifest.subtitle)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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}`, '');
|
|
43
39
|
const answer = pageAnswer(state);
|
|
44
40
|
let priorStep = -1;
|
|
45
41
|
for (let i = 0; i < state.slots.length; i++) {
|
|
@@ -57,18 +53,64 @@ export function renderOverview(state, cols, rows) {
|
|
|
57
53
|
if (response !== undefined)
|
|
58
54
|
lines.push(` ${DIM}${truncate(singleLine(answerSummary(answerSlot(answer, slot))), Math.max(10, cols - 10))}${RESET}`);
|
|
59
55
|
}
|
|
60
|
-
if (
|
|
61
|
-
lines.push(
|
|
62
|
-
lines.push(
|
|
56
|
+
if (hasBody)
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(` ${DIM}${hline(maxW)}${RESET}`);
|
|
63
59
|
const display = !state.manifest.slots.some(isResponseBearingSlot);
|
|
64
60
|
lines.push(display ? ` ${DIM}q${RESET} dismiss` : ` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} close`);
|
|
65
61
|
return centeredWindow(lines, state, cols, rows, maxW + 2);
|
|
66
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. */
|
|
67
69
|
export function renderInputBuffer(buffer, cursor, maxWidth) {
|
|
68
70
|
const chars = [...buffer];
|
|
69
71
|
const at = Math.max(0, Math.min(cursor, chars.length));
|
|
70
|
-
const
|
|
71
|
-
|
|
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;
|
|
72
114
|
}
|
|
73
115
|
export function wrappedRowBounds(buffer, width) {
|
|
74
116
|
const chars = [...buffer];
|
|
@@ -129,10 +171,12 @@ function itemLines(state, cols) {
|
|
|
129
171
|
const maxW = Math.min(Math.max(20, cols - 4), 120);
|
|
130
172
|
const paneW = Math.max(20, cols - 4);
|
|
131
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}`];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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))
|
|
178
|
+
lines.push(` ${line}`);
|
|
179
|
+
}
|
|
136
180
|
if (!terminalAnswerable(slot))
|
|
137
181
|
lines.push('', ` ${DIM}${sanitize(slot.kind)} — cannot be answered in the terminal; open in Northlight${RESET}`);
|
|
138
182
|
else if (slot.kind === 'chart') {
|
|
@@ -157,7 +201,7 @@ function itemLines(state, cols) {
|
|
|
157
201
|
}
|
|
158
202
|
lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
|
|
159
203
|
if (state.inputMode !== null) {
|
|
160
|
-
const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor
|
|
204
|
+
const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor)}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
|
|
161
205
|
lines.push(` ${BOLD}${label}${RESET}`);
|
|
162
206
|
for (const line of renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, Math.max(10, maxW - 4)))
|
|
163
207
|
lines.push(` ${line}`);
|
|
@@ -171,7 +215,8 @@ function itemLines(state, cols) {
|
|
|
171
215
|
const mark = selected(state, slot, action.group, action.id) ? `${GREEN}✓${RESET}` : ' ';
|
|
172
216
|
lines.push(` ${cursor} [${mark}] ${action.shortcut ? `[${action.shortcut}] ` : ''}${sanitize(action.label)}`);
|
|
173
217
|
if (action.description)
|
|
174
|
-
|
|
218
|
+
for (const line of wrap(sanitize(action.description), Math.max(10, maxW - 6)))
|
|
219
|
+
lines.push(` ${DIM}${line}${RESET}`);
|
|
175
220
|
}
|
|
176
221
|
else {
|
|
177
222
|
const key = action.kind === 'freetext' ? 'r' : action.kind === 'edit' ? 'e' : 'a';
|
|
@@ -179,10 +224,11 @@ function itemLines(state, cols) {
|
|
|
179
224
|
}
|
|
180
225
|
});
|
|
181
226
|
for (const comment of responseComments(state, slot))
|
|
182
|
-
|
|
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}`);
|
|
183
229
|
if (state.hint)
|
|
184
230
|
lines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
|
|
185
|
-
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}`);
|
|
186
232
|
}
|
|
187
233
|
const maxLine = Math.max(0, ...lines.map(visibleWidth));
|
|
188
234
|
return { lines, maxW, maxH: Math.max(0, maxLine - Math.max(1, cols)) };
|
|
@@ -221,8 +267,6 @@ export function renderFinal(state, cols, rows) {
|
|
|
221
267
|
const mark = answered ? `${GREEN}✓${RESET}` : `${YELLOW}○${RESET}`;
|
|
222
268
|
lines.push(` ${mark} ${BOLD}${sanitize(manifestSlot === undefined ? slot.kind : describeSlot(manifestSlot))}${RESET} ${DIM}${sanitize(answerSummary(slot))}${RESET}`);
|
|
223
269
|
}
|
|
224
|
-
for (const line of proseLines(state.finalProse, maxW, Math.max(20, cols - 4), state.manifest.dialect))
|
|
225
|
-
lines.push(` ${line}`);
|
|
226
270
|
const complete = pageComplete(state);
|
|
227
271
|
if (!complete) {
|
|
228
272
|
lines.push('', ` ${YELLOW}Cannot submit yet.${RESET}`);
|
|
@@ -20,11 +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
27
|
export declare function slotAnswered(state: TuiState, slot: PageSlot): boolean;
|
|
28
28
|
export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'responses'>): boolean;
|
|
29
29
|
export declare function commentIdFor(anchor: CommentAnchor): string;
|
|
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[];
|
|
30
32
|
export declare function awaitsResponse(manifest: PageManifest): boolean;
|
|
@@ -18,7 +18,7 @@ export function describeSlot(slot) {
|
|
|
18
18
|
return 'Chart';
|
|
19
19
|
return slot.kind;
|
|
20
20
|
}
|
|
21
|
-
/** 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. */
|
|
22
22
|
export function terminalAnswerable(slot) {
|
|
23
23
|
if (slot.unvalidated === true)
|
|
24
24
|
return false;
|
|
@@ -34,18 +34,16 @@ export function slotActions(slot) {
|
|
|
34
34
|
const config = slot.config;
|
|
35
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 }));
|
|
36
36
|
if (config.allowFreetext === true)
|
|
37
|
-
rows.push({ kind: 'freetext', label: config.freetextLabel ?? '
|
|
37
|
+
rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Something else' });
|
|
38
38
|
}
|
|
39
39
|
else if (slot.kind === 'cards') {
|
|
40
40
|
const config = slot.config;
|
|
41
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 }));
|
|
42
42
|
}
|
|
43
43
|
else if (slot.kind === 'text') {
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
if (config.singleLine === true || config.editable !== false)
|
|
48
|
-
rows.push({ kind: 'edit', label: 'Edit' });
|
|
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' });
|
|
49
47
|
rows.push({ kind: 'accept', label: 'Accept as-is' });
|
|
50
48
|
}
|
|
51
49
|
else if (slot.kind === 'table') {
|
|
@@ -114,14 +112,20 @@ export function pageComplete(state) {
|
|
|
114
112
|
}
|
|
115
113
|
export function commentIdFor(anchor) {
|
|
116
114
|
switch (anchor.kind) {
|
|
117
|
-
case 'whole': return 'whole';
|
|
118
115
|
case 'option': return `option:${anchor.optionId}`;
|
|
119
|
-
case 'card': return `card:${anchor.cardId}`;
|
|
120
116
|
case 'row': return `row:${anchor.rowId}`;
|
|
121
|
-
case 'column': return `column:${anchor.columnId}`;
|
|
122
|
-
case 'range': return `range:${anchor.start}:${anchor.end}`;
|
|
123
117
|
}
|
|
124
118
|
}
|
|
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 [];
|
|
128
|
+
}
|
|
125
129
|
export function awaitsResponse(manifest) {
|
|
126
|
-
return manifest.
|
|
130
|
+
return manifest.delivery.reply;
|
|
127
131
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { Key } from './keys.js';
|
|
2
2
|
import type { ProductPageComponents } from '../../../types.js';
|
|
3
3
|
import type { CommentAnchor, PageManifest, PageResponses, PageSlot, SlotResponse } from '../../../core/human/page-schema.js';
|
|
4
|
-
import type { PageSourceProjection } from '../../../core/human/page.js';
|
|
5
4
|
export type Phase = 'overview' | 'item-review' | 'final';
|
|
6
5
|
export type InputMode = null | {
|
|
7
6
|
kind: 'comment';
|
|
8
7
|
buffer: string;
|
|
9
8
|
cursor: number;
|
|
10
|
-
anchor
|
|
9
|
+
anchor: CommentAnchor;
|
|
11
10
|
} | {
|
|
12
11
|
kind: 'freetext';
|
|
13
12
|
buffer: string;
|
|
@@ -22,9 +21,6 @@ export interface TuiState {
|
|
|
22
21
|
currentIndex: number;
|
|
23
22
|
manifest: PageManifest;
|
|
24
23
|
slots: PageSlot[];
|
|
25
|
-
overviewProse: string[];
|
|
26
|
-
slotProse: Map<number, string[]>;
|
|
27
|
-
finalProse: string[];
|
|
28
24
|
responses: Map<string, SlotResponse>;
|
|
29
25
|
productKinds: ProductPageComponents;
|
|
30
26
|
inputMode: InputMode;
|
|
@@ -43,7 +39,6 @@ export interface TuiState {
|
|
|
43
39
|
}
|
|
44
40
|
export interface MountedPanelOpts {
|
|
45
41
|
manifest: PageManifest;
|
|
46
|
-
projection: PageSourceProjection;
|
|
47
42
|
productKinds?: ProductPageComponents;
|
|
48
43
|
initialResponses?: PageResponses | null;
|
|
49
44
|
cols: number;
|
|
@@ -59,7 +54,7 @@ export interface MountedPanel {
|
|
|
59
54
|
render(): string[];
|
|
60
55
|
handleResize(cols: number, rows: number): string[];
|
|
61
56
|
unmount(): void;
|
|
62
|
-
loadPage(manifest: PageManifest,
|
|
57
|
+
loadPage(manifest: PageManifest, opts?: {
|
|
63
58
|
initialResponses?: PageResponses | null;
|
|
64
59
|
productKinds?: ProductPageComponents;
|
|
65
60
|
}): void;
|
|
@@ -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}`;
|
|
@@ -7,9 +7,9 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { Readable } from 'node:stream';
|
|
8
8
|
import { parseArgv } from '../../core/command.js';
|
|
9
9
|
import { renderLeafArgv } from '../../core/help.js';
|
|
10
|
-
import { renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
13
|
import { registerHuman } from '../human.js';
|
|
14
14
|
import { pageHelpText } from '../human/shared.js';
|
|
15
15
|
const nlThreadRegistration = {
|
|
@@ -17,62 +17,7 @@ const nlThreadRegistration = {
|
|
|
17
17
|
description: 'message conversation with participants and metadata',
|
|
18
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
19
|
display: true,
|
|
20
|
-
doc:
|
|
21
|
-
'`nl-thread` renders a conversation as bubbles down one column: your own messages on the right in the accent, the other party on the left, consecutive messages from one sender grouped under a single avatar.',
|
|
22
|
-
'',
|
|
23
|
-
'Display only: it contributes nothing to the response, stays readable and expandable on a resolved page, and does not require a slot `id`.',
|
|
24
|
-
'',
|
|
25
|
-
'## Config',
|
|
26
|
-
'',
|
|
27
|
-
'| field | type | required | meaning |',
|
|
28
|
-
'| --- | --- | --- | --- |',
|
|
29
|
-
'| `label` | string | no | the heading above the thread - what this conversation is, in your own words |',
|
|
30
|
-
'| `platform` | string | no | where it happened (`email`, `linkedin`, `imessage`, `slack`); shown as a small chip |',
|
|
31
|
-
'| `messages` | message[] | no | the conversation, oldest first; an empty or absent list renders "No messages in this thread." |',
|
|
32
|
-
'| `participants` | participant[] | no | display identities, joined to messages by handle |',
|
|
33
|
-
'| `collapsedByDefault` | boolean | no | open on the last 3 messages, the rest behind a "Show N earlier messages" control |',
|
|
34
|
-
'',
|
|
35
|
-
'Unknown config properties are rejected: the slot then renders an unavailable notice naming the offending key.',
|
|
36
|
-
'',
|
|
37
|
-
'### message',
|
|
38
|
-
'',
|
|
39
|
-
'| field | type | required | meaning |',
|
|
40
|
-
'| --- | --- | --- | --- |',
|
|
41
|
-
'| `sender` | string | yes | `self` is the viewer, `them` the unnamed other party; any other value is a named sender |',
|
|
42
|
-
'| `body` | string | yes | plain text; authored newlines are preserved, markdown is NOT rendered |',
|
|
43
|
-
'| `senderName` | string | no | display name for this message, winning over the participant roster |',
|
|
44
|
-
'| `senderHandle` | string | no | the raw address - phone, email, profile id - joined against `participants` |',
|
|
45
|
-
'| `timestamp` | string | no | ISO 8601; a clock within today, date + time before that. Anything unparseable is shown verbatim |',
|
|
46
|
-
'',
|
|
47
|
-
'### participant',
|
|
48
|
-
'',
|
|
49
|
-
'| field | type | required | meaning |',
|
|
50
|
-
'| --- | --- | --- | --- |',
|
|
51
|
-
'| `handle` | string | yes | matches the `senderHandle` on a message |',
|
|
52
|
-
'| `name` | string | no | display name for every message from that handle |',
|
|
53
|
-
'| `avatarDataUrl` | string | no | a `data:image/...` URL ONLY. A page never fetches, so an http(s) avatar is rejected - omit it and initials are drawn instead |',
|
|
54
|
-
'',
|
|
55
|
-
'There is no `source` field and no artifact reference: messages are always inline in the config.',
|
|
56
|
-
'',
|
|
57
|
-
'## Example',
|
|
58
|
-
'',
|
|
59
|
-
'```html',
|
|
60
|
-
'<crtr-slot kind="nl-thread" slot-id="conversation">',
|
|
61
|
-
' <script type="application/json">',
|
|
62
|
-
' {',
|
|
63
|
-
' "label": "Your thread with Dana Whitfield",',
|
|
64
|
-
' "platform": "linkedin",',
|
|
65
|
-
' "collapsedByDefault": true,',
|
|
66
|
-
' "participants": [{ "handle": "dana-whitfield", "name": "Dana Whitfield" }],',
|
|
67
|
-
' "messages": [',
|
|
68
|
-
' { "sender": "them", "senderHandle": "dana-whitfield", "timestamp": "2026-03-02T15:04:00Z", "body": "Saw the launch, congrats." },',
|
|
69
|
-
' { "sender": "self", "timestamp": "2026-03-02T16:20:00Z", "body": "Thank you! Want a walkthrough next week?" }',
|
|
70
|
-
' ]',
|
|
71
|
-
' }',
|
|
72
|
-
' </script>',
|
|
73
|
-
'</crtr-slot>',
|
|
74
|
-
'```',
|
|
75
|
-
].join('\n'),
|
|
20
|
+
doc: '`nl-thread` renders a conversation as bubbles down one column. Display only: it contributes nothing to the response.',
|
|
76
21
|
};
|
|
77
22
|
async function withPipedStdin(body, fn) {
|
|
78
23
|
const original = process.stdin;
|
|
@@ -86,12 +31,12 @@ async function withPipedStdin(body, fn) {
|
|
|
86
31
|
Object.defineProperty(process, 'stdin', { value: original, configurable: true });
|
|
87
32
|
}
|
|
88
33
|
}
|
|
89
|
-
function
|
|
34
|
+
function withPageSurface(pageSurface, run) {
|
|
90
35
|
const home = mkdtempSync(join(tmpdir(), 'crtr-human-help-'));
|
|
91
36
|
const originalHome = process.env['HOME'];
|
|
92
37
|
process.env['HOME'] = home;
|
|
93
38
|
mkdirSync(join(home, '.crouter'), { recursive: true });
|
|
94
|
-
writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({
|
|
39
|
+
writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({ page_surface: pageSurface }));
|
|
95
40
|
try {
|
|
96
41
|
return run();
|
|
97
42
|
}
|
|
@@ -105,80 +50,92 @@ function withHtmlSurface(htmlSurface, run) {
|
|
|
105
50
|
}
|
|
106
51
|
describe('human page input', () => {
|
|
107
52
|
test('stdin supplies the authored page document', async () => {
|
|
108
|
-
const document = '
|
|
109
|
-
const result = await withPipedStdin(document, () => parseArgv(
|
|
53
|
+
const document = 'export default () => <Page title="Question" />;\n';
|
|
54
|
+
const result = await withPipedStdin(document, () => parseArgv(humanSend.help.params ?? [], []));
|
|
110
55
|
assert.equal(result['document'], document);
|
|
111
56
|
});
|
|
112
57
|
test('empty stdin remains empty alongside --page', async () => {
|
|
113
|
-
const result = await withPipedStdin('', () => parseArgv(
|
|
114
|
-
assert.equal(result['page'], 'page.
|
|
58
|
+
const result = await withPipedStdin('', () => parseArgv(humanSend.help.params ?? [], ['--page', 'page.tsx']));
|
|
59
|
+
assert.equal(result['page'], 'page.tsx');
|
|
115
60
|
assert.equal(result['document'], '');
|
|
116
61
|
});
|
|
117
62
|
test('a positional document is rejected', async () => {
|
|
118
|
-
await assert.rejects(() => parseArgv(
|
|
63
|
+
await assert.rejects(() => parseArgv(humanSend.help.params ?? [], ['page.tsx']), (err) => {
|
|
119
64
|
assert.equal(err.code, 'bad_invocation');
|
|
120
65
|
assert.match(err.message, /takes no positional arguments/);
|
|
121
66
|
assert.match(err.details?.next ?? '', /schema/);
|
|
122
67
|
return true;
|
|
123
68
|
});
|
|
124
69
|
});
|
|
125
|
-
test('
|
|
126
|
-
const
|
|
127
|
-
assert.
|
|
128
|
-
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/);
|
|
129
78
|
});
|
|
130
79
|
});
|
|
131
|
-
test('human help
|
|
132
|
-
|
|
133
|
-
const originalRootUseWhen = 'when a person must decide, review a document with you, or receive a live display.';
|
|
134
|
-
const htmlAuthoring = 'Use inline HTML pages as the default way to put structured or interactive content in front of the human; the environment renders web content, so a page is not a special occasion. Write one complete HTML document to `$CRTR_CONTEXT_DIR/pages/<name>.html`. Its `<head>` has required `<meta name="crtr-page-title" content="…">`, optional `<meta name="crtr-page-subtitle" content="…">`, and optional `<meta name="crtr-page-placement" content="inline|panel">` (default `panel`); interactive or multi-step pages need one `<crtr-pages>` wrapper with `<crtr-page>` steps, and each component config is its first `<script type="application/json">` child. Display-only pages are first-class: a page with no questions is a legitimate, encouraged way to show structured UI with `notify --page`.';
|
|
135
|
-
const htmlRootUseWhen = 'when a person must decide, review a document with you, or receive a live display; use inline HTML pages by default for structured or interactive content because the environment renders web content.';
|
|
136
|
-
withHtmlSurface(false, () => {
|
|
80
|
+
test('human help carries the page authoring model and follows the page-surface setting', () => {
|
|
81
|
+
withPageSurface(false, () => {
|
|
137
82
|
const human = registerHuman();
|
|
138
|
-
|
|
139
|
-
assert.equal(human.rootEntry?.useWhen,
|
|
140
|
-
assert.ok(human.help.model?.includes(
|
|
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/);
|
|
141
89
|
});
|
|
142
|
-
|
|
90
|
+
withPageSurface(true, () => {
|
|
143
91
|
const human = registerHuman();
|
|
144
|
-
|
|
145
|
-
assert.
|
|
146
|
-
assert.
|
|
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));
|
|
147
96
|
});
|
|
148
97
|
});
|
|
149
|
-
|
|
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', () => {
|
|
150
115
|
const legacyRegistration = { kind: 'product-card' };
|
|
151
116
|
const catalog = renderComponentCatalog([nlThreadRegistration, legacyRegistration]);
|
|
152
|
-
assert.match(catalog, /^<components count="
|
|
153
|
-
assert.match(catalog, /\n`
|
|
154
|
-
assert.match(catalog, /\n`
|
|
155
|
-
assert.
|
|
156
|
-
assert.
|
|
157
|
-
|
|
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");
|
|
158
126
|
});
|
|
159
|
-
test('human
|
|
160
|
-
const show =
|
|
127
|
+
test('human components show resolves every registry name, families included', async () => {
|
|
128
|
+
const show = humanComponents.children.find((child) => child.name === 'show');
|
|
161
129
|
assert.ok(show !== undefined && show.kind === 'leaf');
|
|
162
|
-
const
|
|
163
|
-
assert.ok(
|
|
164
|
-
assert.equal(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Config JSON
|
|
175
|
-
- \`options\` — required nonempty array of \`{id:string (nonempty, unique), label:string (nonempty), description?:string}\`.
|
|
176
|
-
- \`mode\` — required: \`single\` or \`multi\`.
|
|
177
|
-
- \`label?:string\` — slot label; required by the multi-response page rule.
|
|
178
|
-
- \`allowFreetext?:boolean\`, \`freetextLabel?:string\`, \`freetextPlaceholder?:string\` — optional free-text affordance fields.
|
|
179
|
-
|
|
180
|
-
Response rules
|
|
181
|
-
- \`selectedOptionIds\` contains unique known option ids. A \`single\` response has at most one selected id and requires exactly one selection or nonempty allowed \`freetext\`; a \`multi\` response requires a selection, comment, or nonempty allowed \`freetext\`.
|
|
182
|
-
- \`freetext\` is accepted only when \`allowFreetext:true\`.
|
|
183
|
-
- Comments are \`{id:string (nonempty), text:string, anchor:Anchor}\`. Every response carries \`comments\` as an array. \`Anchor\` is one of \`{kind:"whole"}\`, \`{kind:"option",optionId:string}\`, \`{kind:"row",rowId:string}\`, \`{kind:"column",columnId:string}\`, \`{kind:"card",cardId:string}\`, or \`{kind:"range",start:nonnegative integer,end:nonnegative integer >= start,quote:string}\`; each component accepts only the anchors named below. Options accept only \`whole\` and \`option\` anchors; \`optionId\` must name a configured option.`);
|
|
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;
|
|
140
|
+
});
|
|
184
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
|
});
|
|
@@ -15,7 +15,7 @@ export const canvasSnapshotLeaf = defineLeaf({
|
|
|
15
15
|
{ name: 'subscriptions', type: 'object', required: true, constraint: 'Authoritative subscribes_to adjacency map: each node id maps to its publisher/child node ids in edge order.' },
|
|
16
16
|
],
|
|
17
17
|
outputKind: 'object',
|
|
18
|
-
effects: ['Read-only: queries canvas.db, focus rows, telemetry files, and human
|
|
18
|
+
effects: ['Read-only: queries canvas.db, focus rows, telemetry files, and pending human-reply counts.'],
|
|
19
19
|
// Machine-readable dump for the browser canvas, not for an agent/human to
|
|
20
20
|
// glance at — collapse it in the attach viewer by default.
|
|
21
21
|
preview: { suppressOutput: true },
|
package/dist/commands/canvas.js
CHANGED
|
@@ -43,7 +43,7 @@ export function registerCanvas() {
|
|
|
43
43
|
help: {
|
|
44
44
|
name: 'canvas',
|
|
45
45
|
summary: 'observe and supervise the whole agent graph',
|
|
46
|
-
model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine write verb (`push`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending
|
|
46
|
+
model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine write verb (`push`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending replies from the user across the graph; `revive --all` mass-reconnects every disconnected node (bringing back ONE named node is `node lifecycle revive <id>` instead); `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `prune` bounds growth by deleting terminal nodes past a TTL. `config` registers/lists remote canvas targets (preview endpoint + relay token); `use` switches remote-capable read commands such as `node inspect` onto a configured target or back to local.',
|
|
47
47
|
},
|
|
48
48
|
children: [dashboardLeaf, canvasSnapshotLeaf, canvasRosterLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, chordLeaf, issueBranch, canvasPruneLeaf, canvasRebuildIndexLeaf, canvasConfigBranch, canvasUseLeaf],
|
|
49
49
|
});
|