@north-light/crouter 0.3.231 → 0.3.232
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 +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +9 -5
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/on-read.js +2 -1
- package/dist/core/substrate/surface-match.js +164 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
|
@@ -9,7 +9,8 @@ import { parseArgv } from '../../core/command.js';
|
|
|
9
9
|
import { renderLeafArgv } from '../../core/help.js';
|
|
10
10
|
import { componentDoc, renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
|
|
11
11
|
import { humanComponents } from '../human/components.js';
|
|
12
|
-
import { humanSend } from '../human/prompts.js';
|
|
12
|
+
import { humanSend as buildHumanSend } from '../human/prompts.js';
|
|
13
|
+
const humanSend = buildHumanSend();
|
|
13
14
|
import { registerHuman } from '../human.js';
|
|
14
15
|
import { pageHelpText } from '../human/shared.js';
|
|
15
16
|
const nlThreadRegistration = {
|
|
@@ -69,7 +70,10 @@ describe('human page input', () => {
|
|
|
69
70
|
});
|
|
70
71
|
test('send help keeps page authoring at the human branch and makes reply routing explicit', () => {
|
|
71
72
|
const rendered = renderLeafArgv(humanSend.help);
|
|
72
|
-
assert.match(rendered, /^human send: send a durable TSX
|
|
73
|
+
assert.match(rendered, /^human send: send a durable TSX page\./);
|
|
74
|
+
// HTML is named on the send leaf only where an HTML page can be read.
|
|
75
|
+
assert.doesNotMatch(rendered, /HTML|\.html/);
|
|
76
|
+
assert.match(renderLeafArgv(buildHumanSend(true).help), /^human send: send a durable TSX or HTML page\./);
|
|
73
77
|
assert.match(rendered, /Page authoring: `crtr human -h`\./);
|
|
74
78
|
assert.doesNotMatch(rendered, /--no-reply|--reply/);
|
|
75
79
|
assert.match(rendered, /page_id/);
|
|
@@ -85,7 +89,12 @@ test('human help carries the page authoring model and follows the page-surface s
|
|
|
85
89
|
assert.ok(human.help.model?.includes(help.authoring));
|
|
86
90
|
assert.ok(human.help.model?.includes('`crtr human components`'));
|
|
87
91
|
assert.doesNotMatch(human.help.model ?? '', /doc check/);
|
|
88
|
-
|
|
92
|
+
// A terminal renders an HTML page as its title and nothing else, so the
|
|
93
|
+
// dialect is not offered here; what IS offered is the reason each question
|
|
94
|
+
// must stand alone on this host.
|
|
95
|
+
assert.doesNotMatch(human.help.model ?? '', /\.html/);
|
|
96
|
+
assert.match(human.help.model ?? '', /one question at a time/);
|
|
97
|
+
assert.match(human.help.model ?? '', /rather than running your module/);
|
|
89
98
|
});
|
|
90
99
|
withPageSurface(true, () => {
|
|
91
100
|
const human = registerHuman();
|
|
@@ -93,6 +102,12 @@ test('human help carries the page authoring model and follows the page-surface s
|
|
|
93
102
|
assert.notEqual(help.authoring, pageHelpText(false).authoring);
|
|
94
103
|
assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
|
|
95
104
|
assert.ok(human.help.model?.includes(help.authoring));
|
|
105
|
+
assert.match(human.help.model ?? '', /\.html/);
|
|
106
|
+
// The flattening warning is terminal-only: a page surface runs the module.
|
|
107
|
+
assert.doesNotMatch(human.help.model ?? '', /rather than running your module/);
|
|
108
|
+
// Both surfaces reach for pages by default; only the reason differs.
|
|
109
|
+
for (const surface of [true, false])
|
|
110
|
+
assert.match(pageHelpText(surface).authoring, /Use inline pages as the default way/);
|
|
96
111
|
});
|
|
97
112
|
});
|
|
98
113
|
// The gate itself, not its prose: a page-surface host must not carry the two
|
|
@@ -2,13 +2,13 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { renderLeafArgv } from '../../core/help.js';
|
|
4
4
|
import { nodeMessage } from '../node/message.js';
|
|
5
|
-
test('node message send help documents the
|
|
5
|
+
test('node message send help documents the reopen re-task semantics', () => {
|
|
6
6
|
const send = nodeMessage.children.find((child) => child.kind === 'leaf' && child.name === 'send');
|
|
7
7
|
if (send === undefined || send.kind !== 'leaf')
|
|
8
8
|
assert.fail('node message send leaf is missing');
|
|
9
9
|
const rendered = renderLeafArgv(send.help);
|
|
10
10
|
assert.match(rendered, /--reopen/);
|
|
11
|
-
assert.match(rendered, /
|
|
12
|
-
assert.match(rendered, /
|
|
11
|
+
assert.match(rendered, /Re-task the target: commits it resident and clears any finalization latch before delivery/);
|
|
12
|
+
assert.match(rendered, /valid for live, parked, and finalized targets/);
|
|
13
13
|
assert.match(rendered, /Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted/);
|
|
14
14
|
});
|
|
@@ -18,7 +18,7 @@ import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { CrtrClient, ApiError } from '../api/index.js';
|
|
20
20
|
import { apiErrorToCliError } from '../core/io.js';
|
|
21
|
-
import { ensureDaemon } from '../daemon/manage.js';
|
|
21
|
+
import { autostartDisabled, ensureDaemon } from '../daemon/manage.js';
|
|
22
22
|
import { DAEMON_STARTUP_WINDOW_MS } from '../daemon/startup-policy.js';
|
|
23
23
|
import { readState } from '../core/config.js';
|
|
24
24
|
import { resolveRemoteCanvasTarget } from '../core/remote-canvas-target.js';
|
|
@@ -60,12 +60,6 @@ export async function getNodeOrNull(nodeId) {
|
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
// cliClient — the one place the CLI constructs a daemon client
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
|
-
/** Whether autostart is suppressed for this invocation. `--no-autostart` or
|
|
64
|
-
* `CRTR_NO_DAEMON_AUTOSTART=1` forces fail-loud on a cold socket (spec §7.1) —
|
|
65
|
-
* for environments that manage crtrd externally and want the diagnostic. */
|
|
66
|
-
function autostartDisabled() {
|
|
67
|
-
return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1' || process.argv.includes('--no-autostart');
|
|
68
|
-
}
|
|
69
63
|
/** Bounded tail of crtrd.err surfaced on a cold-start `/healthz` timeout.
|
|
70
64
|
* Large enough to catch a real startup failure/stack trace,
|
|
71
65
|
* small enough that a runaway daemon log can never leak unbounded into a CLI
|
|
@@ -41,13 +41,7 @@ const canvasConfigAdd = defineLeaf({
|
|
|
41
41
|
{ kind: 'flag', name: 'relay-token', type: 'string', required: true, constraint: 'Must be exactly `-` — the raw token is read from stdin, never argv (keeps it out of shell history). Any other value is rejected.' },
|
|
42
42
|
{ kind: 'flag', name: 'cp', type: 'string', required: false, constraint: 'Optional control-plane origin, when this target has one distinct from the preview endpoint.' },
|
|
43
43
|
],
|
|
44
|
-
output: [
|
|
45
|
-
{ name: 'name', type: 'string', required: true, constraint: 'The target name just registered.' },
|
|
46
|
-
{ name: 'previewEndpoint', type: 'string', required: true, constraint: 'The stored preview endpoint.' },
|
|
47
|
-
{ name: 'relayTokenRef', type: 'string', required: true, constraint: 'The secrets-store key the token was written under (equals name).' },
|
|
48
|
-
{ name: 'cpOrigin', type: 'string', required: false, constraint: 'The stored control-plane origin, when --cp was given.' },
|
|
49
|
-
{ name: 'tokenStored', type: 'boolean', required: true, constraint: 'Always true on success \u2014 confirms the token was written to the 0600 store.' },
|
|
50
|
-
],
|
|
44
|
+
output: [],
|
|
51
45
|
outputKind: 'object',
|
|
52
46
|
effects: ['Writes remoteCanvas.targets.<name> to config.json (previewEndpoint, relayTokenRef, cpOrigin).', 'Writes the raw token to the 0600 secrets store under relayTokenRef \u2014 never to config.json or stdout.'],
|
|
53
47
|
},
|
|
@@ -78,9 +72,9 @@ const canvasConfigAdd = defineLeaf({
|
|
|
78
72
|
cfg.remoteCanvas.targets[name] = target;
|
|
79
73
|
});
|
|
80
74
|
setRelayToken(name, token, SCOPE);
|
|
81
|
-
return {
|
|
75
|
+
return {};
|
|
82
76
|
},
|
|
83
|
-
render: (
|
|
77
|
+
render: () => 'Registered remote canvas target (token stored).',
|
|
84
78
|
});
|
|
85
79
|
// ---------------------------------------------------------------------------
|
|
86
80
|
// canvas config list
|
|
@@ -169,9 +163,7 @@ const canvasConfigRemove = defineLeaf({
|
|
|
169
163
|
params: [
|
|
170
164
|
{ kind: 'positional', name: 'name', required: true, constraint: 'Target name.' },
|
|
171
165
|
],
|
|
172
|
-
output: [
|
|
173
|
-
{ name: 'removed', type: 'string', required: true, constraint: 'The target name removed.' },
|
|
174
|
-
],
|
|
166
|
+
output: [],
|
|
175
167
|
outputKind: 'object',
|
|
176
168
|
effects: ['Deletes remoteCanvas.targets.<name> from config.json.', 'Deletes the stored token under relayTokenRef from the secrets store.'],
|
|
177
169
|
},
|
|
@@ -190,9 +182,9 @@ const canvasConfigRemove = defineLeaf({
|
|
|
190
182
|
delete c.remoteCanvas.targets[name];
|
|
191
183
|
});
|
|
192
184
|
deleteRelayToken(target.relayTokenRef, SCOPE);
|
|
193
|
-
return {
|
|
185
|
+
return {};
|
|
194
186
|
},
|
|
195
|
-
render: (
|
|
187
|
+
render: () => 'Removed remote canvas target.'
|
|
196
188
|
});
|
|
197
189
|
// ---------------------------------------------------------------------------
|
|
198
190
|
// Export — mounted under `crtr canvas`
|
|
@@ -18,9 +18,7 @@ export const canvasUseLeaf = defineLeaf({
|
|
|
18
18
|
{ kind: 'positional', name: 'name', required: false, constraint: 'A remote canvas target registered via `crtr canvas config add`. Mutually exclusive with --local; exactly one of the two is required.' },
|
|
19
19
|
{ kind: 'flag', name: 'local', type: 'bool', required: false, constraint: 'Switch back to the local canvas. Mutually exclusive with <name>.' },
|
|
20
20
|
],
|
|
21
|
-
output: [
|
|
22
|
-
{ name: 'active', type: 'string', required: false, constraint: 'The now-active target name, or omitted/null for local.' },
|
|
23
|
-
],
|
|
21
|
+
output: [],
|
|
24
22
|
outputKind: 'object',
|
|
25
23
|
effects: ['Writes state.json\u2019s activeCanvas (user scope).'],
|
|
26
24
|
},
|
|
@@ -45,7 +43,7 @@ export const canvasUseLeaf = defineLeaf({
|
|
|
45
43
|
updateState(SCOPE, (s) => {
|
|
46
44
|
s.activeCanvas = null;
|
|
47
45
|
});
|
|
48
|
-
return {
|
|
46
|
+
return {};
|
|
49
47
|
}
|
|
50
48
|
if (getRemoteCanvasTarget(name, SCOPE) === undefined) {
|
|
51
49
|
throw new InputError({
|
|
@@ -57,7 +55,7 @@ export const canvasUseLeaf = defineLeaf({
|
|
|
57
55
|
updateState(SCOPE, (s) => {
|
|
58
56
|
s.activeCanvas = name;
|
|
59
57
|
});
|
|
60
|
-
return {
|
|
58
|
+
return {};
|
|
61
59
|
},
|
|
62
|
-
render: (
|
|
60
|
+
render: () => 'Canvas selection updated.'
|
|
63
61
|
});
|
package/dist/commands/cron.js
CHANGED
|
@@ -18,14 +18,14 @@ import { resolve as resolvePath } from 'node:path';
|
|
|
18
18
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
19
19
|
import { InputError, readStdinRaw } from '../core/io.js';
|
|
20
20
|
import { parseWhen, parseCadence, cadenceDisplay } from '../core/wake.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveProfileOperand } from '../core/profiles/manifest.js';
|
|
22
22
|
import { profilesStateBlock } from '../core/profiles/state-block.js';
|
|
23
23
|
import { cliClient, rethrowAsCliError } from './api-client.js';
|
|
24
|
-
/** Canonicalize a profile operand (id
|
|
24
|
+
/** Canonicalize a profile operand (id, manifest name, or nearest match) to its profile id so
|
|
25
25
|
* every cron row and scope query speaks the same identity. Resolution errors
|
|
26
26
|
* name the available profiles on this host. */
|
|
27
27
|
function canonicalProfileId(raw) {
|
|
28
|
-
return
|
|
28
|
+
return resolveProfileOperand(raw.trim()).profileId;
|
|
29
29
|
}
|
|
30
30
|
/** Build ParseOpts, including `tz` only when actually provided. */
|
|
31
31
|
function parseOpts(now, tz) {
|
|
@@ -147,12 +147,11 @@ const addLeaf = defineLeaf({
|
|
|
147
147
|
{ kind: 'flag', name: 'overlap', type: 'enum', choices: ['skip', 'queue', 'replace'], required: false, default: 'skip', constraint: 'A fire landing while the previous run is still going. skip (default): log the skip, do not start a second run. queue: run once more after the current finishes (depth 1; further fires collapse). replace: kill the running instance and start fresh.' },
|
|
148
148
|
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Directory every run executes in. Defaults to this one, must exist now, and is snapshotted onto the row — a cron never re-resolves its context.' },
|
|
149
149
|
{ kind: 'flag', name: 'env', type: 'string', required: false, repeatable: true, constraint: 'KEY=VALUE added to every run\'s env (repeat per variable). Snapshotted at arm time; overlays the operational env, alongside $CRTR_CRON_ID/$CRTR_CRON_NAME/$CRTR_RUN_ID.' },
|
|
150
|
-
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile the runs resolve config and credentials under, and — under --scope profile — the profile this cron lists and cancels within. A profile id or
|
|
150
|
+
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile the runs resolve config and credentials under, and — under --scope profile — the profile this cron lists and cancels within. A profile id or manifest name from the <profiles> list below; a close-enough name resolves to the nearest profile. Defaults to the calling node\'s ($CRTR_PROFILE_ID).' },
|
|
151
151
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['profile', 'global'], required: false, constraint: 'Who lists and cancels this cron: profile (default when a profile is resolved) confines it to that profile; global makes it canvas-home-wide, visible from every profile.' },
|
|
152
152
|
],
|
|
153
153
|
output: [
|
|
154
|
-
{ name: 'cron_id', type: 'string', required: true, constraint: 'The armed cron — inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`.' },
|
|
155
|
-
{ name: 'name', type: 'string', required: true, constraint: 'The label as armed.' },
|
|
154
|
+
{ name: 'cron_id', type: 'string', required: true, constraint: 'The server-minted armed cron id — inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`.' },
|
|
156
155
|
{ name: 'fire_at', type: 'string', required: true, constraint: 'Next fire (UTC ISO).' },
|
|
157
156
|
{ name: 'recur', type: 'string', required: true, constraint: 'Cadence display; "none" for a one-shot.' },
|
|
158
157
|
],
|
|
@@ -267,12 +266,12 @@ const addLeaf = defineLeaf({
|
|
|
267
266
|
overlap: input['overlap'] ?? 'skip',
|
|
268
267
|
})
|
|
269
268
|
.catch(rethrowAsCliError);
|
|
270
|
-
return { cron_id: dto.cron_id,
|
|
269
|
+
return { cron_id: dto.cron_id, fire_at: dto.fire_at, recur: cadenceDisplay(dto.recur) };
|
|
271
270
|
},
|
|
272
271
|
render: (r) => {
|
|
273
272
|
const recur = String(r['recur']);
|
|
274
273
|
const cadence = recur === 'none' ? 'one-shot (row deleted after the run)' : recur;
|
|
275
|
-
return `Cron armed — ${String(r['
|
|
274
|
+
return `Cron armed — ${String(r['cron_id'])}.\n\n- next fire: ${String(r['fire_at'])}\n- cadence: ${cadence}\n\nIt runs daemon-side; nothing more to do here. Inspect it with \`crtr cron show ${String(r['cron_id'])}\`; cancel any time with \`crtr cron cancel ${String(r['cron_id'])}\`.`;
|
|
276
275
|
},
|
|
277
276
|
});
|
|
278
277
|
// ---------------------------------------------------------------------------
|
|
@@ -460,19 +459,16 @@ const pauseLeaf = defineLeaf({
|
|
|
460
459
|
params: [
|
|
461
460
|
{ kind: 'positional', name: 'cron-id', required: true, constraint: 'The cron to pause (from `cron list`).' },
|
|
462
461
|
],
|
|
463
|
-
output: [
|
|
464
|
-
{ name: 'cron_id', type: 'string', required: true, constraint: 'The paused cron.' },
|
|
465
|
-
{ name: 'state', type: 'string', required: true, constraint: 'Always "paused".' },
|
|
466
|
-
],
|
|
462
|
+
output: [],
|
|
467
463
|
outputKind: 'object',
|
|
468
464
|
effects: ['Stops the cron firing until `cron resume`. An in-flight run settles normally; the row just stops being due.'],
|
|
469
465
|
},
|
|
470
466
|
run: async (input) => {
|
|
471
467
|
const id = requireCronId(input);
|
|
472
|
-
|
|
473
|
-
return {
|
|
468
|
+
await cliClient().pauseCron(id, scopeQuery()).catch((err) => rethrowAsCliError(err, 'Check the id against `crtr cron list`.'));
|
|
469
|
+
return {};
|
|
474
470
|
},
|
|
475
|
-
render: (
|
|
471
|
+
render: () => 'Cron paused.'
|
|
476
472
|
});
|
|
477
473
|
const resumeLeaf = defineLeaf({
|
|
478
474
|
name: 'resume',
|
|
@@ -485,8 +481,6 @@ const resumeLeaf = defineLeaf({
|
|
|
485
481
|
{ kind: 'positional', name: 'cron-id', required: true, constraint: 'The cron to resume (from `cron list`).' },
|
|
486
482
|
],
|
|
487
483
|
output: [
|
|
488
|
-
{ name: 'cron_id', type: 'string', required: true, constraint: 'The resumed cron.' },
|
|
489
|
-
{ name: 'state', type: 'string', required: true, constraint: 'Always "active".' },
|
|
490
484
|
{ name: 'fire_at', type: 'string', required: true, constraint: 'Next fire — if already past, it fires on the next daemon tick.' },
|
|
491
485
|
],
|
|
492
486
|
outputKind: 'object',
|
|
@@ -495,9 +489,9 @@ const resumeLeaf = defineLeaf({
|
|
|
495
489
|
run: async (input) => {
|
|
496
490
|
const id = requireCronId(input);
|
|
497
491
|
const dto = await cliClient().resumeCron(id, scopeQuery()).catch((err) => rethrowAsCliError(err, 'Check the id against `crtr cron list`.'));
|
|
498
|
-
return {
|
|
492
|
+
return { fire_at: dto.fire_at };
|
|
499
493
|
},
|
|
500
|
-
render: (r) => `Cron
|
|
494
|
+
render: (r) => `Cron resumed — next fire ${String(r['fire_at'])} (if that is already past, it fires on the next tick).`,
|
|
501
495
|
});
|
|
502
496
|
// ---------------------------------------------------------------------------
|
|
503
497
|
// cron cancel
|
|
@@ -512,7 +506,7 @@ const cancelLeaf = defineLeaf({
|
|
|
512
506
|
params: [
|
|
513
507
|
{ kind: 'positional', name: 'cron-id', required: true, constraint: 'The cron to remove (from `cron list`, or $CRTR_CRON_ID inside a run).' },
|
|
514
508
|
],
|
|
515
|
-
output: [
|
|
509
|
+
output: [],
|
|
516
510
|
outputKind: 'object',
|
|
517
511
|
effects: ['Deletes the cron row and its run history; kills any in-flight run. A no-op if already gone.'],
|
|
518
512
|
},
|
|
@@ -525,9 +519,9 @@ const cancelLeaf = defineLeaf({
|
|
|
525
519
|
...(runId === undefined || runId === '' ? {} : { run_id: runId }),
|
|
526
520
|
})
|
|
527
521
|
.catch((err) => rethrowAsCliError(err, 'Check the id against `crtr cron list`.'));
|
|
528
|
-
return {
|
|
522
|
+
return {};
|
|
529
523
|
},
|
|
530
|
-
render: (
|
|
524
|
+
render: () => 'Cron canceled.'
|
|
531
525
|
});
|
|
532
526
|
// ---------------------------------------------------------------------------
|
|
533
527
|
// Registration
|
|
@@ -17,5 +17,5 @@ export declare function sender(): Promise<{
|
|
|
17
17
|
source: PageSource;
|
|
18
18
|
lifecycle: 'terminal' | 'resident' | null;
|
|
19
19
|
}>;
|
|
20
|
-
export declare
|
|
20
|
+
export declare function humanSend(pageSurface?: boolean): import("../../core/command.js").LeafDef;
|
|
21
21
|
export declare const humanShow: import("../../core/command.js").LeafDef;
|
|
@@ -15,7 +15,10 @@ import { pageManifestPath } from '../../core/human/convention.js';
|
|
|
15
15
|
import { isResponseBearingSlot, validateAuthoredPageManifest } from '../../core/human/page-schema.js';
|
|
16
16
|
import { display } from '../../core/termrender/display.js';
|
|
17
17
|
import { pageHelpText, resolveMaxPanes } from './shared.js';
|
|
18
|
-
|
|
18
|
+
// Only `pageHint` is surface-independent; everything else this file reads off
|
|
19
|
+
// PageHelpText is chosen per host, so the send leaf is built for a surface
|
|
20
|
+
// rather than defined once as a const.
|
|
21
|
+
const PAGE_HINT = pageHelpText().pageHint;
|
|
19
22
|
/** The asking node's id, or null when run from a bare shell (no parent to route to). */
|
|
20
23
|
function askingNode() {
|
|
21
24
|
return process.env['CRTR_NODE_ID'] ?? null;
|
|
@@ -159,122 +162,125 @@ function queuedFollowUp() {
|
|
|
159
162
|
// ---------------------------------------------------------------------------
|
|
160
163
|
// send
|
|
161
164
|
// ---------------------------------------------------------------------------
|
|
162
|
-
export
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
165
|
+
export function humanSend(pageSurface = false) {
|
|
166
|
+
const pageHelp = pageHelpText(pageSurface);
|
|
167
|
+
return defineLeaf({
|
|
168
|
+
name: 'send',
|
|
169
|
+
description: 'send a durable page with explicit delivery',
|
|
170
|
+
whenToUse: pageHelp.sendWhenToUse,
|
|
171
|
+
help: {
|
|
172
|
+
name: 'human send',
|
|
173
|
+
summary: pageHelp.sendSummary,
|
|
174
|
+
guide: 'Page authoring: `crtr human -h`.',
|
|
175
|
+
params: [
|
|
176
|
+
{ kind: 'stdin', name: 'document', required: false, allowPositional: false, constraint: pageHelp.stdinConstraint },
|
|
177
|
+
{ kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HINT} ${pageHelp.pageFlagConstraint}` },
|
|
178
|
+
{ kind: 'flag', name: 'placement', type: 'enum', choices: ['inline', 'panel'], required: false, default: 'inline', constraint: "Where the page is delivered. inline renders in this conversation's transcript; panel opens from the inbox beside it." },
|
|
179
|
+
{ kind: 'flag', name: 'inbox', type: 'bool', required: false, constraint: 'Project this page into the inbox. Mutually exclusive with --no-inbox.' },
|
|
180
|
+
{ kind: 'flag', name: 'no-inbox', type: 'bool', required: false, constraint: "Do not project this page into the inbox, leaving this conversation's transcript as its only route to the user. Mutually exclusive with --inbox, and refused from a terminal node, whose transcript nobody reads once it finishes." },
|
|
181
|
+
{ kind: 'flag', name: 'replace', type: 'string', required: false, constraint: 'Page id returned by an earlier send. Rewrites source in place while retaining its delivery and transcript anchor. Refused once the page is settled or canceled. Not combinable with delivery flags.' },
|
|
182
|
+
],
|
|
183
|
+
output: [
|
|
184
|
+
{ name: 'page_id', type: 'string', required: true, constraint: 'Identifier for the page; pass it as --replace to revise source in place.' },
|
|
185
|
+
{ name: 'dir', type: 'string', required: true, constraint: pageHelp.dirConstraint },
|
|
186
|
+
{ name: 'placement', type: 'inline | panel', required: true, constraint: 'Resolved page placement.' },
|
|
187
|
+
{ name: 'inbox', type: 'boolean', required: true, constraint: 'Whether the page is projected into the inbox.' },
|
|
188
|
+
{ name: 'reply', type: 'boolean', required: true, constraint: 'Whether completion is routed back through a human bridge. Not a flag: true exactly when the page has response-bearing components.' },
|
|
189
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'What happens next; never instructs the caller to poll.' },
|
|
190
|
+
],
|
|
191
|
+
outputKind: 'object',
|
|
192
|
+
effects: [
|
|
193
|
+
'Writes one durable page record. Inbox projection follows the delivery flags, except from a terminal node, which always projects; reply routing follows the page\'s own components.',
|
|
194
|
+
"A display-only page's inbox entry is a notice, not a task: it clears itself once the person has actually seen the page, wherever they see it.",
|
|
195
|
+
'A reply-bearing page creates a kind:"human" bridge under the caller and routes completion back.',
|
|
196
|
+
'--replace rewrites source in place while retaining delivery and emitted time; open surfaces reload it without creating another page.',
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
run: async (input, context) => {
|
|
200
|
+
const { sourceFile, tempDir } = pageInput(input);
|
|
201
|
+
const replacePage = input['replace'];
|
|
202
|
+
const deliveryFlags = ['placement', 'inbox', 'noInbox'];
|
|
203
|
+
if (input['inbox'] === true && input['noInbox'] === true) {
|
|
204
|
+
throw new InputError({ error: 'bad_invocation', message: 'received conflicting delivery flags', field: 'inbox', next: 'Pass at most one of --inbox and --no-inbox.' });
|
|
205
|
+
}
|
|
206
|
+
if (replacePage !== undefined && deliveryFlags.some((flag) => context?.providedParams.has(flag) === true)) {
|
|
207
|
+
throw new InputError({ error: 'bad_invocation', message: '--replace cannot change delivery', field: 'replace', next: 'Remove delivery flags: replacement retains the page placement, inbox, reply, and emitted time.' });
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
if (replacePage !== undefined) {
|
|
211
|
+
const prior = replacedPage(replacePage);
|
|
212
|
+
// The human's submit wins the race with a revision: once the ticket has
|
|
213
|
+
// any terminal outcome, replacement is refused, and that refusal is the
|
|
214
|
+
// reviser's signal that the ticket is closed.
|
|
215
|
+
if (readTicketResult(ticketDir(replacePage)) !== null) {
|
|
216
|
+
throw new InputError({
|
|
217
|
+
error: 'page_settled',
|
|
218
|
+
message: `page ${replacePage} already has a terminal outcome`,
|
|
219
|
+
field: 'replace',
|
|
220
|
+
next: 'The page was answered or canceled; nothing on it can change now. Raise a new page if more is needed.',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// A revision changes only the content. Identity, routing, and emitted
|
|
224
|
+
// time stay the original sender's, so the prior source is inherited
|
|
225
|
+
// verbatim — a fork revising the page must not repoint its provenance.
|
|
226
|
+
const revised = submitAuthoredPage(() => preparePage({ sourceFile, source: prior.source, productKinds: pageProductKinds(), delivery: prior.delivery }));
|
|
227
|
+
const { dir } = publishPage(ticketDir(replacePage), revised);
|
|
228
|
+
return { page_id: replacePage, dir, ...prior.delivery, follow_up: 'Replaced in place. Every open surface reloads it; delivery and emitted time are unchanged.' };
|
|
229
|
+
}
|
|
230
|
+
const { source, lifecycle } = await sender();
|
|
231
|
+
// A terminal node finishes and its transcript stops being read, so the
|
|
232
|
+
// inbox is the only surface its page can still reach a human on. Refuse
|
|
233
|
+
// rather than quietly overriding the flag: the caller asked for a page
|
|
234
|
+
// nobody would ever see, and that is worth knowing.
|
|
235
|
+
if (lifecycle === 'terminal' && input['noInbox'] === true) {
|
|
211
236
|
throw new InputError({
|
|
212
|
-
error: '
|
|
213
|
-
message:
|
|
214
|
-
field: '
|
|
215
|
-
next: '
|
|
237
|
+
error: 'invalid_page_delivery',
|
|
238
|
+
message: 'a terminal node cannot send a page --no-inbox',
|
|
239
|
+
field: 'inbox',
|
|
240
|
+
next: 'Drop --no-inbox. This node finishes, and nobody reads a finished node\'s transcript, so a page kept out of the inbox reaches no one.',
|
|
216
241
|
});
|
|
217
242
|
}
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
// Static extraction is an evaluation. Derive and validate delivery from this
|
|
244
|
+
// exact manifest so authored code runs once and cannot change between passes.
|
|
245
|
+
const productKinds = pageProductKinds();
|
|
246
|
+
const prepared = submitAuthoredPage(() => preparePage({ sourceFile, source, productKinds }));
|
|
247
|
+
const responseBearing = prepared.manifest.slots.some(isResponseBearingSlot);
|
|
248
|
+
const delivery = deliveryFrom(input, responseBearing, lifecycle);
|
|
249
|
+
prepared.manifest = submitAuthoredPage(() => validateAuthoredPageManifest({ ...prepared.manifest, delivery }, productKinds));
|
|
250
|
+
if (!delivery.reply) {
|
|
251
|
+
const pageId = `page-${randomBytes(4).toString('hex')}`;
|
|
252
|
+
const { dir } = publishPage(ticketDir(pageId), prepared);
|
|
253
|
+
// A page with no reply route carries no response-bearing components, so what the
|
|
254
|
+
// follow-up must get right is where the person will meet it and how it
|
|
255
|
+
// settles: an inline page is already in this conversation, and its
|
|
256
|
+
// inbox entry is a notice that clears itself the moment the page has
|
|
257
|
+
// actually been seen — an agent that points the person at their inbox
|
|
258
|
+
// for a page on their screen sends them to an empty list.
|
|
259
|
+
const followUp = delivery.placement === 'inline' && delivery.inbox
|
|
260
|
+
? 'Shown in this conversation and queued in the user\'s inbox.'
|
|
261
|
+
: delivery.placement === 'inline'
|
|
262
|
+
? 'Shown in this conversation as passive page content with no inbox entry.'
|
|
263
|
+
: delivery.inbox
|
|
264
|
+
? 'Queued in the inbox; it opens in the panel and waits until dismissed.'
|
|
265
|
+
: 'Published as passive page content with no inbox or response lifecycle.';
|
|
266
|
+
return { page_id: pageId, dir, ...delivery, follow_up: followUp };
|
|
267
|
+
}
|
|
268
|
+
const originNodeId = askingNode();
|
|
269
|
+
const bridgeNodeId = (await cliClient().createHumanBridge({ kind: 'human', parent: originNodeId, cwd: process.cwd(), name: 'human-page' })).node_id;
|
|
270
|
+
const idir = ticketDir(bridgeNodeId);
|
|
271
|
+
bindTicketReplyRoute(idir, bridgeNodeId);
|
|
272
|
+
const { dir } = publishPage(idir, prepared);
|
|
273
|
+
if (originNodeId !== null && originNodeId !== '')
|
|
274
|
+
markStopSignal(originNodeId, 'requestedHumanReply');
|
|
275
|
+
return { page_id: bridgeNodeId, dir, ...delivery, follow_up: queuedFollowUp() };
|
|
237
276
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const prepared = submitAuthoredPage(() => preparePage({ sourceFile, source, productKinds }));
|
|
242
|
-
const responseBearing = prepared.manifest.slots.some(isResponseBearingSlot);
|
|
243
|
-
const delivery = deliveryFrom(input, responseBearing, lifecycle);
|
|
244
|
-
prepared.manifest = submitAuthoredPage(() => validateAuthoredPageManifest({ ...prepared.manifest, delivery }, productKinds));
|
|
245
|
-
if (!delivery.reply) {
|
|
246
|
-
const pageId = `page-${randomBytes(4).toString('hex')}`;
|
|
247
|
-
const { dir } = publishPage(ticketDir(pageId), prepared);
|
|
248
|
-
// A page with no reply route carries no response-bearing components, so what the
|
|
249
|
-
// follow-up must get right is where the person will meet it and how it
|
|
250
|
-
// settles: an inline page is already in this conversation, and its
|
|
251
|
-
// inbox entry is a notice that clears itself the moment the page has
|
|
252
|
-
// actually been seen — an agent that points the person at their inbox
|
|
253
|
-
// for a page on their screen sends them to an empty list.
|
|
254
|
-
const followUp = delivery.placement === 'inline' && delivery.inbox
|
|
255
|
-
? 'Shown in this conversation and queued in the user\'s inbox. The inbox entry clears itself the user has seen the page — someone viewing this conversation acknowledges it on sight, so do not send them to their inbox for it. They already know this, do not inform them.'
|
|
256
|
-
: delivery.placement === 'inline'
|
|
257
|
-
? 'Shown in this conversation as passive page content with no inbox entry.'
|
|
258
|
-
: delivery.inbox
|
|
259
|
-
? 'Queued in the inbox; it opens in the panel and waits until dismissed.'
|
|
260
|
-
: 'Published as passive page content with no inbox or response lifecycle.';
|
|
261
|
-
return { page_id: pageId, dir, ...delivery, follow_up: followUp };
|
|
277
|
+
finally {
|
|
278
|
+
if (tempDir !== undefined)
|
|
279
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
262
280
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
bindTicketReplyRoute(idir, bridgeNodeId);
|
|
267
|
-
const { dir } = publishPage(idir, prepared);
|
|
268
|
-
if (originNodeId !== null && originNodeId !== '')
|
|
269
|
-
markStopSignal(originNodeId, 'requestedHumanReply');
|
|
270
|
-
return { page_id: bridgeNodeId, dir, ...delivery, follow_up: queuedFollowUp() };
|
|
271
|
-
}
|
|
272
|
-
finally {
|
|
273
|
-
if (tempDir !== undefined)
|
|
274
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
});
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
278
284
|
// ---------------------------------------------------------------------------
|
|
279
285
|
// show (no job, non-blocking passthrough) — passive live document display, NOT
|
|
280
286
|
// a ticket surface. `display()` puts a file live in a watch pane and never
|