@plurnk/plurnk 0.71.4 → 0.72.0
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/README.md +4 -4
- package/dist/agui.d.ts +13 -0
- package/dist/agui.d.ts.map +1 -1
- package/dist/agui.js +54 -9
- package/dist/agui.js.map +1 -1
- package/dist/agui_cli.d.ts +12 -15
- package/dist/agui_cli.d.ts.map +1 -1
- package/dist/agui_cli.js +67 -34
- package/dist/agui_cli.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts +6 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -7
- package/dist/cli.js.map +1 -1
- package/dist/completion.js +1 -1
- package/dist/completion.js.map +1 -1
- package/dist/diagnostics.d.ts +65 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +228 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dispatcher.d.ts +2 -5
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +85 -84
- package/dist/dispatcher.js.map +1 -1
- package/dist/render.d.ts +22 -5
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +37 -34
- package/dist/render.js.map +1 -1
- package/dist/stream.d.ts +4 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +5 -4
- package/dist/stream.js.map +1 -1
- package/dist/subcommands.d.ts.map +1 -1
- package/dist/subcommands.js +16 -22
- package/dist/subcommands.js.map +1 -1
- package/dist/transport.d.ts +20 -7
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +141 -40
- package/dist/transport.js.map +1 -1
- package/dist/tui.d.ts +7 -4
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +129 -97
- package/dist/tui.js.map +1 -1
- package/package.json +6 -4
- package/dist/telemetry.d.ts +0 -48
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -246
- package/dist/telemetry.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -21,11 +21,16 @@ import { extractOpenPaths } from "./openpaths.js";
|
|
|
21
21
|
import { pathPartial, completePath, dslOpPartial, completeOps } from "./completion.js";
|
|
22
22
|
import { renderLogEntry, renderSummary, isPromptEntry, coordLabel, progressLabel, entryTarget, isEntryMaterialization } from "./render.js";
|
|
23
23
|
import { renderProposalMenu, keyToResolution, isServerResolved, questionFromProposal, renderQuestionMenu, answerForLine } from "./proposal.js";
|
|
24
|
-
import {
|
|
25
|
-
import { renderTelemetryEvent, report, clientSubcommandUnknownVerb, NO_MODEL_HINT } from "./telemetry.js";
|
|
24
|
+
import { ProblemError, renderDiagnostic, report, clientSubcommandUnknownVerb, NO_MODEL_HINT } from "./diagnostics.js";
|
|
26
25
|
import StreamTrace, { inlineable, renderInline } from "./stream.js";
|
|
27
|
-
import { runOAuth } from "./auth.js";
|
|
28
26
|
import { runModels, runWorkspaceList, runWorkspaceWorkers, runLogRead } from "./subcommands.js";
|
|
27
|
+
export const renderTuiFailure = (cause) => {
|
|
28
|
+
if (cause instanceof ProblemError) {
|
|
29
|
+
return renderDiagnostic(cause.problem)
|
|
30
|
+
+ (cause.problem.status === 501 ? NO_MODEL_HINT : "");
|
|
31
|
+
}
|
|
32
|
+
return ` \x1b[31merror: ${cause instanceof Error ? cause.message : String(cause)}\x1b[0m`;
|
|
33
|
+
};
|
|
29
34
|
// One verb vocabulary across nvim's :AI/, the TUI, and (where they exist)
|
|
30
35
|
// the argv subcommands. Convergence is policy: divergence needs a reason.
|
|
31
36
|
// Singular = CREATE, plural = LIST: /workspace makes a new workspace, /workspaces
|
|
@@ -33,14 +38,14 @@ import { runModels, runWorkspaceList, runWorkspaceWorkers, runLogRead } from "./
|
|
|
33
38
|
// ambiguous (workspace or worker?) and is gone. /rename retargets the current
|
|
34
39
|
// workspace's mutable handle (a worker's name is immutable — no /rename for workers).
|
|
35
40
|
export const VERBS = [
|
|
36
|
-
"help", "models", "workspaces", "workers", "log", "model",
|
|
41
|
+
"help", "models", "workspaces", "workers", "log", "model", "child",
|
|
37
42
|
"yolo", "workspace", "rename", "worker", "stop", "quit",
|
|
38
|
-
"pick", "hide", "view", "
|
|
39
|
-
"
|
|
43
|
+
"pick", "hide", "view", "drop", "members", "import", "script",
|
|
44
|
+
"accept", "reject", "cancel", "edit",
|
|
40
45
|
];
|
|
41
46
|
export const TUI_HELP = [
|
|
42
|
-
" /models /workspaces /
|
|
43
|
-
" /model <alias>
|
|
47
|
+
" /models /workspaces /workers /log [n] inspect",
|
|
48
|
+
" /model <alias> · /child <alias|inherit> parent and child models",
|
|
44
49
|
" /yolo toggle local auto-accept",
|
|
45
50
|
" /workspace [name] new workspace",
|
|
46
51
|
" /rename <name> rename this workspace (a mutable handle)",
|
|
@@ -48,12 +53,10 @@ export const TUI_HELP = [
|
|
|
48
53
|
" /pick <glob> membership: track file(s) in manifest",
|
|
49
54
|
" /hide <glob> membership: block file(s) from manifest",
|
|
50
55
|
" /view <glob> membership: track file(s) in manifest (read-only)",
|
|
51
|
-
" /
|
|
52
|
-
" /drop <glob> membership: remove tracking rules for file(s), remove git tracking for folder",
|
|
56
|
+
" /drop <glob> membership: remove a matching rule",
|
|
53
57
|
" /members the model's resolved file universe (+ rules)",
|
|
54
58
|
" /import <path> dump a local file's content into the prompt",
|
|
55
59
|
" /script <path> run a .plk file (its DSL → op.parse)",
|
|
56
|
-
" /auth <target> OAuth an auth-protected exec (e.g. notion) via device code",
|
|
57
60
|
" /accept /reject /cancel /edit resolve a pending proposal (or keys a/e/r/c)",
|
|
58
61
|
" /stop cancel the running loop",
|
|
59
62
|
" /quit exit",
|
|
@@ -86,7 +89,7 @@ export const expandNewlines = (line) => line.replaceAll(NL_MARK, "\n");
|
|
|
86
89
|
// doesn't change a control code), Ctrl-s is XOFF, Ctrl-i is Tab, and readline
|
|
87
90
|
// owns a/e/n/p/u/w/k/l. Alt-b/f/d are skipped (readline word-ops).
|
|
88
91
|
export const ALT_SHORTCUTS = Object.freeze({
|
|
89
|
-
m: "/models", s: "/workspaces", R: "/
|
|
92
|
+
m: "/models", s: "/workspaces", R: "/workers", L: "/log",
|
|
90
93
|
Y: "/yolo", N: "/workspace", M: "/members", x: "/stop", h: "/help",
|
|
91
94
|
});
|
|
92
95
|
// An Alt-<letter> keypress (ESC then a single letter, no `[`/`O` → not an arrow
|
|
@@ -96,14 +99,9 @@ export const altShortcut = (forward) => {
|
|
|
96
99
|
const m = forward.match(/^\x1b([a-zA-Z])$/);
|
|
97
100
|
return m ? (ALT_SHORTCUTS[m[1]] ?? null) : null;
|
|
98
101
|
};
|
|
99
|
-
//
|
|
100
|
-
// The
|
|
101
|
-
|
|
102
|
-
// [signal](target)<sel>:body through untouched — the daemon only ever sees a real
|
|
103
|
-
// READ. `\b` keeps `<<LOOKUP` from matching. null when the line isn't a LOOK.
|
|
104
|
-
export const lookRewrite = (line) => /^<<LOOK\b/i.test(line)
|
|
105
|
-
? line.replace(/^<<LOOK\b/i, "<<READ").replace(/:LOOK(\s*)$/i, ":READ$1")
|
|
106
|
-
: null;
|
|
102
|
+
// Recognize the client-only LOOK surface so it can be routed to `op.look`.
|
|
103
|
+
// The AG-UI observation action owns validation and the single LOOK→READ rewrite.
|
|
104
|
+
export const lookStatement = (line) => /^<<LOOK\b/i.test(line) ? line : null;
|
|
107
105
|
// §2.0 prompt prefixes (converged with nvim's :AI ? / :AI :): `? ` puts mode:"ask"
|
|
108
106
|
// on the wire flags, `: ` forces act, both overriding a --flags base mode; `...`
|
|
109
107
|
// (injection) strips without minting flags. Mode is a per-line habit, never --ask.
|
|
@@ -135,7 +133,7 @@ export const parseSlash = (line) => {
|
|
|
135
133
|
const m = line.match(/^\/(\S*)\s*(.*)$/);
|
|
136
134
|
return { verb: m?.[1] ?? "", rest: (m?.[2] ?? "").trim() };
|
|
137
135
|
};
|
|
138
|
-
// readline completer — verbs after `/`, model aliases after `/model `.
|
|
136
|
+
// readline completer — verbs after `/`, model aliases after `/model ` or `/child `.
|
|
139
137
|
// Plain readline machinery only; no screen takeover, no terminal hell.
|
|
140
138
|
// readline's async completer form (arity 2 → async). Verb/alias completion
|
|
141
139
|
// answers synchronously; path positions read the local fs (co-location law)
|
|
@@ -146,9 +144,12 @@ export const makeCompleter = (getAliases, cwd) => (line, callback) => {
|
|
|
146
144
|
callback(null, [VERBS.map((v) => `/${v}`).filter((v) => v.startsWith(line)), line]);
|
|
147
145
|
return;
|
|
148
146
|
}
|
|
149
|
-
const aliasFrag = line.match(/^\/model\s+(\S*)$/);
|
|
147
|
+
const aliasFrag = line.match(/^\/(model|child)\s+(\S*)$/);
|
|
150
148
|
if (aliasFrag) {
|
|
151
|
-
|
|
149
|
+
const candidates = aliasFrag[1] === "child"
|
|
150
|
+
? ["inherit", ...getAliases().filter((alias) => alias !== "inherit")]
|
|
151
|
+
: getAliases();
|
|
152
|
+
callback(null, [candidates.filter((a) => a.startsWith(aliasFrag[2])), aliasFrag[2]]);
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
154
155
|
const op = dslOpPartial(line);
|
|
@@ -223,6 +224,21 @@ export const handleVerb = async (line, ctx) => {
|
|
|
223
224
|
opts.model = ctx.resolveModel?.(rest);
|
|
224
225
|
write(` model: ${rest}\n`);
|
|
225
226
|
return;
|
|
227
|
+
case "child":
|
|
228
|
+
if (rest.length === 0) {
|
|
229
|
+
write(` child: ${opts.childAlias ?? "inherit"}\n`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (rest === "inherit") {
|
|
233
|
+
opts.childAlias = null;
|
|
234
|
+
opts.childModel = undefined;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
opts.childAlias = rest;
|
|
238
|
+
opts.childModel = ctx.resolveModel?.(rest);
|
|
239
|
+
}
|
|
240
|
+
write(` child: ${rest}\n`);
|
|
241
|
+
return;
|
|
226
242
|
case "yolo":
|
|
227
243
|
opts.yolo = !opts.yolo;
|
|
228
244
|
write(` yolo: ${opts.yolo ? "ON" : "OFF"}\n`);
|
|
@@ -260,9 +276,8 @@ export const handleVerb = async (line, ctx) => {
|
|
|
260
276
|
}
|
|
261
277
|
case "pick":
|
|
262
278
|
case "hide":
|
|
263
|
-
case "view":
|
|
264
|
-
|
|
265
|
-
// Membership overlay (svc#200/#242) — service vocabulary, live via
|
|
279
|
+
case "view": {
|
|
280
|
+
// Membership overlay — service vocabulary, live via
|
|
266
281
|
// workspace.constrain (workspace-scoped, re-resolved immediately).
|
|
267
282
|
if (rest.length === 0) {
|
|
268
283
|
write(` usage: /${verb} <glob>\n`);
|
|
@@ -298,7 +313,7 @@ export const handleVerb = async (line, ctx) => {
|
|
|
298
313
|
const editable = members.filter((m) => m.effect === "member");
|
|
299
314
|
const view = members.filter((m) => m.effect === "view");
|
|
300
315
|
if (members.length === 0 && hidden.length === 0) {
|
|
301
|
-
write(" the model's universe is empty — no members
|
|
316
|
+
write(" the model's universe is empty — no Git members or /pick rules\n");
|
|
302
317
|
}
|
|
303
318
|
else {
|
|
304
319
|
write(` the model's universe: ${members.length} file${members.length === 1 ? "" : "s"}`
|
|
@@ -351,19 +366,6 @@ export const handleVerb = async (line, ctx) => {
|
|
|
351
366
|
// Typed no-modifier fallback for the a/e/r/c proposal review keys.
|
|
352
367
|
await ctx.resolveProposal(verb);
|
|
353
368
|
return;
|
|
354
|
-
case "auth": {
|
|
355
|
-
// #116 — the OAuth device-grant leg: authorize → print URL + code →
|
|
356
|
-
// poll to authorized. Blocks the REPL while polling (Ctrl-C exits); the
|
|
357
|
-
// service half is stateless, we own the poll loop + expiry bound.
|
|
358
|
-
const target = rest.trim();
|
|
359
|
-
if (target.length === 0) {
|
|
360
|
-
write(" \x1b[2musage: /auth <target> (the exec needing auth, e.g. notion)\x1b[0m");
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
const r = await runOAuth(rpc, target, { print: write });
|
|
364
|
-
write(` ${r.ok ? "✅" : "❌"} ${r.message}`);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
369
|
case "stop":
|
|
368
370
|
await rpc.call("loop.cancel", { reason: "user_stop" });
|
|
369
371
|
return;
|
|
@@ -390,6 +392,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
390
392
|
let embeddingPercent = null;
|
|
391
393
|
let searchFetching = false;
|
|
392
394
|
let searchPercent = null;
|
|
395
|
+
let branchBatch = null;
|
|
393
396
|
let hibernating = false; // the loop parked on a SEND[202] (awaiting streams/workers)
|
|
394
397
|
// <<LOOK off-run inspection: the REAL target URIs of prior operations the
|
|
395
398
|
// waterfall has shown (oldest→newest, e.g. worker:///plan.md) feed the Alt-p/
|
|
@@ -448,13 +451,12 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
448
451
|
// the row identity. 🐹 (U+1F439, the brand head — was the generic 👤
|
|
449
452
|
// U+1F464) and ✅ (U+2705) are plain East-Asian-Wide single code points —
|
|
450
453
|
// width 2 in node AND every major terminal, no VS16 — so they stay; the
|
|
451
|
-
// 201 is the
|
|
454
|
+
// 201 is the input-affordance constant;
|
|
452
455
|
// 💬 (U+1F4AC, stable-wide) fills the op slot the toxic ✉️ vacated.
|
|
453
456
|
// Policy: VS16/ambiguous glyphs are banned from the ENTIRE palette
|
|
454
457
|
// (render.ts) — stable widths are also what make columns align.
|
|
455
458
|
// The prompt is the user's row, coordinate-prefixed like every other
|
|
456
|
-
// waterfall line (§5.1). The coordinate is the
|
|
457
|
-
// get: the prompt becomes the next loop's foist EDIT at <next>/01/01.
|
|
459
|
+
// waterfall line (§5.1). The coordinate is the next actionless prompt row.
|
|
458
460
|
// lastLoopSeq tracks the highest loop the waterfall has shown; the next
|
|
459
461
|
// prompt is one beyond it. Plain-ASCII coordinate — width-safe.
|
|
460
462
|
// The prompt is the SAME whether or not a loop is running — kept alive above
|
|
@@ -474,11 +476,23 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
474
476
|
// TWO lanes exactly, same as every waterfall row (identity · status), so the
|
|
475
477
|
// prompt sits flush in the ladder. All glyphs are same-width palette members;
|
|
476
478
|
// swapping in place keeps the columns and the readline cursor stable.
|
|
477
|
-
const busy = inFlight || embedding || searchFetching;
|
|
478
|
-
const origin = embedding ? "🧮" : searchFetching ? "🔎" : "🐹";
|
|
479
|
-
const status =
|
|
479
|
+
const busy = inFlight || embedding || searchFetching || branchBatch !== null;
|
|
480
|
+
const origin = embedding ? "🧮" : searchFetching ? "🔎" : branchBatch !== null ? "🌿" : "🐹";
|
|
481
|
+
const status = branchBatch?.state === "recovery_required"
|
|
482
|
+
? "❌"
|
|
483
|
+
: busy ? (inFlight && hibernating && branchBatch === null ? "💤" : "⏳") : " ";
|
|
480
484
|
const yolo = opts.yolo ? "🔥" : " ";
|
|
481
|
-
const
|
|
485
|
+
const branchCompleted = Number(branchBatch?.completed);
|
|
486
|
+
const branchTotal = Number(branchBatch?.total);
|
|
487
|
+
const branchPercent = branchBatch !== null
|
|
488
|
+
&& Number.isFinite(branchCompleted)
|
|
489
|
+
&& Number.isFinite(branchTotal)
|
|
490
|
+
&& branchTotal > 0
|
|
491
|
+
? Math.floor((branchCompleted / branchTotal) * 100)
|
|
492
|
+
: null;
|
|
493
|
+
const activePercent = embedding ? embeddingPercent
|
|
494
|
+
: searchFetching ? searchPercent
|
|
495
|
+
: branchPercent;
|
|
482
496
|
const position = activePercent !== null
|
|
483
497
|
? progressLabel(activePercent)
|
|
484
498
|
: coordLabel(lastLoopSeq + 1, 1, 1);
|
|
@@ -539,17 +553,14 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
539
553
|
return;
|
|
540
554
|
setLine(`<<LOOK(${priorTargets[lookCursor]})::LOOK`);
|
|
541
555
|
};
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// the contract: status is required, content is `string | null` (svc ReadResult).
|
|
548
|
-
const runLook = async (readText) => {
|
|
549
|
-
const r = await transport.rpc("op.look", { text: readText });
|
|
556
|
+
// LOOK is a pure query: AG-UI validates and rewrites the original statement, then
|
|
557
|
+
// resolves READ without writing a log entry. Run it on the conversation connection
|
|
558
|
+
// so run-relative targets resolve against the right run.
|
|
559
|
+
const runLook = async (lookText) => {
|
|
560
|
+
const r = await transport.rpc("op.look", { text: lookText });
|
|
550
561
|
const content = r.content ?? "";
|
|
551
562
|
printAbove(content.length > 0 ? content : ` \x1b[2m(look ${r.status}: no content)\x1b[0m`);
|
|
552
|
-
return r
|
|
563
|
+
return r;
|
|
553
564
|
};
|
|
554
565
|
// Multi-line composition. The non-submitting newline keys insert a single ↵
|
|
555
566
|
// marker into readline's ONE line; Enter submits, expanding ↵→\n. Keeping it
|
|
@@ -625,7 +636,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
625
636
|
await transport.resolve({ logEntryId: p.logEntryId, ...resolution });
|
|
626
637
|
}
|
|
627
638
|
catch (cause) {
|
|
628
|
-
process.stdout.write(
|
|
639
|
+
process.stdout.write(`${renderTuiFailure(cause)}\n`);
|
|
629
640
|
}
|
|
630
641
|
showNextProposal();
|
|
631
642
|
reprompt();
|
|
@@ -644,7 +655,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
644
655
|
resolution = (await keyToResolution("e", p)) ?? { decision: "cancel", outcome: "edit_failed" };
|
|
645
656
|
}
|
|
646
657
|
catch (cause) {
|
|
647
|
-
process.stdout.write(
|
|
658
|
+
process.stdout.write(`${renderTuiFailure(cause)}\n`);
|
|
648
659
|
resolution = { decision: "cancel", outcome: "edit_error" };
|
|
649
660
|
}
|
|
650
661
|
finally {
|
|
@@ -668,19 +679,19 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
668
679
|
// transport is live. Same bodies as the old inline rpc.onNotification handlers;
|
|
669
680
|
// they render the shared workspace's activity whether this REPL started the loop
|
|
670
681
|
// or a worker/second client did (multi-client observability).
|
|
671
|
-
const
|
|
682
|
+
const handleNotice = (notice) => {
|
|
672
683
|
// engine:turn liveness is the ⏳ gutter (inFlight), not a waterfall line.
|
|
673
|
-
if (
|
|
684
|
+
if (notice.source === "engine:turn")
|
|
674
685
|
return;
|
|
675
686
|
// Indexing progress is ephemeral prompt state, not an append-only waterfall
|
|
676
687
|
// record. The coordinate slot becomes a fixed-width percentage while warm,
|
|
677
688
|
// then returns to the next real L/T/S coordinate on completion.
|
|
678
|
-
if (
|
|
679
|
-
const phase = typeof
|
|
689
|
+
if (notice.source === "engine:derivation" && notice.kind === "embed_progress") {
|
|
690
|
+
const phase = typeof notice.phase === "string" ? notice.phase : null;
|
|
680
691
|
embedding = phase === "preparing"
|
|
681
|
-
|| (phase !== "complete" && phase !== "failed" && Number(
|
|
682
|
-
const completed = Number(
|
|
683
|
-
const total = Number(
|
|
692
|
+
|| (phase !== "complete" && phase !== "failed" && Number(notice.completed) < Number(notice.total));
|
|
693
|
+
const completed = Number(notice.completed);
|
|
694
|
+
const total = Number(notice.total);
|
|
684
695
|
embeddingPercent = embedding && Number.isFinite(completed) && Number.isFinite(total) && total > 0
|
|
685
696
|
? Math.floor((completed / total) * 100)
|
|
686
697
|
: null;
|
|
@@ -688,15 +699,37 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
688
699
|
return;
|
|
689
700
|
}
|
|
690
701
|
// Search acquisition is the same compact lifecycle shape: update the
|
|
691
|
-
// fixed-width prompt gauge, never append one
|
|
692
|
-
if (
|
|
693
|
-
searchFetching =
|
|
694
|
-
const percent = Number(
|
|
702
|
+
// fixed-width prompt gauge, never append one notice line per tick.
|
|
703
|
+
if (notice.kind === "search_progress" && notice.source.startsWith("exec:")) {
|
|
704
|
+
searchFetching = notice.phase !== "complete" && notice.phase !== "failed";
|
|
705
|
+
const percent = Number(notice.percent);
|
|
695
706
|
searchPercent = searchFetching && Number.isFinite(percent) ? percent : null;
|
|
696
707
|
repromptPreserving();
|
|
697
708
|
return;
|
|
698
709
|
}
|
|
699
|
-
printAbove(
|
|
710
|
+
printAbove(renderDiagnostic(notice));
|
|
711
|
+
};
|
|
712
|
+
const handleBranchBatch = (event) => {
|
|
713
|
+
let rendered = false;
|
|
714
|
+
if (event.state === "completed") {
|
|
715
|
+
branchBatch = null;
|
|
716
|
+
printAbove(` 🌿 branch batch ${event.batchId} complete (${event.completed ?? event.total ?? 0}/${event.total ?? event.completed ?? 0})`);
|
|
717
|
+
rendered = true;
|
|
718
|
+
}
|
|
719
|
+
else if (event.state === "failed") {
|
|
720
|
+
branchBatch = null;
|
|
721
|
+
printAbove(` \x1b[31m❌ branch batch ${event.batchId} failed: ${event.problem?.detail ?? "branch preflight failed"}\x1b[0m`);
|
|
722
|
+
rendered = true;
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
branchBatch = event;
|
|
726
|
+
if (event.state === "recovery_required") {
|
|
727
|
+
printAbove(` \x1b[31m❌ branch batch ${event.batchId} requires recovery: ${event.problem?.detail ?? "inspect the workspace Git state"}\x1b[0m`);
|
|
728
|
+
rendered = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!rendered)
|
|
732
|
+
repromptPreserving();
|
|
700
733
|
};
|
|
701
734
|
transport.subscribe({
|
|
702
735
|
onEntry: (entry) => {
|
|
@@ -716,17 +749,16 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
716
749
|
hibernating = entry.op === "SEND" && entry.signal === 202;
|
|
717
750
|
printAbove(renderLogEntry(entry));
|
|
718
751
|
},
|
|
719
|
-
|
|
752
|
+
onNotice: handleNotice,
|
|
753
|
+
onProblem: (problem) => printAbove(renderDiagnostic(problem)),
|
|
754
|
+
onBranchBatch: handleBranchBatch,
|
|
720
755
|
onStream: (payload) => {
|
|
721
|
-
// One channel for the lifecycle: concluded carries
|
|
756
|
+
// One channel for the lifecycle: concluded carries its exact result, a start
|
|
722
757
|
// event carries state. One start line, one conclusion line, tiny outputs inlined.
|
|
723
|
-
if (typeof payload.
|
|
758
|
+
if (typeof payload.result?.status === "number") {
|
|
724
759
|
const p = payload;
|
|
725
760
|
printAbove(streams.concluded(p));
|
|
726
|
-
|
|
727
|
-
if (p.closeStatus === 401)
|
|
728
|
-
printAbove(` 🔒 ${p.scheme} needs authorization — run \x1b[1m/auth ${p.scheme}\x1b[0m`);
|
|
729
|
-
void transport.rpc("entry.read", { target: p.target }).then((r) => {
|
|
761
|
+
void transport.rpc("entry.read", { target: p.target, workerId: p.workerId }).then((r) => {
|
|
730
762
|
const channels = r.entry?.channels ?? {};
|
|
731
763
|
for (const name of ["stdout", "stderr"]) {
|
|
732
764
|
const content = channels[name]?.content;
|
|
@@ -767,7 +799,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
767
799
|
});
|
|
768
800
|
// Startup warming begins while AG-UI is still establishing the workspace, before a
|
|
769
801
|
// request-scoped SSE exists. Poll the engine's latest structured state until terminal;
|
|
770
|
-
//
|
|
802
|
+
// Later membership actions stay on their own SSE while warming streams live events.
|
|
771
803
|
let lastDerivationState = "";
|
|
772
804
|
let derivationPoll = null;
|
|
773
805
|
const pollDerivation = async () => {
|
|
@@ -782,7 +814,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
782
814
|
const signature = JSON.stringify(status);
|
|
783
815
|
if (signature !== lastDerivationState) {
|
|
784
816
|
lastDerivationState = signature;
|
|
785
|
-
|
|
817
|
+
handleNotice({ ...status, source: "engine:derivation", kind: "embed_progress" });
|
|
786
818
|
}
|
|
787
819
|
if (status.phase === "complete" || status.phase === "failed") {
|
|
788
820
|
if (derivationPoll !== null)
|
|
@@ -807,7 +839,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
807
839
|
rpc: verbRpc, opts, resolveModel: opts.resolveModel,
|
|
808
840
|
getWorkspace: () => current,
|
|
809
841
|
setWorkspace: (s) => { current = s; },
|
|
810
|
-
switchWorkspace: (name) => transport.useSession(name, { projectRoot: opts.projectRoot, client: opts.client
|
|
842
|
+
switchWorkspace: (name) => transport.useSession(name, { projectRoot: opts.projectRoot, client: opts.client }),
|
|
811
843
|
write: (text) => { process.stdout.write(text); },
|
|
812
844
|
importFile: async (rest) => {
|
|
813
845
|
const abs = isAbsolute(rest) ? rest : resolve(process.cwd(), rest);
|
|
@@ -846,7 +878,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
846
878
|
}
|
|
847
879
|
}
|
|
848
880
|
catch (cause) {
|
|
849
|
-
process.stdout.write(
|
|
881
|
+
process.stdout.write(`${renderTuiFailure(cause)}\n`);
|
|
850
882
|
}
|
|
851
883
|
reprompt();
|
|
852
884
|
})();
|
|
@@ -899,8 +931,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
899
931
|
}
|
|
900
932
|
}
|
|
901
933
|
catch (cause) {
|
|
902
|
-
|
|
903
|
-
process.stdout.write(` \x1b[31merror: ${msg}\x1b[0m\n`);
|
|
934
|
+
process.stdout.write(`${renderTuiFailure(cause)}\n`);
|
|
904
935
|
}
|
|
905
936
|
reprompt();
|
|
906
937
|
return;
|
|
@@ -929,26 +960,25 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
929
960
|
reprompt();
|
|
930
961
|
const start = Date.now();
|
|
931
962
|
let turnCount = 0;
|
|
932
|
-
let
|
|
963
|
+
let terminalResult = { status: 0 };
|
|
933
964
|
let hitMaxTurns = false;
|
|
934
965
|
let usage;
|
|
935
966
|
try {
|
|
936
|
-
const lookText = trimmed.startsWith("<<") ?
|
|
967
|
+
const lookText = trimmed.startsWith("<<") ? lookStatement(trimmed) : null;
|
|
937
968
|
if (lookText !== null) {
|
|
938
969
|
// <<LOOK: off-run READ on the side connection — for me, not the model.
|
|
939
|
-
|
|
970
|
+
terminalResult = await runLook(lookText);
|
|
940
971
|
}
|
|
941
972
|
else if (trimmed.startsWith("<<")) {
|
|
942
973
|
// Raw DSL: send to op.parse
|
|
943
974
|
const result = await transport.rpc("op.parse", { text: trimmed });
|
|
944
|
-
|
|
975
|
+
terminalResult = result.results[result.results.length - 1] ?? { status: 0 };
|
|
945
976
|
}
|
|
946
977
|
else if (trimmed.startsWith("!")) {
|
|
947
978
|
// `! cmd` — exec via the daemon (proposal-gated like any
|
|
948
979
|
// side effect; output streams as stream/event traces).
|
|
949
980
|
const command = trimmed.replace(/^!+\s*/, "");
|
|
950
|
-
|
|
951
|
-
finalStatus = result.status;
|
|
981
|
+
terminalResult = await transport.rpc("op.exec", { command });
|
|
952
982
|
}
|
|
953
983
|
else {
|
|
954
984
|
// Prompt. `? ` = ask (read-only loop, flags.mode="ask");
|
|
@@ -959,6 +989,10 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
959
989
|
loopParams.alias = opts.modelAlias; // display label
|
|
960
990
|
if (opts.model !== undefined)
|
|
961
991
|
loopParams.model = opts.model; // #90 client-resolved routing (precedence)
|
|
992
|
+
if (opts.childAlias !== undefined)
|
|
993
|
+
loopParams.childAlias = opts.childAlias;
|
|
994
|
+
if (opts.childModel !== undefined)
|
|
995
|
+
loopParams.childModel = opts.childModel;
|
|
962
996
|
if (lineFlags !== undefined && Object.keys(lineFlags).length > 0)
|
|
963
997
|
loopParams.flags = lineFlags;
|
|
964
998
|
if (opts.maxTurns !== undefined)
|
|
@@ -967,22 +1001,19 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
967
1001
|
if (openPaths.length > 0)
|
|
968
1002
|
loopParams.openPaths = openPaths;
|
|
969
1003
|
// The transport owns the ack→terminated bridge; done resolves
|
|
970
|
-
// with the loop's outcome. A
|
|
971
|
-
//
|
|
1004
|
+
// with the loop's outcome. A pre-stream HTTP failure surfaces as
|
|
1005
|
+
// an exact ProblemError (caught below; 501 gets the .env pointer).
|
|
972
1006
|
const t = await transport.run(promptText, loopParams).done;
|
|
973
|
-
|
|
1007
|
+
terminalResult = t.result;
|
|
974
1008
|
hitMaxTurns = t.hitMaxTurns;
|
|
975
1009
|
turnCount = t.turnIds?.length ?? 0;
|
|
976
1010
|
usage = t.usage;
|
|
977
1011
|
}
|
|
978
1012
|
const wallMs = Date.now() - start;
|
|
979
|
-
printAbove(renderSummary(turnCount, wallMs,
|
|
1013
|
+
printAbove(renderSummary(turnCount, wallMs, terminalResult, hitMaxTurns, usage, usage?.promptBudget));
|
|
980
1014
|
}
|
|
981
1015
|
catch (cause) {
|
|
982
|
-
|
|
983
|
-
? cause.message + NO_MODEL_HINT
|
|
984
|
-
: cause instanceof Error ? cause.message : String(cause);
|
|
985
|
-
printAbove(` \x1b[31merror: ${msg}\x1b[0m`);
|
|
1016
|
+
printAbove(renderTuiFailure(cause));
|
|
986
1017
|
}
|
|
987
1018
|
finally {
|
|
988
1019
|
inFlight = false;
|
|
@@ -999,6 +1030,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
999
1030
|
process.stdout.write("\x1b[?2004l");
|
|
1000
1031
|
process.stdin.off("data", onStdin);
|
|
1001
1032
|
process.stdin.setRawMode?.(false);
|
|
1033
|
+
process.stdin.pause();
|
|
1002
1034
|
// Steal pi's nicety: hand back the one-liner to pick this workspace up.
|
|
1003
1035
|
process.stdout.write(`\n \x1b[2mresume this workspace: plurnk --workspace ${current.name}\x1b[0m\n`);
|
|
1004
1036
|
resolve();
|