@north-light/crouter 0.3.179 → 0.3.181
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 +27 -2
- package/dist/api/client.js +41 -1
- package/dist/api/dto/broker.d.ts +32 -0
- package/dist/api/dto/crons.d.ts +17 -0
- package/dist/api/dto/human.d.ts +1 -7
- package/dist/api/dto/inbox.d.ts +71 -1
- package/dist/api/dto/inbox.js +9 -1
- package/dist/api/dto/memory.d.ts +17 -0
- package/dist/api/dto/memory.js +6 -0
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/reviews.d.ts +8 -4
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +5 -0
- package/dist/api/routes.js +8 -1
- package/dist/build-root.d.ts +7 -0
- package/dist/build-root.js +21 -0
- package/dist/builtin-memory/insights/init.md +48 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
- package/dist/cli.js +1 -2
- package/dist/clients/attach/__tests__/context-message.test.js +5 -2
- package/dist/clients/attach/assets/README.md +7 -0
- package/dist/clients/attach/assets/whip-06.mp3 +0 -0
- package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
- package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
- package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
- package/dist/clients/attach/chrome/canvas-panels.js +20 -3
- package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
- package/dist/clients/attach/chrome/review-wait.js +22 -0
- package/dist/clients/attach/chrome/roster.js +23 -2
- package/dist/clients/attach/chrome/widgets.js +1 -1
- package/dist/clients/attach/input/controller.js +4 -3
- package/dist/clients/attach/overlays/mcp.js +3 -1
- package/dist/clients/attach/render/chat-view.js +1 -1
- package/dist/clients/attach/session/whip.d.ts +1 -0
- package/dist/clients/attach/session/whip.js +26 -0
- package/dist/clients/attach/slash/dispatch.js +2 -0
- package/dist/clients/attach/viewer.js +578 -573
- package/dist/clients/inbox/review/document-surface.d.ts +1 -1
- package/dist/clients/inbox/review/document-surface.js +4 -4
- package/dist/clients/inbox/review/launch.js +16 -4
- package/dist/clients/inbox/review/review-client.d.ts +9 -4
- package/dist/clients/inbox/review/review-client.js +3 -0
- package/dist/commands/cron.js +30 -8
- package/dist/commands/human/prompts.d.ts +7 -2
- package/dist/commands/human/prompts.js +15 -10
- package/dist/commands/human.js +1 -2
- package/dist/commands/memory/find.js +11 -8
- package/dist/commands/memory/read.js +111 -11
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.d.ts +13 -0
- package/dist/commands/pkg/market-manage.js +39 -33
- package/dist/commands/pkg/plugin-inspect.js +4 -3
- package/dist/commands/pkg/plugin-manage.js +12 -11
- package/dist/commands/surface/node/focus.js +1 -2
- package/dist/commands/sys/doctor.js +4 -4
- package/dist/commands/sys/setup-core.d.ts +14 -7
- package/dist/commands/sys/setup-core.js +66 -11
- package/dist/commands/sys/setup-wizard.js +2 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
- package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
- package/dist/core/__tests__/helpers/harness.js +1 -2
- package/dist/core/__tests__/phase4-review-store.test.js +1 -0
- package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
- package/dist/core/__tests__/session-model.test.js +5 -3
- package/dist/core/bootstrap.d.ts +0 -4
- package/dist/core/bootstrap.js +1 -55
- package/dist/core/canvas/crons.d.ts +54 -2
- package/dist/core/canvas/crons.js +48 -4
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/command-manifests/manifest.d.ts +11 -0
- package/dist/core/command-manifests/manifest.js +45 -4
- package/dist/core/command-manifests/schema.d.ts +1 -1
- package/dist/core/command-plugins/bundle.d.ts +1 -0
- package/dist/core/command-plugins/bundle.js +3 -3
- package/dist/core/command-plugins/discovery.d.ts +5 -2
- package/dist/core/command-plugins/discovery.js +5 -5
- package/dist/core/command-plugins/help-addenda.d.ts +12 -0
- package/dist/core/command-plugins/help-addenda.js +30 -0
- package/dist/core/command.js +25 -2
- package/dist/core/config.js +0 -1
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +0 -6
- package/dist/core/keybindings/inbox.d.ts +6 -8
- package/dist/core/keybindings/inbox.js +6 -15
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/memory/doc-link-grammar.js +4 -1
- package/dist/core/memory-resolver.d.ts +28 -4
- package/dist/core/memory-resolver.js +51 -39
- package/dist/core/review/stage.js +1 -0
- package/dist/core/review/store.d.ts +5 -0
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.d.ts +8 -1
- package/dist/core/runtime/broker/event-projection.js +25 -1
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +50 -8
- package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
- package/dist/core/runtime/broker/message-ledger.js +143 -0
- package/dist/core/runtime/broker/rebind.js +14 -0
- package/dist/core/runtime/broker-protocol.d.ts +46 -1
- package/dist/core/runtime/broker.js +11 -2
- package/dist/core/runtime/interactive-deliver.d.ts +5 -2
- package/dist/core/runtime/interactive-deliver.js +6 -3
- package/dist/core/runtime/shell-expansion.d.ts +32 -0
- package/dist/core/runtime/shell-expansion.js +102 -0
- package/dist/core/session-model/session-state.d.ts +9 -4
- package/dist/core/session-model/session-state.js +5 -1
- package/dist/daemon/api/handlers/broker-ops.js +8 -0
- package/dist/daemon/api/handlers/crons.js +14 -1
- package/dist/daemon/api/handlers/inbox.js +346 -2
- package/dist/daemon/api/handlers/memory.d.ts +2 -0
- package/dist/daemon/api/handlers/memory.js +48 -0
- package/dist/daemon/api/handlers/messages.js +7 -1
- package/dist/daemon/api/handlers/reviews.js +7 -5
- package/dist/daemon/api/map.js +3 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +71 -3
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
- package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
- package/dist/daemon/review/companion.d.ts +8 -0
- package/dist/daemon/review/companion.js +35 -0
- package/dist/daemon/review/deliver.js +2 -1
- package/dist/daemon/review/finish.d.ts +29 -2
- package/dist/daemon/review/finish.js +75 -2
- package/dist/shared/generated-context.d.ts +3 -4
- package/dist/shared/generated-context.js +24 -6
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
|
|
4
|
+
// Integration test: /insights:init --active spawns an explorer and coordinates
|
|
5
|
+
// insight capture flow. This test verifies that:
|
|
6
|
+
// 1. The active mode doc exists and has slash: true
|
|
7
|
+
// 2. The active mode instructions are present and executable
|
|
8
|
+
// 3. A reader following the instructions can spawn an explorer
|
|
9
|
+
|
|
10
|
+
test('insights/init has active mode documentation with executable commands', async () => {
|
|
11
|
+
const { readFileSync } = await import('fs');
|
|
12
|
+
const initPath = new URL('../../../builtin-memory/insights/init.md', import.meta.url);
|
|
13
|
+
const content = readFileSync(initPath, 'utf-8');
|
|
14
|
+
|
|
15
|
+
// Must be marked as a slash command
|
|
16
|
+
assert(content.includes('slash: true'), 'init.md must have slash: true frontmatter');
|
|
17
|
+
|
|
18
|
+
// Must have active mode section
|
|
19
|
+
assert(
|
|
20
|
+
content.includes('## Active mode: research and surface claims'),
|
|
21
|
+
'init.md must document active mode'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Must have active flag check
|
|
25
|
+
assert(
|
|
26
|
+
content.includes('Check for --active flag') || content.includes('--active'),
|
|
27
|
+
'init.md must explain --active flag'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Must have executable crtr commands for spawning explorer
|
|
31
|
+
assert(
|
|
32
|
+
content.includes('crtr node new --kind explore'),
|
|
33
|
+
'init.md must include crtr command to spawn explorer'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Must reference the capture workflow
|
|
37
|
+
assert(
|
|
38
|
+
content.includes('[[insights/capture]]'),
|
|
39
|
+
'init.md must link to capture workflow'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Must document waiting for explorer report
|
|
43
|
+
assert(
|
|
44
|
+
content.includes('Wait for the explorer to report') || content.includes('explorer'),
|
|
45
|
+
'init.md must explain waiting for explorer'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Must document coordinating user responses
|
|
49
|
+
assert(
|
|
50
|
+
content.includes('user response') || content.includes('Turn user responses'),
|
|
51
|
+
'init.md must explain handling user responses'
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('active mode provides concrete bash commands agent can execute', async () => {
|
|
56
|
+
const { readFileSync } = await import('fs');
|
|
57
|
+
const initPath = new URL('../../../builtin-memory/insights/init.md', import.meta.url);
|
|
58
|
+
const content = readFileSync(initPath, 'utf-8');
|
|
59
|
+
|
|
60
|
+
// The active mode section should have a bash code block
|
|
61
|
+
const bashBlockMatch = content.match(/```bash\n([\s\S]*?)\n```/);
|
|
62
|
+
assert(bashBlockMatch, 'active mode must have a bash code block');
|
|
63
|
+
|
|
64
|
+
const bashContent = bashBlockMatch[1];
|
|
65
|
+
|
|
66
|
+
// Must have the crtr node new command
|
|
67
|
+
assert(bashContent.includes('crtr node new'), 'bash block must invoke crtr node new');
|
|
68
|
+
|
|
69
|
+
// Must specify --kind explore
|
|
70
|
+
assert(
|
|
71
|
+
bashContent.includes('--kind explore'),
|
|
72
|
+
'bash command must spawn explore kind'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Must have the task heredoc
|
|
76
|
+
assert(
|
|
77
|
+
bashContent.includes('<<') || bashContent.includes('TASK'),
|
|
78
|
+
'bash command must include task definition'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Must document the explorer task
|
|
82
|
+
assert(
|
|
83
|
+
bashContent.includes('investigate') || bashContent.includes('domain'),
|
|
84
|
+
'explorer task must mention domain investigation'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Must ask explorer to surface claims
|
|
88
|
+
assert(
|
|
89
|
+
bashContent.includes('claim') || bashContent.includes('surface'),
|
|
90
|
+
'explorer task must ask for claims/surface evidence'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Must reference output location
|
|
94
|
+
assert(
|
|
95
|
+
bashContent.includes('$CRTR_CONTEXT_DIR') || bashContent.includes('context'),
|
|
96
|
+
'explorer task must reference context directory output'
|
|
97
|
+
);
|
|
98
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateTail, formatSize, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import {
|
|
4
3
|
readFileSync,
|
|
5
4
|
readdirSync,
|
|
@@ -14,6 +13,8 @@ import { homedir } from "node:os";
|
|
|
14
13
|
import { join, resolve, dirname } from "node:path";
|
|
15
14
|
import { loadProfileManifest } from "../../../core/profiles/manifest.js";
|
|
16
15
|
import { stripMarkdownComments } from "../../../core/runtime/command-expansion.js";
|
|
16
|
+
import { expandShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
|
|
17
|
+
import { piShellRunner } from "./pi-shell-runner.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// claude-plugin-commands: surfaces Claude Code commands AND skills as pi slash
|
|
@@ -367,57 +368,13 @@ function shellTimeoutMs(fm: string): number {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
// ---- shell execution (mirrors @juicesharp/rpiv-args) -----------------------
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
function truncateForLLM(content: string): string {
|
|
376
|
-
const t = truncateTail(content, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
377
|
-
let out = t.content;
|
|
378
|
-
if (t.truncated) {
|
|
379
|
-
const limit = t.truncatedBy === "lines" ? `${t.maxLines} lines` : formatSize(t.maxBytes);
|
|
380
|
-
out += `\n[truncated: hit ${limit}]`;
|
|
381
|
-
}
|
|
382
|
-
return out;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async function runShell(cmd: string, pi: ExtensionAPI, cwd: string, timeoutMs: number): Promise<string> {
|
|
386
|
-
const [sh, flag] = process.platform === "win32" ? ["powershell.exe", "-Command"] : ["sh", "-c"];
|
|
387
|
-
const res: any = await pi.exec(sh, [flag, cmd], { cwd, timeout: timeoutMs });
|
|
388
|
-
if (res.killed) return `[Shell error: timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s]`;
|
|
389
|
-
if (res.code !== 0) return `[Shell error: exit code ${res.code}]\n${truncateForLLM(res.stderr ?? "")}`;
|
|
390
|
-
let combined = res.stdout ?? "";
|
|
391
|
-
if (res.stderr) {
|
|
392
|
-
const sep = combined.length === 0 || combined.endsWith("\n") ? "" : "\n";
|
|
393
|
-
combined = `${combined}${sep}[stderr]\n${res.stderr}`;
|
|
394
|
-
}
|
|
395
|
-
return truncateForLLM(combined);
|
|
396
|
-
}
|
|
371
|
+
//
|
|
372
|
+
// The `!`cmd`` / ```! grammar and substitution order are shared with every
|
|
373
|
+
// other surface that expands a Markdown body (`core/runtime/shell-expansion`);
|
|
374
|
+
// the pi-engine execution strategy is shared through `pi-shell-runner`.
|
|
397
375
|
|
|
398
376
|
async function executeShell(body: string, pi: ExtensionAPI, cwd: string, timeoutMs: number): Promise<string> {
|
|
399
|
-
|
|
400
|
-
const blockOut: string[] = [];
|
|
401
|
-
let masked = "";
|
|
402
|
-
let last = 0;
|
|
403
|
-
for (const m of body.matchAll(SHELL_BLOCK)) {
|
|
404
|
-
const idx = m.index ?? 0;
|
|
405
|
-
masked += body.slice(last, idx) + `\x00B${blockOut.length}\x00`;
|
|
406
|
-
blockOut.push(await runShell(m[1] ?? "", pi, cwd, timeoutMs));
|
|
407
|
-
last = idx + m[0].length;
|
|
408
|
-
}
|
|
409
|
-
masked += body.slice(last);
|
|
410
|
-
|
|
411
|
-
let inlined = "";
|
|
412
|
-
last = 0;
|
|
413
|
-
for (const m of masked.matchAll(SHELL_INLINE)) {
|
|
414
|
-
const idx = m.index ?? 0;
|
|
415
|
-
inlined += masked.slice(last, idx) + (await runShell(m[1] ?? "", pi, cwd, timeoutMs));
|
|
416
|
-
last = idx + m[0].length;
|
|
417
|
-
}
|
|
418
|
-
inlined += masked.slice(last);
|
|
419
|
-
|
|
420
|
-
return inlined.replace(/\x00B(\d+)\x00/g, (_, n) => blockOut[parseInt(n, 10)] ?? "");
|
|
377
|
+
return expandShellBlocks(body, piShellRunner(pi, cwd, timeoutMs));
|
|
421
378
|
}
|
|
422
379
|
|
|
423
380
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
expandDeterministicCommand,
|
|
6
6
|
type DeterministicCommandExpansion,
|
|
7
7
|
} from "../../../core/runtime/command-expansion.js";
|
|
8
|
+
import { expandShellBlocks, hasShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
|
|
9
|
+
import { piShellRunner } from "./pi-shell-runner.js";
|
|
8
10
|
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
// memory-slash-commands: any memory doc frontmatter-flagged `slash: true`
|
|
@@ -136,10 +138,23 @@ export default async function (pi: ExtensionAPI) {
|
|
|
136
138
|
);
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
141
|
+
// Shell runs HERE and only here. `expandDeterministicCommand` stays
|
|
142
|
+
// inert because it also drives the attach viewer's pre-submit preview,
|
|
143
|
+
// which re-expands on every keystroke — executing there would run the
|
|
144
|
+
// document's commands repeatedly while the user is still typing. This
|
|
145
|
+
// handler fires once, at submit, so the preview shows the command text
|
|
146
|
+
// and the submitted turn carries its output.
|
|
147
|
+
const expanded = expandDeterministicCommand(expansion, args ?? "");
|
|
148
|
+
const content = hasShellBlocks(expanded)
|
|
149
|
+
? await expandShellBlocks(
|
|
150
|
+
expanded,
|
|
151
|
+
piShellRunner(pi, (ctx as { cwd?: string }).cwd ?? process.cwd(), DEFAULT_SHELL_TIMEOUT_MS),
|
|
152
|
+
)
|
|
153
|
+
: expanded;
|
|
139
154
|
pi.sendMessage(
|
|
140
155
|
{
|
|
141
156
|
customType: "memory-slash-command",
|
|
142
|
-
content
|
|
157
|
+
content,
|
|
143
158
|
display: true,
|
|
144
159
|
},
|
|
145
160
|
{ triggerTurn: true },
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateTail, formatSize, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
// The broker-side execution strategy for `core/runtime/shell-expansion`. Every
|
|
5
|
+
// command surface that expands a Markdown body inside a pi engine shares it, so
|
|
6
|
+
// the engine owns the subprocess and truncation uses pi's own limits. The
|
|
7
|
+
// grammar and substitution order live in the shared module; only this strategy
|
|
8
|
+
// is pi-specific, and it stays out of that module so CLI leaves can import the
|
|
9
|
+
// expander without loading the pi runtime.
|
|
10
|
+
|
|
11
|
+
function truncateForLLM(content: string): string {
|
|
12
|
+
const t = truncateTail(content, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
13
|
+
let out = t.content;
|
|
14
|
+
if (t.truncated) {
|
|
15
|
+
const limit = t.truncatedBy === "lines" ? `${t.maxLines} lines` : formatSize(t.maxBytes);
|
|
16
|
+
out += `\n[truncated: hit ${limit}]`;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function piShellRunner(pi: ExtensionAPI, cwd: string, timeoutMs: number) {
|
|
22
|
+
const [sh, flag] = process.platform === "win32" ? ["powershell.exe", "-Command"] : ["sh", "-c"];
|
|
23
|
+
return async (cmd: string): Promise<string> => {
|
|
24
|
+
const res: any = await pi.exec(sh, [flag, cmd], { cwd, timeout: timeoutMs });
|
|
25
|
+
if (res.killed) return `[Shell error: timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s]`;
|
|
26
|
+
if (res.code !== 0) return `[Shell error: exit code ${res.code}]\n${truncateForLLM(res.stderr ?? "")}`;
|
|
27
|
+
let combined = res.stdout ?? "";
|
|
28
|
+
if (res.stderr) {
|
|
29
|
+
const sep = combined.length === 0 || combined.endsWith("\n") ? "" : "\n";
|
|
30
|
+
combined = `${combined}${sep}[stderr]\n${res.stderr}`;
|
|
31
|
+
}
|
|
32
|
+
return truncateForLLM(combined);
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { runCli } from './core/command.js';
|
|
|
7
7
|
import { resolveRoot } from './build-root.js';
|
|
8
8
|
import { maybeBootRoot } from './core/runtime/front-door.js';
|
|
9
9
|
import { maybeAutoUpdate } from './core/auto-update.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ensureProjectScope } from './core/bootstrap.js';
|
|
11
11
|
import { provisionExports, pruneLegacyExports } from './core/host-exports/export.js';
|
|
12
12
|
import { installStdoutErrorPolicy } from './core/stdout-errors.js';
|
|
13
13
|
import { handle } from './core/io.js';
|
|
@@ -54,7 +54,6 @@ async function main() {
|
|
|
54
54
|
const root = await resolveRoot(process.argv[2]);
|
|
55
55
|
mark('cli.root_resolved');
|
|
56
56
|
provisionExports(root, process.argv);
|
|
57
|
-
ensureOfficialMarketplace(process.argv);
|
|
58
57
|
ensureProjectScope(process.argv);
|
|
59
58
|
maybeAutoUpdate(process.argv);
|
|
60
59
|
mark('cli.dispatch_start');
|
|
@@ -11,7 +11,7 @@ import test from 'node:test';
|
|
|
11
11
|
import { Container } from '@earendil-works/pi-tui';
|
|
12
12
|
import { applyTheme } from '../config.js';
|
|
13
13
|
import { ChatView } from '../render/chat-view.js';
|
|
14
|
-
import { CONTEXT_INTRO_CUSTOM_TYPE, CONTEXT_NUDGE_CUSTOM_TYPE, REVIVE_KICKOFF_SENTINEL, RUNTIME_RESTART_CONTINUATION, formatModelFallbackRecovery, generatedContextPresentation, } from '../../../shared/generated-context.js';
|
|
14
|
+
import { CONTEXT_INTRO_CUSTOM_TYPE, CONTEXT_NUDGE_CUSTOM_TYPE, REVIVE_KICKOFF_SENTINEL, RUNTIME_RESTART_CONTINUATION, formatModelFallbackRecovery, formatReviewApproval, generatedContextPresentation, } from '../../../shared/generated-context.js';
|
|
15
15
|
applyTheme();
|
|
16
16
|
test('collapsed generated context never leaks its body; Ctrl+O restores it', async () => {
|
|
17
17
|
const container = new Container();
|
|
@@ -38,11 +38,12 @@ test('collapsed generated context never leaks its body; Ctrl+O restores it', asy
|
|
|
38
38
|
content: '[crtr] Context ~150k. private context reminder',
|
|
39
39
|
},
|
|
40
40
|
{ role: 'user', content: 'From child-node-123 — 1 update:\n [update] private inbox detail' },
|
|
41
|
+
{ role: 'user', content: formatReviewApproval('The human reviewed and approved private-review.md.') },
|
|
41
42
|
],
|
|
42
43
|
stats: {},
|
|
43
44
|
state: { isStreaming: false },
|
|
44
45
|
});
|
|
45
|
-
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|runtime-restart-continuation|private context reminder|private inbox detail/);
|
|
46
|
+
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|runtime-restart-continuation|private context reminder|private inbox detail|private-review\.md/);
|
|
46
47
|
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: false });
|
|
47
48
|
assert.equal(internals.toolOutputExpanded, true);
|
|
48
49
|
const expanded = container.render(80).join('\n');
|
|
@@ -50,6 +51,7 @@ test('collapsed generated context never leaks its body; Ctrl+O restores it', asy
|
|
|
50
51
|
assert.match(expanded, /resume from this note/);
|
|
51
52
|
assert.doesNotMatch(expanded, /runtime-restart-continuation|private context reminder/);
|
|
52
53
|
assert.match(expanded, /private inbox detail/, 'an expanded inbox card shows the actual delivered update');
|
|
54
|
+
assert.match(expanded, /private-review\.md/, 'an expanded review card shows the approval');
|
|
53
55
|
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: true });
|
|
54
56
|
assert.equal(internals.toolOutputExpanded, false);
|
|
55
57
|
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|runtime-restart-continuation|private context reminder|private inbox detail/);
|
|
@@ -61,4 +63,5 @@ test('a human message that reads like runtime recovery is not folded as generate
|
|
|
61
63
|
const marked = formatModelFallbackRecovery('custom(provider)/model', 'new/model:high', 'not-found');
|
|
62
64
|
assert.equal(generatedContextPresentation({ role: 'user', content: marked })?.kind, 'recovery');
|
|
63
65
|
assert.equal(generatedContextPresentation({ role: 'user', content: marked.split('\n').slice(1, -1).join('\n') }), null, 'a human message matching the recovery prose lacks the producer marker');
|
|
66
|
+
assert.equal(generatedContextPresentation({ role: 'user', content: 'The human reviewed and approved private-review.md.' }), null, 'a human message matching the approval prose lacks the producer marker');
|
|
64
67
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Whip sounds
|
|
2
|
+
|
|
3
|
+
These files are the first three results for [“whip crack” on Pixabay](https://pixabay.com/sound-effects/search/whip%20crack/), downloaded from the playable audio URLs embedded in each result page on 2026-08-04. They are available under the [Pixabay Content License](https://pixabay.com/service/license-summary/).
|
|
4
|
+
|
|
5
|
+
- `whip-snap.mp3` — [“Whip Snap” by Universfield](https://pixabay.com/sound-effects/film-special-effects-whip-snap-242215/), with 100ms removed from its start to align its crack with the animation flash
|
|
6
|
+
- `whip-crack.mp3` — [“Whip Crack” by Universfield](https://pixabay.com/sound-effects/film-special-effects-whip-crack-123738/)
|
|
7
|
+
- `whip-06.mp3` — [“Whip 06” by Universfield](https://pixabay.com/sound-effects/film-special-effects-whip-06-487886/)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CanvasSource } from '../../../core/canvas/source.js';
|
|
2
2
|
import type { CronDTO } from '../../../api/dto/crons.js';
|
|
3
|
+
import type { ReviewDTO } from '../../../api/dto/reviews.js';
|
|
3
4
|
import type { BashJobStatus } from '../../../core/bash-jobs.js';
|
|
4
5
|
import type { AttachPalette } from '../config.js';
|
|
5
6
|
export type CanvasPanelItem = {
|
|
@@ -11,6 +12,11 @@ export type CanvasPanelItem = {
|
|
|
11
12
|
key: 'human';
|
|
12
13
|
kind: 'human';
|
|
13
14
|
line: string;
|
|
15
|
+
} | {
|
|
16
|
+
key: string;
|
|
17
|
+
kind: 'review';
|
|
18
|
+
companionNodeId: string;
|
|
19
|
+
line: string;
|
|
14
20
|
} | {
|
|
15
21
|
key: 'bash';
|
|
16
22
|
kind: 'bash';
|
|
@@ -33,4 +39,4 @@ export interface CanvasPanelLines {
|
|
|
33
39
|
* `{}` to omit the ⚑ badges. `crons` are this node's armed schedules.
|
|
34
40
|
* `palette`
|
|
35
41
|
* drives the headers + dim cells; omit it and the panels render uncolored. */
|
|
36
|
-
export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>, palette: AttachPalette | undefined, source: CanvasSource, crons?: readonly CronDTO[], jobs?: readonly BashJobStatus[]): Promise<CanvasPanelLines>;
|
|
42
|
+
export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>, palette: AttachPalette | undefined, source: CanvasSource, crons?: readonly CronDTO[], jobs?: readonly BashJobStatus[], reviews?: readonly ReviewDTO[]): Promise<CanvasPanelLines>;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// into Containers around the editor:
|
|
6
6
|
// reports → BELOW the editor: this node's manager (↑), its live
|
|
7
7
|
// (↓, active|idle) reports, and trailing wait rows for human
|
|
8
|
-
// tickets, live background bash jobs, and
|
|
9
|
-
// inline scroll+open/swap.
|
|
8
|
+
// tickets, open document reviews, live background bash jobs, and
|
|
9
|
+
// armed crons. Backs inline scroll+open/swap.
|
|
10
10
|
//
|
|
11
11
|
// Pure: given a node id + the per-node ticket-count map it returns ANSI strings; the
|
|
12
12
|
// caller owns the Containers and the refresh trigger. beginFrameTelem() clears the
|
|
@@ -16,9 +16,13 @@ import { beginFrameTelem, navLabel, nodeGlyph, truncate, tokensCell, cycleBadge,
|
|
|
16
16
|
/** Columns the bash row spends on everything except the command itself:
|
|
17
17
|
* ` ⚙ ` + `bash job · ` + elapsed + ` · ` + ` · ↵ details`. */
|
|
18
18
|
const BASH_ROW_CHROME = 34;
|
|
19
|
+
/** Columns the review row spends on everything except the file name:
|
|
20
|
+
* ` ✎ ` + the longest state prefix + ` · ` + ` · ↵ companion`. */
|
|
21
|
+
const REVIEW_ROW_CHROME = 40;
|
|
19
22
|
import { RemoteCanvasSource } from '../../../core/canvas/remote-canvas-source.js';
|
|
20
23
|
import { cronWaitSummary } from './cron-wait.js';
|
|
21
24
|
import { bashJobsWaitSummary } from './bash-jobs.js';
|
|
25
|
+
import { reviewWaitSummary } from './review-wait.js';
|
|
22
26
|
/** Column widths for the aligned panel rows (visible columns). name is sized to
|
|
23
27
|
* the widest label across BOTH panels (capped); kind is wide enough for the
|
|
24
28
|
* longest persona kind (`orchestrator` = 12) so real kinds never truncate;
|
|
@@ -39,7 +43,7 @@ function padStartVisible(s, w) {
|
|
|
39
43
|
* `{}` to omit the ⚑ badges. `crons` are this node's armed schedules.
|
|
40
44
|
* `palette`
|
|
41
45
|
* drives the headers + dim cells; omit it and the panels render uncolored. */
|
|
42
|
-
export async function buildCanvasPanelLines(nodeId, asks, palette, source, crons = [], jobs = []) {
|
|
46
|
+
export async function buildCanvasPanelLines(nodeId, asks, palette, source, crons = [], jobs = [], reviews = []) {
|
|
43
47
|
// A remote source provides only graph structure + node metadata; tokens/
|
|
44
48
|
// activity/status-glyph adornments are local disk reads (telemetry.json,
|
|
45
49
|
// fault state, pid liveness) and are suppressed to safe defaults below —
|
|
@@ -159,6 +163,19 @@ export async function buildCanvasPanelLines(nodeId, asks, palette, source, crons
|
|
|
159
163
|
const selfAsks = asks[nodeId] ?? 0;
|
|
160
164
|
if (selfAsks > 0)
|
|
161
165
|
items.push({ key: 'human', kind: 'human', line: ` ${YELLOW}⚑ waiting on human · ${selfAsks} pending${RESET}` });
|
|
166
|
+
// Review rows sit with the human row: an open review is either waiting on the
|
|
167
|
+
// person or already submitted and finishing behind its companion, and either
|
|
168
|
+
// way this node resumes only when the review completes.
|
|
169
|
+
for (const review of reviews) {
|
|
170
|
+
const pen = palette ? palette.info('✎') : '✎';
|
|
171
|
+
const summary = reviewWaitSummary(review, Math.max(8, fillWidth() - REVIEW_ROW_CHROME));
|
|
172
|
+
items.push({
|
|
173
|
+
key: `review:${review.review_id}`,
|
|
174
|
+
kind: 'review',
|
|
175
|
+
companionNodeId: review.companion_node_id,
|
|
176
|
+
line: ` ${pen} ${muted(`${summary} · ↵ companion`)}${RESET}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
162
179
|
// Bash wait row: attention (human) -> active-now (running background work) ->
|
|
163
180
|
// scheduled-future (armed crons). The leading glyph distinguishes the row type.
|
|
164
181
|
// `x stop` is advertised on the row because it is the roster's only
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReviewDTO } from '../../../api/dto/reviews.js';
|
|
2
|
+
/** Compact cockpit row for one open review: state first (matching the cron and
|
|
3
|
+
* bash rows), then the file it is about. A review whose submit is already in
|
|
4
|
+
* reads as off the human's hands — the daemon approves it the moment the
|
|
5
|
+
* companion stops editing, and the origin node resumes on that approval. */
|
|
6
|
+
export declare function reviewWaitSummary(review: ReviewDTO, fileCap?: number): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// chrome/review-wait.ts — the attach-side formatter for this node's open
|
|
2
|
+
// document reviews, mirroring chrome/cron-wait.ts and chrome/bash-jobs.ts: one
|
|
3
|
+
// compact cockpit row per awaited review. Pure — takes the already-listed
|
|
4
|
+
// reviews, never calls the daemon itself (that's the roster's job). Enter on
|
|
5
|
+
// the row opens the review's companion viewer, which is where the editing the
|
|
6
|
+
// row reports on is actually happening.
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
/** The reviewed file as one cell, ellipsized to `cap` columns. */
|
|
9
|
+
function fileCell(file, cap) {
|
|
10
|
+
const name = basename(file);
|
|
11
|
+
return name.length <= cap ? name : `${name.slice(0, Math.max(1, cap - 1))}…`;
|
|
12
|
+
}
|
|
13
|
+
/** Compact cockpit row for one open review: state first (matching the cron and
|
|
14
|
+
* bash rows), then the file it is about. A review whose submit is already in
|
|
15
|
+
* reads as off the human's hands — the daemon approves it the moment the
|
|
16
|
+
* companion stops editing, and the origin node resumes on that approval. */
|
|
17
|
+
export function reviewWaitSummary(review, fileCap = 28) {
|
|
18
|
+
const file = fileCell(review.file, Math.max(8, fileCap));
|
|
19
|
+
return review.submit_requested_at === undefined
|
|
20
|
+
? `review open · ${file} · waiting on you`
|
|
21
|
+
: `review submitted · ${file} · companion still editing`;
|
|
22
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// highlight, ↑ off the TOP row returns to the editor, ↓ CLAMPS at the bottom,
|
|
6
6
|
// Enter opens the row (SWAPS this pane in place onto a node, or opens the wait
|
|
7
7
|
// detail surface for a human/cron/bash row: a human row opens the inbox, a cron
|
|
8
|
-
// row the Inspector's schedule section, a bash row the Inspector's jobs section
|
|
8
|
+
// row the Inspector's schedule section, a bash row the Inspector's jobs section,
|
|
9
|
+
// a review row the review's companion viewer),
|
|
9
10
|
// Esc / any typed key returns to the editor. Disabled entirely for a REMOTE
|
|
10
11
|
// attach: a remote graph id has no local pane to swap into and no local
|
|
11
12
|
// jobs/crons to inspect.
|
|
@@ -91,7 +92,13 @@ export function createRoster(s, hooks) {
|
|
|
91
92
|
// Local-only, same gate as ownedCrons: a remote graph has no local jobs dir
|
|
92
93
|
// to scan (and a local nodeId collision must never leak background jobs).
|
|
93
94
|
const bashJobs = remote ? [] : activeBackgroundBashJobs(contextDir(nodeId));
|
|
94
|
-
|
|
95
|
+
// Reviews this node is the origin of: the person still holds an open one, or
|
|
96
|
+
// the daemon is finishing a submitted one behind its companion. Local-only,
|
|
97
|
+
// like the crons above — a remote graph id has no local review surface.
|
|
98
|
+
const reviews = remote
|
|
99
|
+
? []
|
|
100
|
+
: (await cliClient().listReviews({ state: 'open', origin_node_id: nodeId })).reviews;
|
|
101
|
+
const built = await buildCanvasPanelLines(nodeId, hooks.asks(), pal, canvasSource, ownedCrons, bashJobs, reviews);
|
|
95
102
|
items = built.items;
|
|
96
103
|
// After an in-place swap, land on the previously open node, which has taken
|
|
97
104
|
// the swapped-to node's place in this self-excluding roster.
|
|
@@ -198,6 +205,20 @@ export function createRoster(s, hooks) {
|
|
|
198
205
|
hooks.openInspector('schedule');
|
|
199
206
|
return;
|
|
200
207
|
}
|
|
208
|
+
if (item.kind === 'review') {
|
|
209
|
+
if (role !== 'controller') {
|
|
210
|
+
hooks.setNotice('Read-only observer attach — cannot open the review companion');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
exit();
|
|
214
|
+
// Beside this pane, never in place: the person is asking to watch the
|
|
215
|
+
// companion, not to give up the view they are asking from.
|
|
216
|
+
execFile('crtr', ['surface', 'node', 'focus', item.companionNodeId], (err) => {
|
|
217
|
+
if (err !== null)
|
|
218
|
+
hooks.setNotice(`Could not open the companion: ${err.message.split('\n')[0]}`);
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
201
222
|
if (item.kind === 'bash') {
|
|
202
223
|
if (activeBackgroundBashJobs(contextDir(nodeId)).length === 0) {
|
|
203
224
|
hooks.setNotice('Background job already finished');
|
|
@@ -41,7 +41,7 @@ export function createChromeWidgets(ctx, state) {
|
|
|
41
41
|
const span = Math.max(1, (process.stdout.columns ?? 80) - 4);
|
|
42
42
|
queued.addChild(new Text(pal.muted(`queued (${session.queued.length}):`), 1, 0));
|
|
43
43
|
for (const msg of session.queued) {
|
|
44
|
-
const oneLine = msg.replace(/\s+/g, ' ').trim();
|
|
44
|
+
const oneLine = msg.text.replace(/\s+/g, ' ').trim();
|
|
45
45
|
queued.addChild(new Text(pal.faint(` ⋯ ${truncateToWidth(oneLine, span, '…')}`), 1, 0));
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// lifetime belongs to InputOverlayOwner; viewer permission belongs to the single
|
|
3
3
|
// ViewerCapabilities value supplied by session/input-wiring.
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
7
|
import { tmpdir } from 'node:os';
|
|
7
8
|
import { join } from 'node:path';
|
|
@@ -45,7 +46,7 @@ export class InputController {
|
|
|
45
46
|
this.emitCommand({ type: 'cycle_ladder', direction });
|
|
46
47
|
}
|
|
47
48
|
sendInterrupting(text) {
|
|
48
|
-
return this.emitDrive({ type: 'prompt', text, interrupt: true });
|
|
49
|
+
return this.emitDrive({ type: 'prompt', text, interrupt: true, message_id: randomUUID() });
|
|
49
50
|
}
|
|
50
51
|
attachDialog(request) {
|
|
51
52
|
this.overlays.attachDialog(request);
|
|
@@ -181,7 +182,7 @@ export class InputController {
|
|
|
181
182
|
if (delivery !== 'steer' && delivery !== 'prompt')
|
|
182
183
|
return;
|
|
183
184
|
const expanded = this.expandImagePlaceholders(classification.kind === 'slash' ? text : classification.text);
|
|
184
|
-
if (!this.emitDrive(delivery === 'steer' ? { type: 'steer', text: expanded } : { type: 'prompt', text: expanded }))
|
|
185
|
+
if (!this.emitDrive(delivery === 'steer' ? { type: 'steer', text: expanded, message_id: randomUUID() } : { type: 'prompt', text: expanded, message_id: randomUUID() }))
|
|
185
186
|
return;
|
|
186
187
|
this.editor.addToHistory(classification.trimmed);
|
|
187
188
|
this.editor.setText('');
|
|
@@ -193,7 +194,7 @@ export class InputController {
|
|
|
193
194
|
const trimmed = text.trim();
|
|
194
195
|
if (!trimmed)
|
|
195
196
|
return;
|
|
196
|
-
if (!this.emitDrive({ type: 'follow_up', text: this.expandImagePlaceholders(text) }))
|
|
197
|
+
if (!this.emitDrive({ type: 'follow_up', text: this.expandImagePlaceholders(text), message_id: randomUUID() }))
|
|
197
198
|
return;
|
|
198
199
|
this.editor.addToHistory(trimmed);
|
|
199
200
|
this.editor.setText('');
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// as cached but fails to reconnect). Action names verified against the
|
|
40
40
|
// installed adapter (pi-mcp-adapter index.ts: "auth-start" / "auth-complete" /
|
|
41
41
|
// "ui-messages"; `connect: <name>`).
|
|
42
|
-
import { createHash } from 'node:crypto';
|
|
42
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
43
43
|
import { existsSync } from 'node:fs';
|
|
44
44
|
import { homedir } from 'node:os';
|
|
45
45
|
import { join, resolve } from 'node:path';
|
|
@@ -257,6 +257,7 @@ export function startMcpServerAction(row, send, notify) {
|
|
|
257
257
|
send({
|
|
258
258
|
type: 'prompt',
|
|
259
259
|
text: `Run mcp({ action: "auth-start", server: ${nameJson} }) and show me the returned OAuth URL verbatim (with any instructions). If the server does not need OAuth, run mcp({ connect: ${nameJson} }) instead and report the result.`,
|
|
260
|
+
message_id: randomUUID(),
|
|
260
261
|
});
|
|
261
262
|
notify(`Asking the engine to start OAuth for ${row.name}…`);
|
|
262
263
|
}
|
|
@@ -264,6 +265,7 @@ export function startMcpServerAction(row, send, notify) {
|
|
|
264
265
|
send({
|
|
265
266
|
type: 'prompt',
|
|
266
267
|
text: `Run mcp({ connect: ${nameJson} }) to reconnect that MCP server and report the result. If it fails with an authentication or authorization error, run mcp({ action: "auth-start", server: ${nameJson} }) and show me the returned OAuth URL verbatim (with any instructions).`,
|
|
268
|
+
message_id: randomUUID(),
|
|
267
269
|
});
|
|
268
270
|
notify(`Asking the engine to reconnect ${row.name}…`);
|
|
269
271
|
}
|
|
@@ -1018,7 +1018,7 @@ export class ChatView {
|
|
|
1018
1018
|
if (generated !== null &&
|
|
1019
1019
|
(message.role !== 'custom' || message.display === true)) {
|
|
1020
1020
|
this.pushTranscriptText(generated.body);
|
|
1021
|
-
const component = new ContextMessageComponent(generatedContextExpandedBody(generated), this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), generated.label, ` ${generated.summary} — ctrl+o to expand`, generated.label === 'crtr context' || generated.kind === 'revive' || generated.kind === 'inbox');
|
|
1021
|
+
const component = new ContextMessageComponent(generatedContextExpandedBody(generated), this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), generated.label, ` ${generated.summary} — ctrl+o to expand`, generated.label === 'crtr context' || generated.kind === 'revive' || generated.kind === 'inbox' || generated.kind === 'review-approval');
|
|
1022
1022
|
// A successor broker's exact restart continuation is a visible seam, not
|
|
1023
1023
|
// a group member: the preceding span keeps its own recap and the next tool
|
|
1024
1024
|
// call begins a broker-aligned fresh group.
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
// session/whip.ts — viewer-local Sisyphus whip animation and steering cues.
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
3
5
|
const FRAME_MS = 40;
|
|
6
|
+
// The build copies this beside bundled viewer.js, whose import.meta.url is the
|
|
7
|
+
// runtime location after esbuild folds this module into that entrypoint.
|
|
8
|
+
const WHIP_SOUND_PATHS = [
|
|
9
|
+
fileURLToPath(new URL('./assets/whip-snap.mp3', import.meta.url)),
|
|
10
|
+
fileURLToPath(new URL('./assets/whip-crack.mp3', import.meta.url)),
|
|
11
|
+
fileURLToPath(new URL('./assets/whip-06.mp3', import.meta.url)),
|
|
12
|
+
];
|
|
13
|
+
/** Play a bundled crack without delaying the animation or input handling. */
|
|
14
|
+
function playWhipSound(soundPath) {
|
|
15
|
+
const command = process.platform === 'darwin' ? 'afplay' : process.platform === 'linux' ? 'aplay' : undefined;
|
|
16
|
+
if (command === undefined)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const child = spawn(command, [soundPath], { stdio: 'ignore', detached: true });
|
|
20
|
+
child.on('error', () => { });
|
|
21
|
+
child.unref();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* Sound playback must never interfere with the whip action. */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
const WHIP_FRAMES = [
|
|
5
28
|
[
|
|
6
29
|
"",
|
|
@@ -296,6 +319,7 @@ export class WhipAnimation {
|
|
|
296
319
|
tui;
|
|
297
320
|
pal;
|
|
298
321
|
frame = -1;
|
|
322
|
+
nextSound = 0;
|
|
299
323
|
timer;
|
|
300
324
|
constructor(tui, pal) {
|
|
301
325
|
this.tui = tui;
|
|
@@ -303,6 +327,8 @@ export class WhipAnimation {
|
|
|
303
327
|
}
|
|
304
328
|
play() {
|
|
305
329
|
this.stop();
|
|
330
|
+
playWhipSound(WHIP_SOUND_PATHS[this.nextSound]);
|
|
331
|
+
this.nextSound = (this.nextSound + 1) % WHIP_SOUND_PATHS.length;
|
|
306
332
|
this.frame = 0;
|
|
307
333
|
this.tui.requestRender();
|
|
308
334
|
this.timer = setInterval(() => {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// SCOPED OUT (no engine method): `/login`, `/logout`, `/share`,
|
|
23
23
|
// `/trust` — a brief "not supported in attach" notice, never a frame.
|
|
24
24
|
import { execFile } from 'node:child_process';
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
25
26
|
import { BUILTIN_SLASH_COMMANDS } from '../../../core/runtime/pi-vendored.js';
|
|
26
27
|
import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
|
|
27
28
|
import { viewerOperationForSlash } from '../input/capabilities.js';
|
|
@@ -448,6 +449,7 @@ function promoteNode(arg, ctx) {
|
|
|
448
449
|
ctx.send({
|
|
449
450
|
type: 'prompt',
|
|
450
451
|
text: 'You have just been promoted to an orchestrator. Your new-role guidance is arriving — read it, author your roadmap, and start delegating.',
|
|
452
|
+
message_id: randomUUID(),
|
|
451
453
|
});
|
|
452
454
|
});
|
|
453
455
|
child.stdin?.end();
|