@plurnk/plurnk 0.74.0 → 0.76.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/.env.defaults +3 -4
- package/README.md +60 -23
- package/completions/_plurnk +13 -0
- package/completions/plurnk.bash +14 -0
- package/completions/plurnk.fish +37 -0
- package/conformance/agui-client.json +863 -0
- package/dist/agents.d.ts +11 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +170 -0
- package/dist/agents.js.map +1 -0
- package/dist/agui.d.ts +1 -0
- package/dist/agui.d.ts.map +1 -1
- package/dist/agui.js +45 -5
- package/dist/agui.js.map +1 -1
- package/dist/agui_cli.d.ts +1 -4
- package/dist/agui_cli.d.ts.map +1 -1
- package/dist/agui_cli.js +29 -9
- package/dist/agui_cli.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -7
- package/dist/cli.js.map +1 -1
- package/dist/color.d.ts +2 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +5 -0
- package/dist/color.js.map +1 -0
- package/dist/completion.js +4 -4
- package/dist/completion.js.map +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +4 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/dispatcher.d.ts +8 -8
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +196 -104
- package/dist/dispatcher.js.map +1 -1
- package/dist/envdefaults.d.ts.map +1 -1
- package/dist/envdefaults.js +4 -6
- package/dist/envdefaults.js.map +1 -1
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +234 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.d.ts +11 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +213 -69
- package/dist/mcp.js.map +1 -1
- package/dist/paste.d.ts +1 -0
- package/dist/paste.d.ts.map +1 -1
- package/dist/paste.js +5 -0
- package/dist/paste.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/plan.d.ts +12 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +30 -0
- package/dist/plan.js.map +1 -0
- package/dist/proposal.d.ts +3 -5
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +54 -34
- package/dist/proposal.js.map +1 -1
- package/dist/reasoning-events.d.ts +23 -0
- package/dist/reasoning-events.d.ts.map +1 -0
- package/dist/reasoning-events.js +40 -0
- package/dist/reasoning-events.js.map +1 -0
- package/dist/reasoning.d.ts +11 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +10 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +93 -50
- package/dist/render.js.map +1 -1
- package/dist/skills.d.ts +7 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +152 -0
- package/dist/skills.js.map +1 -0
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +11 -11
- package/dist/stream.js.map +1 -1
- package/dist/subcommands.d.ts +3 -0
- package/dist/subcommands.d.ts.map +1 -1
- package/dist/subcommands.js +32 -11
- package/dist/subcommands.js.map +1 -1
- package/dist/transport.d.ts +12 -4
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +77 -21
- package/dist/transport.js.map +1 -1
- package/dist/tui.d.ts +23 -13
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +335 -92
- package/dist/tui.js.map +1 -1
- package/man/plurnk.1 +153 -0
- package/package.json +16 -9
package/dist/tui.js
CHANGED
|
@@ -19,12 +19,16 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
19
19
|
import PasteFilter from "./paste.js";
|
|
20
20
|
import { extractOpenPaths } from "./openpaths.js";
|
|
21
21
|
import { pathPartial, completePath, dslOpPartial, completeOps, dslStatement } from "./completion.js";
|
|
22
|
-
import { renderLogEntry, renderSummary, isPromptEntry,
|
|
23
|
-
import { renderProposalMenu, keyToResolution, isServerResolved,
|
|
22
|
+
import { renderLogEntry, renderReasoning, renderReasoningPreview, renderSummary, isPromptEntry, progressLabel, entryTarget, isEntryMaterialization } from "./render.js";
|
|
23
|
+
import { renderProposalMenu, keyToResolution, isServerResolved, renderQuestionMenu, questionChoices, answerForQuestion, editInEditor } from "./proposal.js";
|
|
24
24
|
import { ProblemError, renderDiagnostic, report, clientSubcommandUnknownVerb, NO_MODEL_HINT } from "./diagnostics.js";
|
|
25
25
|
import StreamTrace, { inlineable, renderInline } from "./stream.js";
|
|
26
26
|
import { runModels, runWorkspaceList, runWorkspaceWorkers, runLogRead } from "./subcommands.js";
|
|
27
|
+
import { Validator, } from "@plurnk/plurnk-contracts";
|
|
27
28
|
import { handleMcp } from "./mcp.js";
|
|
29
|
+
import { handleSkills } from "./skills.js";
|
|
30
|
+
import { handleAgents } from "./agents.js";
|
|
31
|
+
import { formatWorkerReasoning, readWorkerReasoning, setWorkerReasoning, } from "./reasoning.js";
|
|
28
32
|
export const renderTuiFailure = (cause) => {
|
|
29
33
|
if (cause instanceof ProblemError) {
|
|
30
34
|
return renderDiagnostic(cause.problem)
|
|
@@ -39,14 +43,15 @@ export const renderTuiFailure = (cause) => {
|
|
|
39
43
|
// ambiguous (workspace or worker?) and is gone. /rename retargets the current
|
|
40
44
|
// workspace's mutable handle (a worker's name is immutable — no /rename for workers).
|
|
41
45
|
export const VERBS = [
|
|
42
|
-
"help", "models", "workspaces", "workers", "log", "model", "child",
|
|
46
|
+
"help", "models", "workspaces", "workers", "log", "model", "child", "reasoning",
|
|
43
47
|
"yolo", "workspace", "rename", "worker", "stop", "quit",
|
|
44
|
-
"pick", "hide", "view", "drop", "members", "import", "script", "mcp",
|
|
48
|
+
"pick", "hide", "view", "drop", "members", "import", "script", "mcp", "editor",
|
|
45
49
|
"accept", "reject", "cancel", "edit",
|
|
46
50
|
];
|
|
47
51
|
export const TUI_HELP = [
|
|
48
|
-
" /models /workspaces /workers /log [n] inspect",
|
|
49
|
-
" /model <
|
|
52
|
+
" /models [search] /workspaces /workers /log [n] inspect",
|
|
53
|
+
" /model <selector> · /child <selector|inherit> parent and child models",
|
|
54
|
+
" /reasoning [off|adaptive|effort] inspect or set reasoning",
|
|
50
55
|
" /yolo toggle local auto-accept",
|
|
51
56
|
" /workspace [name] new workspace",
|
|
52
57
|
" /rename <name> rename this workspace (a mutable handle)",
|
|
@@ -58,8 +63,13 @@ export const TUI_HELP = [
|
|
|
58
63
|
" /members the model's resolved file universe (+ rules)",
|
|
59
64
|
" /import <path> dump a local file's content into the prompt",
|
|
60
65
|
" /script <path> run a .plk file (its DSL → op.parse)",
|
|
61
|
-
" /mcp
|
|
62
|
-
"
|
|
66
|
+
" /mcp list available workspace MCP servers",
|
|
67
|
+
" add <alias> <target> [options.json] · enable/disable/remove <alias>",
|
|
68
|
+
" oauth <alias> <callback-url>",
|
|
69
|
+
" /agents list this Worker's outbound A2A agents",
|
|
70
|
+
" discover <url> · add <alias> <url> [options.json] · enable/disable/remove <alias>",
|
|
71
|
+
" /skills list this Worker's Agent Skills",
|
|
72
|
+
" discover <query|source> · add <name> <source> [--global] · enable/disable/remove <name>",
|
|
63
73
|
" /accept /reject /cancel /edit resolve a pending proposal (or keys a/e/r/c)",
|
|
64
74
|
" /stop cancel the running loop",
|
|
65
75
|
" /quit exit",
|
|
@@ -67,6 +77,8 @@ export const TUI_HELP = [
|
|
|
67
77
|
" ## LOOK0 (uri) off-run READ — inspect a uri for you, not the model",
|
|
68
78
|
" Alt-p / Alt-n cycle ## LOOK0 through prior operations' targets",
|
|
69
79
|
" Ctrl-J / Alt-Enter insert a ↵ newline (editable); Enter submits",
|
|
80
|
+
" Esc cancel the running loop; idle: clear the line",
|
|
81
|
+
" /editor · Alt-e compose the prompt line in $EDITOR",
|
|
70
82
|
" Alt-m/s · Alt-R/L/Y/N/M · Alt-x quick verbs (nvim case): models workspaces runs",
|
|
71
83
|
" log yolo workspace members stop · Alt-h help",
|
|
72
84
|
].join("\n") + "\n";
|
|
@@ -93,7 +105,7 @@ export const expandNewlines = (line) => line.replaceAll(NL_MARK, "\n");
|
|
|
93
105
|
// owns a/e/n/p/u/w/k/l. Alt-b/f/d are skipped (readline word-ops).
|
|
94
106
|
export const ALT_SHORTCUTS = Object.freeze({
|
|
95
107
|
m: "/models", s: "/workspaces", R: "/workers", L: "/log",
|
|
96
|
-
Y: "/yolo", N: "/workspace", M: "/members", x: "/stop", h: "/help",
|
|
108
|
+
Y: "/yolo", N: "/workspace", M: "/members", x: "/stop", h: "/help", e: "/editor",
|
|
97
109
|
});
|
|
98
110
|
// An Alt-<letter> keypress (ESC then a single letter, no `[`/`O` → not an arrow
|
|
99
111
|
// or function key) mapped to its verb, or null. Case-sensitive (mirrors nvim).
|
|
@@ -140,8 +152,11 @@ export const parseSlash = (line) => {
|
|
|
140
152
|
// Plain readline machinery only; no screen takeover, no terminal hell.
|
|
141
153
|
// readline's async completer form (arity 2 → async). Verb/alias completion
|
|
142
154
|
// answers synchronously; path positions read the local fs (co-location law)
|
|
143
|
-
// and answer once readdir resolves.
|
|
144
|
-
|
|
155
|
+
// and answer once readdir resolves. A fragment holding a provider prefix
|
|
156
|
+
// (`/model openai/…`) completes lazily from one bounded, provider-scoped
|
|
157
|
+
// daemon catalog page ([§cli-plurnk-models]); the client never preloads or
|
|
158
|
+
// owns the Models.dev snapshot.
|
|
159
|
+
export const makeCompleter = (getAliases, cwd, getReasoningPolicies = () => [], getProviderModels = async () => []) => (line, callback) => {
|
|
145
160
|
const verbFrag = line.match(/^\/(\w*)$/);
|
|
146
161
|
if (verbFrag) {
|
|
147
162
|
callback(null, [VERBS.map((v) => `/${v}`).filter((v) => v.startsWith(line)), line]);
|
|
@@ -149,18 +164,48 @@ export const makeCompleter = (getAliases, cwd) => (line, callback) => {
|
|
|
149
164
|
}
|
|
150
165
|
const aliasFrag = line.match(/^\/(model|child)\s+(\S*)$/);
|
|
151
166
|
if (aliasFrag) {
|
|
167
|
+
const fragment = aliasFrag[2];
|
|
168
|
+
const provider = /^([A-Za-z0-9_-]+)\//.exec(fragment)?.[1];
|
|
169
|
+
if (provider !== undefined) {
|
|
170
|
+
getProviderModels(provider).then((selectors) => callback(null, [selectors.filter((selector) => selector.startsWith(fragment)), fragment]), () => callback(null, [[], fragment]));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
152
173
|
const candidates = aliasFrag[1] === "child"
|
|
153
174
|
? ["inherit", ...getAliases().filter((alias) => alias !== "inherit")]
|
|
154
175
|
: getAliases();
|
|
155
|
-
callback(null, [candidates.filter((a) => a.startsWith(
|
|
176
|
+
callback(null, [candidates.filter((a) => a.startsWith(fragment)), fragment]);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const reasoningFrag = line.match(/^\/reasoning\s+(\S*)$/);
|
|
180
|
+
if (reasoningFrag) {
|
|
181
|
+
callback(null, [
|
|
182
|
+
getReasoningPolicies().filter((policy) => policy.startsWith(reasoningFrag[1])),
|
|
183
|
+
reasoningFrag[1],
|
|
184
|
+
]);
|
|
156
185
|
return;
|
|
157
186
|
}
|
|
158
187
|
const mcpFrag = line.match(/^\/mcp\s+(\S*)$/);
|
|
159
188
|
if (mcpFrag) {
|
|
160
189
|
const fragment = mcpFrag[1];
|
|
161
|
-
const commands = ["
|
|
190
|
+
const commands = ["add", "enable", "disable", "remove", "oauth"]
|
|
162
191
|
.filter((command) => command.startsWith(fragment));
|
|
163
|
-
|
|
192
|
+
callback(null, [commands, fragment]);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const agentsFrag = line.match(/^\/agents\s+(\S*)$/);
|
|
196
|
+
if (agentsFrag) {
|
|
197
|
+
const fragment = agentsFrag[1];
|
|
198
|
+
const commands = ["add", "discover", "enable", "disable", "remove"]
|
|
199
|
+
.filter((command) => command.startsWith(fragment));
|
|
200
|
+
callback(null, [commands, fragment]);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const skillsFrag = line.match(/^\/skills\s+(\S*)$/);
|
|
204
|
+
if (skillsFrag) {
|
|
205
|
+
const fragment = skillsFrag[1];
|
|
206
|
+
const commands = ["add", "discover", "enable", "disable", "remove"]
|
|
207
|
+
.filter((command) => command.startsWith(fragment));
|
|
208
|
+
callback(null, [commands, fragment]);
|
|
164
209
|
return;
|
|
165
210
|
}
|
|
166
211
|
const op = dslOpPartial(line);
|
|
@@ -190,26 +235,50 @@ export const seedPromptHistory = async (rpc, workspaceId, rl) => {
|
|
|
190
235
|
catch { /* history is a convenience; never block the REPL */ }
|
|
191
236
|
};
|
|
192
237
|
// The one-line startup banner: version · workspace [· worker] · model · help. Pure so the
|
|
193
|
-
// model-label resolution is unit-testable. modelLabel = the client's
|
|
194
|
-
// --model
|
|
238
|
+
// model-label resolution is unit-testable. modelLabel = the client's explicit
|
|
239
|
+
// --model selector when set, else the daemon's active default
|
|
195
240
|
// (providers.list `active`), else an honest fallback.
|
|
196
241
|
export const buildHeader = (opts) => {
|
|
197
242
|
const head = opts.versionNotice ?? "plurnk";
|
|
198
243
|
const worker = opts.workerName !== undefined ? ` · worker: ${opts.workerName}` : "";
|
|
199
|
-
const modelLabel = opts.
|
|
244
|
+
const modelLabel = opts.modelSelector ?? opts.activeAlias ?? "(daemon default)";
|
|
245
|
+
const reasoning = opts.reasoningPolicy === undefined || opts.reasoningPolicy === null
|
|
246
|
+
? ""
|
|
247
|
+
: ` · reasoning: ${opts.reasoningPolicy}`;
|
|
200
248
|
const yolo = opts.yolo ? " · yolo: on" : "";
|
|
201
|
-
return `${head} · workspace: ${opts.workspaceName}${worker} · model: ${modelLabel}${yolo} · /help`;
|
|
249
|
+
return `${head} · workspace: ${opts.workspaceName}${worker} · model: ${modelLabel}${reasoning}${yolo} · /help`;
|
|
202
250
|
};
|
|
251
|
+
const modelRouteOrNull = (value) => value === null ? null : Validator.assertModelRoute(value);
|
|
252
|
+
const workerModelProjection = (value) => {
|
|
253
|
+
if (value === null || typeof value !== "object") {
|
|
254
|
+
throw new TypeError("worker.model.get returned no model projection");
|
|
255
|
+
}
|
|
256
|
+
const projection = value;
|
|
257
|
+
return {
|
|
258
|
+
model: modelRouteOrNull(projection.model),
|
|
259
|
+
spawnModel: modelRouteOrNull(projection.spawnModel),
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
export const resolvedModelLabel = (spec) => spec.alias ?? `${spec.provider}/${spec.model}`;
|
|
203
263
|
export const handleVerb = async (line, ctx) => {
|
|
204
264
|
const { verb, rest } = parseSlash(line);
|
|
205
265
|
const { rpc, opts, write } = ctx;
|
|
266
|
+
const refreshWorkerPolicy = async () => {
|
|
267
|
+
const model = workerModelProjection(await rpc.call("worker.model.get"));
|
|
268
|
+
ctx.setModel(model.model);
|
|
269
|
+
ctx.setSpawnModel(model.spawnModel);
|
|
270
|
+
ctx.setReasoning(await readWorkerReasoning(rpc));
|
|
271
|
+
};
|
|
206
272
|
switch (verb) {
|
|
207
273
|
case "":
|
|
208
274
|
case "help":
|
|
209
275
|
write(TUI_HELP);
|
|
210
276
|
return;
|
|
211
277
|
case "models":
|
|
212
|
-
await runModels(rpc, {
|
|
278
|
+
await runModels(rpc, {
|
|
279
|
+
json: false,
|
|
280
|
+
query: rest.length === 0 ? {} : { search: rest },
|
|
281
|
+
});
|
|
213
282
|
return;
|
|
214
283
|
case "workspaces":
|
|
215
284
|
await runWorkspaceList(rpc, { json: false });
|
|
@@ -225,30 +294,53 @@ export const handleVerb = async (line, ctx) => {
|
|
|
225
294
|
}
|
|
226
295
|
case "model":
|
|
227
296
|
if (rest.length === 0) {
|
|
228
|
-
write(` model: ${
|
|
297
|
+
write(` model: ${ctx.model === null ? "(daemon default)" : resolvedModelLabel(ctx.model)}\n`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// {§worker-model-selection} — /model is a server-backed durable selection:
|
|
301
|
+
// the daemon resolves and persists onto the conversation worker; nothing
|
|
302
|
+
// client-local rides the next loop.
|
|
303
|
+
try {
|
|
304
|
+
ctx.setModel(Validator.assertModelRoute(await rpc.call("worker.model.set", { selector: rest })));
|
|
305
|
+
write(` model: ${rest}\n`);
|
|
306
|
+
}
|
|
307
|
+
catch (cause) {
|
|
308
|
+
write(` model set failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
229
309
|
return;
|
|
230
310
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
311
|
+
try {
|
|
312
|
+
ctx.setReasoning(await readWorkerReasoning(rpc));
|
|
313
|
+
}
|
|
314
|
+
catch (cause) {
|
|
315
|
+
write(` reasoning refresh failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
case "reasoning":
|
|
319
|
+
try {
|
|
320
|
+
const reasoning = rest.length === 0
|
|
321
|
+
? await readWorkerReasoning(rpc)
|
|
322
|
+
: await setWorkerReasoning(rpc, rest);
|
|
323
|
+
ctx.setReasoning(reasoning);
|
|
324
|
+
write(`${formatWorkerReasoning(reasoning).trimEnd().replace(/^/gm, " ")}\n`);
|
|
325
|
+
}
|
|
326
|
+
catch (cause) {
|
|
327
|
+
write(`${renderTuiFailure(cause)}\n`);
|
|
328
|
+
}
|
|
237
329
|
return;
|
|
238
330
|
case "child":
|
|
239
331
|
if (rest.length === 0) {
|
|
240
|
-
write(` child: ${
|
|
332
|
+
write(` child: ${ctx.spawnModel === null ? "inherit" : resolvedModelLabel(ctx.spawnModel)}\n`);
|
|
241
333
|
return;
|
|
242
334
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
335
|
+
try {
|
|
336
|
+
// {§worker-model-selection} — inherit IS the server action (selector null
|
|
337
|
+
// clears the override); the daemon returns null for it.
|
|
338
|
+
ctx.setSpawnModel(modelRouteOrNull(await rpc.call("worker.child.set", { selector: rest === "inherit" ? null : rest })));
|
|
339
|
+
write(` child: ${rest}\n`);
|
|
246
340
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
opts.childModel = ctx.resolveModel?.(rest);
|
|
341
|
+
catch (cause) {
|
|
342
|
+
write(` child set failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
250
343
|
}
|
|
251
|
-
write(` child: ${rest}\n`);
|
|
252
344
|
return;
|
|
253
345
|
case "yolo":
|
|
254
346
|
opts.yolo = !opts.yolo;
|
|
@@ -261,6 +353,7 @@ export const handleVerb = async (line, ctx) => {
|
|
|
261
353
|
// handle (/rename retargets it). client id (#249) + AGENTS override
|
|
262
354
|
// (#268) ride the switch.
|
|
263
355
|
ctx.setWorkspace(await ctx.switchWorkspace(rest.length > 0 ? rest : undefined));
|
|
356
|
+
await refreshWorkerPolicy();
|
|
264
357
|
write(` workspace: ${ctx.getWorkspace().name} (new)\n`);
|
|
265
358
|
return;
|
|
266
359
|
}
|
|
@@ -277,11 +370,12 @@ export const handleVerb = async (line, ctx) => {
|
|
|
277
370
|
return;
|
|
278
371
|
}
|
|
279
372
|
case "worker": {
|
|
280
|
-
// New worker —
|
|
373
|
+
// New worker — run.fork branches this conversation, optionally
|
|
281
374
|
// named at instantiation (immutable after). Bind to the fork so the
|
|
282
375
|
// next prompt speaks there. The workspace (the world) is unchanged.
|
|
283
|
-
const forked = await rpc.call("
|
|
376
|
+
const forked = await rpc.call("run.fork", rest.length > 0 ? { name: rest } : {});
|
|
284
377
|
await rpc.call("workspace.attach", { id: ctx.getWorkspace().id, workerId: forked.workerId });
|
|
378
|
+
await refreshWorkerPolicy();
|
|
285
379
|
write(` worker: ${forked.workerName} (new)\n`);
|
|
286
380
|
return;
|
|
287
381
|
}
|
|
@@ -309,8 +403,8 @@ export const handleVerb = async (line, ctx) => {
|
|
|
309
403
|
write(` no constraint matching ${JSON.stringify(rest)}\n`);
|
|
310
404
|
return;
|
|
311
405
|
}
|
|
312
|
-
for (const
|
|
313
|
-
await rpc.call("workspace.unconstrain",
|
|
406
|
+
for (const { effect, glob } of matches)
|
|
407
|
+
await rpc.call("workspace.unconstrain", { effect, glob });
|
|
314
408
|
write(` dropped ${matches.length} constraint${matches.length === 1 ? "" : "s"} (${rest})\n`);
|
|
315
409
|
return;
|
|
316
410
|
}
|
|
@@ -371,7 +465,15 @@ export const handleVerb = async (line, ctx) => {
|
|
|
371
465
|
return;
|
|
372
466
|
}
|
|
373
467
|
case "mcp": {
|
|
374
|
-
await handleMcp(rest, rpc, write);
|
|
468
|
+
await handleMcp(rest, rpc, write, { overlay: opts.mcpConfiguration });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
case "skills": {
|
|
472
|
+
await handleSkills(rest, rpc, write);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
case "agents": {
|
|
476
|
+
await handleAgents(rest, rpc, write);
|
|
375
477
|
return;
|
|
376
478
|
}
|
|
377
479
|
case "accept":
|
|
@@ -384,6 +486,9 @@ export const handleVerb = async (line, ctx) => {
|
|
|
384
486
|
case "stop":
|
|
385
487
|
await rpc.call("loop.cancel", { reason: "user_stop" });
|
|
386
488
|
return;
|
|
489
|
+
case "editor":
|
|
490
|
+
await ctx.composeInEditor();
|
|
491
|
+
return;
|
|
387
492
|
case "quit":
|
|
388
493
|
return "quit";
|
|
389
494
|
default:
|
|
@@ -393,12 +498,23 @@ export const handleVerb = async (line, ctx) => {
|
|
|
393
498
|
};
|
|
394
499
|
export const runTui = async (transport, workspace, opts) => {
|
|
395
500
|
let current = workspace;
|
|
396
|
-
// Highest loop_seq the waterfall has shown — the next prompt is one beyond.
|
|
397
|
-
let lastLoopSeq = 0;
|
|
398
501
|
// Loop state, hoisted so buildPrompt can show a steer prompt while a loop
|
|
399
502
|
// runs and the line handler / SIGINT can share it.
|
|
400
503
|
let inFlight = false;
|
|
401
504
|
let cancelRequested = false;
|
|
505
|
+
// One cancel path for every interrupt gesture (Ctrl-C, Esc, /stop): the
|
|
506
|
+
// run's active drain cancels via loop.cancel; the pending loop resolves
|
|
507
|
+
// 499 and the REPL continues. A failed cancel SURFACES — a stop button
|
|
508
|
+
// that silently does nothing is the worst kind of broken.
|
|
509
|
+
const requestCancel = (reason) => {
|
|
510
|
+
if (cancelRequested)
|
|
511
|
+
return;
|
|
512
|
+
cancelRequested = true;
|
|
513
|
+
process.stdout.write("\r\x1b[2K \x1b[2mcancelling… (ctrl-c again to quit)\x1b[0m\n");
|
|
514
|
+
void transport.rpc("loop.cancel", { reason }).catch((err) => {
|
|
515
|
+
process.stdout.write(`\r\x1b[2K \x1b[31mcancel failed: ${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
516
|
+
});
|
|
517
|
+
};
|
|
402
518
|
// Prompt status gutter (#114, refined): two reserved slots — 🧮 while
|
|
403
519
|
// embeddings warm, and a busy glyph (⏳ working / 💤 hibernating) whenever a
|
|
404
520
|
// loop OR embedding is active. Glyphs when active, blanks when not, so the
|
|
@@ -414,6 +530,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
414
530
|
// Alt-n cycler — not synthesized log-entry coordinates. lookCursor walks them.
|
|
415
531
|
const priorTargets = [];
|
|
416
532
|
let lookCursor = null;
|
|
533
|
+
let liveReasoning = null;
|
|
417
534
|
// Print a line ABOVE the live readline prompt without eating the user's
|
|
418
535
|
// in-progress input. The canonical "log while prompting" idiom: wipe the
|
|
419
536
|
// prompt row, write the line, then re-render the prompt + current buffer on
|
|
@@ -433,9 +550,10 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
433
550
|
// Alias cache for /model completion + the active alias for the header —
|
|
434
551
|
// one cheap RPC, refreshed never (aliases are daemon-boot-time config).
|
|
435
552
|
// Awaited before the banner so the header can name the model the daemon
|
|
436
|
-
// will actually use when --model
|
|
553
|
+
// will actually use when --model is unset (providers.list
|
|
437
554
|
// marks the boot-time default `active`).
|
|
438
555
|
let aliasCache = [];
|
|
556
|
+
const providerModelCache = new Map();
|
|
439
557
|
let activeAlias;
|
|
440
558
|
// Alias list for completion + the header's active default. Both terminal gauges
|
|
441
559
|
// ride the loop usage envelope; this client never reconstructs curation policy or
|
|
@@ -448,12 +566,50 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
448
566
|
}
|
|
449
567
|
}
|
|
450
568
|
catch { /* completion stays empty; header falls back to (daemon default) */ }
|
|
569
|
+
// {§worker-model-selection} — the worker owns the model. Read the server truth
|
|
570
|
+
// for the header and the /model /child display; an EXPLICIT --model persists
|
|
571
|
+
// onto the worker at startup (a one-time durable selection, not a per-loop
|
|
572
|
+
// reassertion).
|
|
573
|
+
let workerModel = null;
|
|
574
|
+
let workerSpawnModel = null;
|
|
575
|
+
let workerReasoning = { policy: null, supportedPolicies: [] };
|
|
576
|
+
let reasoningFailure;
|
|
577
|
+
// Model identity is control-plane truth, not decorative header data. A
|
|
578
|
+
// transport failure or malformed projection leaves this client unable to
|
|
579
|
+
// know which durable worker policy it is presenting, so admission fails
|
|
580
|
+
// instead of silently relabeling the worker as the daemon default.
|
|
581
|
+
const initialModel = workerModelProjection(await transport.rpc("worker.model.get"));
|
|
582
|
+
workerModel = initialModel.model;
|
|
583
|
+
workerSpawnModel = initialModel.spawnModel;
|
|
584
|
+
if (opts.modelExplicit === true && opts.modelSelector !== undefined) {
|
|
585
|
+
// A deliberate selection is part of invocation admission, not display
|
|
586
|
+
// hydration. Refuse the TUI before it accepts input if the daemon cannot
|
|
587
|
+
// persist it; continuing would silently run the worker's previous model.
|
|
588
|
+
workerModel = Validator.assertModelRoute(await transport.rpc("worker.model.set", { selector: opts.modelSelector }));
|
|
589
|
+
}
|
|
590
|
+
if (opts.reasoningExplicit === true && opts.reasoningPolicy !== undefined) {
|
|
591
|
+
// Same admission rule as --model: an explicit policy must take effect or
|
|
592
|
+
// the invocation fails before any model work can run under stale policy.
|
|
593
|
+
workerReasoning = await setWorkerReasoning({ call: (method, params) => transport.rpc(method, params) }, opts.reasoningPolicy);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
try {
|
|
597
|
+
workerReasoning = await readWorkerReasoning({ call: (method, params) => transport.rpc(method, params) });
|
|
598
|
+
}
|
|
599
|
+
catch (cause) {
|
|
600
|
+
reasoningFailure = cause;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
451
603
|
// One header line: version · workspace [· worker] · model · help (see buildHeader).
|
|
452
604
|
const header = buildHeader({
|
|
453
605
|
versionNotice: opts.versionNotice, workspaceName: current.name, workerName: opts.workerName,
|
|
454
|
-
|
|
606
|
+
modelSelector: workerModel === null ? opts.modelSelector : resolvedModelLabel(workerModel), activeAlias,
|
|
607
|
+
reasoningPolicy: workerReasoning.policy,
|
|
608
|
+
yolo: opts.yolo,
|
|
455
609
|
});
|
|
456
610
|
process.stdout.write(`\x1b[2m${header}\x1b[0m\n\n`);
|
|
611
|
+
if (reasoningFailure !== undefined)
|
|
612
|
+
process.stdout.write(`${renderTuiFailure(reasoningFailure)}\n`);
|
|
457
613
|
// The prompt is the user's row, restricted to WIDTH-STABLE glyphs —
|
|
458
614
|
// settled empirically in two rounds. Round 1 (` 👤 ✉️ ✅ 201 : `)
|
|
459
615
|
// drifted by exactly one column: ✉️ is U+2709+VS16, the variation-
|
|
@@ -468,10 +624,6 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
468
624
|
// 💬 (U+1F4AC, stable-wide) fills the op slot the toxic ✉️ vacated.
|
|
469
625
|
// Policy: VS16/ambiguous glyphs are banned from the ENTIRE palette
|
|
470
626
|
// (render.ts) — stable widths are also what make columns align.
|
|
471
|
-
// The prompt is the user's row, coordinate-prefixed like every other
|
|
472
|
-
// waterfall line (§5.1). The coordinate is the next actionless prompt row.
|
|
473
|
-
// lastLoopSeq tracks the highest loop the waterfall has shown; the next
|
|
474
|
-
// prompt is one beyond it. Plain-ASCII coordinate — width-safe.
|
|
475
627
|
// The prompt is the SAME whether or not a loop is running — kept alive above
|
|
476
628
|
// the streaming traces by printAbove. The user just types; a line submitted
|
|
477
629
|
// while a loop is in flight is folded into it (loop.inject), else it starts
|
|
@@ -506,10 +658,10 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
506
658
|
const activePercent = embedding ? embeddingPercent
|
|
507
659
|
: searchFetching ? searchPercent
|
|
508
660
|
: branchPercent;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return `${yolo}${position}${origin} ${status} \x1b[
|
|
661
|
+
// No coordinate, no decorative code (plurnk#21): the prompt is
|
|
662
|
+
// `[gauge] 🐹 <status> : ` — the busy gauge appears only while active.
|
|
663
|
+
const position = activePercent !== null ? progressLabel(activePercent) : "";
|
|
664
|
+
return `${yolo}${position}${origin} ${status} \x1b[1m: \x1b[0m`;
|
|
513
665
|
};
|
|
514
666
|
// Bracketed-paste buffering (paste.ts): a multi-line paste must become ONE
|
|
515
667
|
// prompt, not one loop.run per line. readline reads a PassThrough we feed
|
|
@@ -525,7 +677,19 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
525
677
|
output: process.stdout,
|
|
526
678
|
terminal: true,
|
|
527
679
|
prompt: buildPrompt(),
|
|
528
|
-
completer: makeCompleter(() => aliasCache, process.cwd()),
|
|
680
|
+
completer: makeCompleter(() => aliasCache, process.cwd(), () => workerReasoning.supportedPolicies, async (provider) => {
|
|
681
|
+
// One bounded page per provider, cached for the session: lazy,
|
|
682
|
+
// provider-scoped, never the whole catalog.
|
|
683
|
+
const cached = providerModelCache.get(provider);
|
|
684
|
+
if (cached !== undefined)
|
|
685
|
+
return cached;
|
|
686
|
+
const page = await transport.rpc("models.list", { provider, limit: 100 });
|
|
687
|
+
const selectors = (page.items ?? [])
|
|
688
|
+
.map(({ selector }) => selector)
|
|
689
|
+
.filter((selector) => typeof selector === "string");
|
|
690
|
+
providerModelCache.set(provider, selectors);
|
|
691
|
+
return selectors;
|
|
692
|
+
}),
|
|
529
693
|
});
|
|
530
694
|
const reprompt = () => {
|
|
531
695
|
readline.cursorTo(process.stdout, 0);
|
|
@@ -542,11 +706,50 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
542
706
|
rl.setPrompt(buildPrompt());
|
|
543
707
|
rl.prompt(true);
|
|
544
708
|
};
|
|
545
|
-
|
|
546
|
-
printAbove = (text) => {
|
|
709
|
+
const clearPromptAndLiveReasoning = () => {
|
|
547
710
|
readline.cursorTo(process.stdout, 0);
|
|
548
711
|
readline.clearLine(process.stdout, 0);
|
|
712
|
+
if (liveReasoning?.shown === true) {
|
|
713
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
714
|
+
readline.cursorTo(process.stdout, 0);
|
|
715
|
+
readline.clearLine(process.stdout, 0);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const redrawLiveReasoning = () => {
|
|
719
|
+
if (liveReasoning?.shown === true) {
|
|
720
|
+
process.stdout.write(`${renderReasoningPreview(liveReasoning.content, process.stdout.columns ?? 80)}\n`);
|
|
721
|
+
}
|
|
722
|
+
rl.prompt(true);
|
|
723
|
+
};
|
|
724
|
+
// Now that rl exists, printAbove can re-render the prompt after each line.
|
|
725
|
+
// A live reasoning preview remains the final row above it, so concurrent
|
|
726
|
+
// stream or worker activity cannot corrupt the preview's cursor ownership.
|
|
727
|
+
printAbove = (text) => {
|
|
728
|
+
clearPromptAndLiveReasoning();
|
|
549
729
|
process.stdout.write(`${text}\n`);
|
|
730
|
+
redrawLiveReasoning();
|
|
731
|
+
};
|
|
732
|
+
const presentReasoning = (update) => {
|
|
733
|
+
if (update.phase === "start") {
|
|
734
|
+
if (liveReasoning !== null)
|
|
735
|
+
throw new TypeError("A second reasoning message started before the first ended.");
|
|
736
|
+
liveReasoning = { messageId: update.messageId, content: "", shown: false };
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (liveReasoning === null || liveReasoning.messageId !== update.messageId) {
|
|
740
|
+
throw new TypeError(`Reasoning ${update.phase} did not match the active message.`);
|
|
741
|
+
}
|
|
742
|
+
clearPromptAndLiveReasoning();
|
|
743
|
+
if (update.phase === "content") {
|
|
744
|
+
liveReasoning.content = update.content;
|
|
745
|
+
liveReasoning.shown = update.content.length > 0;
|
|
746
|
+
redrawLiveReasoning();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const content = update.content;
|
|
750
|
+
liveReasoning = null;
|
|
751
|
+
if (content.length > 0)
|
|
752
|
+
process.stdout.write(`${renderReasoning(content)}\n`);
|
|
550
753
|
rl.prompt(true);
|
|
551
754
|
};
|
|
552
755
|
// Replace the whole readline buffer with `text`, public-API only: Ctrl-U kills
|
|
@@ -590,13 +793,32 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
590
793
|
// handlers are forward-declared (assigned once verbCtx exists).
|
|
591
794
|
let pendingProposal = null;
|
|
592
795
|
let onProposalKey = () => { };
|
|
593
|
-
// A pending
|
|
594
|
-
// choice; anything else is free response), NOT the
|
|
595
|
-
// free response needs text. The line handler
|
|
796
|
+
// A pending request-user-input question ({§question-tool}): answered by TYPING
|
|
797
|
+
// (a number picks an enum choice; anything else is free response), NOT the
|
|
798
|
+
// a/e/r/c keypress path — free response needs text. The line handler
|
|
799
|
+
// intercepts while this is set.
|
|
596
800
|
let pendingQuestion = null;
|
|
597
801
|
let dispatchShortcut = () => { };
|
|
598
802
|
const onStdin = (chunk) => {
|
|
599
|
-
const
|
|
803
|
+
const text = chunk.toString("utf8");
|
|
804
|
+
// Esc interrupts (plurnk#25) — the agent-CLI convention: bare Esc with
|
|
805
|
+
// a loop in flight cancels it (same path as the first Ctrl-C); idle it
|
|
806
|
+
// clears the composed line. Read BEFORE the paste filter, which would
|
|
807
|
+
// otherwise hold a lone ESC as a possible split \x1b[200~ prefix. An
|
|
808
|
+
// exact lone-ESC chunk while no paste is open is the KEY — the same
|
|
809
|
+
// whole-chunk rule the Alt-shortcuts rely on.
|
|
810
|
+
if (text === "\x1b" && paste.idle()) {
|
|
811
|
+
if (inFlight) {
|
|
812
|
+
requestCancel("user_escape");
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (rl.line.length > 0) {
|
|
816
|
+
rl.write(null, { ctrl: true, name: "e" });
|
|
817
|
+
rl.write(null, { ctrl: true, name: "u" });
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const forward = paste.feed(text);
|
|
600
822
|
if (forward.length === 0)
|
|
601
823
|
return;
|
|
602
824
|
// A pending proposal + an EMPTY prompt line: a single review key
|
|
@@ -745,9 +967,8 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
745
967
|
repromptPreserving();
|
|
746
968
|
};
|
|
747
969
|
transport.subscribe({
|
|
970
|
+
onReasoning: presentReasoning,
|
|
748
971
|
onEntry: (entry) => {
|
|
749
|
-
if (entry.loop_seq > lastLoopSeq)
|
|
750
|
-
lastLoopSeq = entry.loop_seq;
|
|
751
972
|
// The typed line at the prompt is the user's record — rendering the
|
|
752
973
|
// prompt broadcast too would duplicate it (see isPromptEntry).
|
|
753
974
|
if (isPromptEntry(entry))
|
|
@@ -787,15 +1008,6 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
787
1008
|
}
|
|
788
1009
|
},
|
|
789
1010
|
onProposal: (p) => {
|
|
790
|
-
// SEND signal 300 question FIRST — even a yolo loop stops the world for a human;
|
|
791
|
-
// the line handler resolves it from the next typed line.
|
|
792
|
-
const q = questionFromProposal(p);
|
|
793
|
-
if (q !== null) {
|
|
794
|
-
pendingQuestion = { logEntryId: p.logEntryId, choices: q.choices };
|
|
795
|
-
printAbove(renderQuestionMenu(q.question, q.choices));
|
|
796
|
-
reprompt();
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
1011
|
if (isServerResolved(p))
|
|
800
1012
|
return;
|
|
801
1013
|
if (opts.yolo) {
|
|
@@ -806,6 +1018,14 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
806
1018
|
proposalQueue.push(p);
|
|
807
1019
|
showNextProposal();
|
|
808
1020
|
},
|
|
1021
|
+
onInteraction: (i) => {
|
|
1022
|
+
// The question tool paused its loop: render the standard message + the
|
|
1023
|
+
// schema's single-property enum choices; the line handler resolves the
|
|
1024
|
+
// typed answer back into the paused run.
|
|
1025
|
+
pendingQuestion = { interactionId: i.interactionId, schema: i.responseSchema };
|
|
1026
|
+
printAbove(renderQuestionMenu(i.message, questionChoices(i.responseSchema)));
|
|
1027
|
+
reprompt();
|
|
1028
|
+
},
|
|
809
1029
|
// A loop terminating just clears the parked glyph; the summary is rendered
|
|
810
1030
|
// from the run's own done (below).
|
|
811
1031
|
onTerminated: () => { hibernating = false; },
|
|
@@ -849,7 +1069,13 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
849
1069
|
// Verb dispatch runs through the testable module-level handleVerb; this
|
|
850
1070
|
// context injects the live workspace / opts / stdout / import glue.
|
|
851
1071
|
const verbCtx = {
|
|
852
|
-
rpc: verbRpc, opts,
|
|
1072
|
+
rpc: verbRpc, opts,
|
|
1073
|
+
get model() { return workerModel; },
|
|
1074
|
+
get spawnModel() { return workerSpawnModel; },
|
|
1075
|
+
get reasoning() { return workerReasoning; },
|
|
1076
|
+
setModel: (spec) => { workerModel = spec; },
|
|
1077
|
+
setSpawnModel: (spec) => { workerSpawnModel = spec; },
|
|
1078
|
+
setReasoning: (reasoning) => { workerReasoning = reasoning; },
|
|
853
1079
|
getWorkspace: () => current,
|
|
854
1080
|
setWorkspace: (s) => { current = s; },
|
|
855
1081
|
switchWorkspace: (name) => transport.useSession(name, { projectRoot: opts.projectRoot, client: opts.client }),
|
|
@@ -877,6 +1103,32 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
877
1103
|
}
|
|
878
1104
|
await resolvePending({ decision: action });
|
|
879
1105
|
},
|
|
1106
|
+
// /editor · Alt-e (plurnk#26): seed $EDITOR with the composed line
|
|
1107
|
+
// (markers expanded), place the result BACK on the line — never
|
|
1108
|
+
// auto-submit (bash Ctrl-X Ctrl-E executes on exit; a prompt line
|
|
1109
|
+
// spends model tokens, so Enter stays the only submit). Same bounded
|
|
1110
|
+
// terminal handoff as proposal edit. Empty buffer ⇒ line unchanged.
|
|
1111
|
+
composeInEditor: async () => {
|
|
1112
|
+
process.stdin.off("data", onStdin);
|
|
1113
|
+
process.stdin.setRawMode?.(false);
|
|
1114
|
+
let edited = null;
|
|
1115
|
+
try {
|
|
1116
|
+
edited = await editInEditor(paste.expand(expandNewlines(rl.line)), ".md");
|
|
1117
|
+
}
|
|
1118
|
+
catch (cause) {
|
|
1119
|
+
process.stdout.write(`${renderTuiFailure(cause)}\n`);
|
|
1120
|
+
}
|
|
1121
|
+
finally {
|
|
1122
|
+
process.stdin.setRawMode?.(true);
|
|
1123
|
+
process.stdin.resume();
|
|
1124
|
+
process.stdin.on("data", onStdin);
|
|
1125
|
+
}
|
|
1126
|
+
if (edited === null)
|
|
1127
|
+
return;
|
|
1128
|
+
rl.write(null, { ctrl: true, name: "e" });
|
|
1129
|
+
rl.write(null, { ctrl: true, name: "u" });
|
|
1130
|
+
rl.write(paste.stash(edited.replace(/\n$/, "")));
|
|
1131
|
+
},
|
|
880
1132
|
};
|
|
881
1133
|
// Alt-<letter> shortcut → run the verb. Wipe the prompt line, dispatch
|
|
882
1134
|
// through the same handleVerb the typed `/verb` uses (the partial prompt in
|
|
@@ -899,19 +1151,20 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
899
1151
|
reprompt();
|
|
900
1152
|
return new Promise((resolve) => {
|
|
901
1153
|
rl.on("line", async (line) => {
|
|
902
|
-
// A pending
|
|
903
|
-
// BEFORE any verb/prompt/inject handling
|
|
904
|
-
// choice, anything else is free response
|
|
905
|
-
//
|
|
1154
|
+
// A pending request-user-input question consumes the typed line as its
|
|
1155
|
+
// answer, BEFORE any verb/prompt/inject handling: a number picks an
|
|
1156
|
+
// enum choice, anything else is free response (or raw JSON for a
|
|
1157
|
+
// multi-property schema). Empty → re-ask. Resolves the paused run with
|
|
1158
|
+
// the standard ElicitResult payload.
|
|
906
1159
|
if (pendingQuestion !== null) {
|
|
907
|
-
const
|
|
908
|
-
if (
|
|
1160
|
+
const content = answerForQuestion(line, pendingQuestion.schema);
|
|
1161
|
+
if (content === null) {
|
|
909
1162
|
reprompt();
|
|
910
1163
|
return;
|
|
911
1164
|
}
|
|
912
|
-
const {
|
|
1165
|
+
const { interactionId } = pendingQuestion;
|
|
913
1166
|
pendingQuestion = null;
|
|
914
|
-
await transport.
|
|
1167
|
+
await transport.resolveInteraction(interactionId, { action: "accept", content })
|
|
915
1168
|
.catch((e) => printAbove(` \x1b[31manswer failed: ${e instanceof Error ? e.message : String(e)}\x1b[0m`));
|
|
916
1169
|
reprompt();
|
|
917
1170
|
return;
|
|
@@ -998,15 +1251,9 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
998
1251
|
// Prompt. `? ` = ask (read-only loop, flags.mode="ask");
|
|
999
1252
|
// `: ` = act. Per-line prefix overrides --flags mode.
|
|
1000
1253
|
const { flags: lineFlags, prompt: promptText } = lineMode(trimmed, opts.loopFlags);
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (opts.model !== undefined)
|
|
1005
|
-
loopParams.model = opts.model; // #90 client-resolved routing (precedence)
|
|
1006
|
-
if (opts.childAlias !== undefined)
|
|
1007
|
-
loopParams.childAlias = opts.childAlias;
|
|
1008
|
-
if (opts.childModel !== undefined)
|
|
1009
|
-
loopParams.childModel = opts.childModel;
|
|
1254
|
+
// {§worker-model-selection} — no model selector rides the loop: the
|
|
1255
|
+
// worker owns the model; /model and /child persisted it server-side.
|
|
1256
|
+
const loopParams = { prompt: promptText, requestUserInput: opts.requestUserInput !== false };
|
|
1010
1257
|
if (lineFlags !== undefined && Object.keys(lineFlags).length > 0)
|
|
1011
1258
|
loopParams.flags = lineFlags;
|
|
1012
1259
|
if (opts.maxTurns !== undefined)
|
|
@@ -1057,11 +1304,7 @@ export const runTui = async (transport, workspace, opts) => {
|
|
|
1057
1304
|
// unblock (op.parse). A failed cancel SURFACES — a stop button that
|
|
1058
1305
|
// silently does nothing is the worst kind of broken.
|
|
1059
1306
|
if (inFlight && !cancelRequested) {
|
|
1060
|
-
|
|
1061
|
-
process.stdout.write("\r\x1b[2K \x1b[2mcancelling… (ctrl-c again to quit)\x1b[0m\n");
|
|
1062
|
-
void transport.rpc("loop.cancel", { reason: "user_sigint" }).catch((err) => {
|
|
1063
|
-
process.stdout.write(`\r\x1b[2K \x1b[31mcancel failed: ${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
1064
|
-
});
|
|
1307
|
+
requestCancel("user_sigint");
|
|
1065
1308
|
return;
|
|
1066
1309
|
}
|
|
1067
1310
|
rl.close();
|