@tt-a1i/openpi 0.1.1 → 0.3.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 +65 -28
- package/SETUP.md +8 -6
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/bash-policy.ts +219 -42
- package/extensions/plan-mode/index.ts +56 -19
- package/extensions/setup/index.ts +96 -10
- package/extensions/shared/child-session.ts +40 -4
- package/extensions/shared/editor-layers.ts +150 -0
- package/extensions/shared/setup-config.ts +26 -15
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +179 -96
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +7 -7
- package/extensions/subagents/src/ui/takeover.ts +231 -133
- package/extensions/subagents/src/ui/transcript.ts +252 -37
- package/extensions/subagents/src/ui/wait-result.ts +6 -19
- package/extensions/suggestions/index.ts +27 -18
- package/extensions/tasks/index.ts +63 -18
- package/extensions/ui-customization/footer.ts +65 -11
- package/extensions/workflows/graph-projection.ts +6 -4
- package/extensions/workflows/index.ts +44 -21
- package/extensions/workflows/invocation-ledger.ts +8 -2
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +10 -40
- package/extensions/workflows/replay-safety.ts +9 -8
- package/package.json +10 -10
- package/skills/subagents/SKILL.md +7 -1
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -25,24 +25,22 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* This module does not parse shell or admit shell composition. Its small
|
|
29
|
+
* tokenizer only recognizes words and quoted literal spans. `$`, backticks
|
|
30
|
+
* and `\` are refused everywhere; shell metacharacters are refused outside
|
|
31
|
+
* quotes. Globs are also refused outside quotes because the shell would expand
|
|
32
|
+
* them before the allowlisted program sees them, while a quoted glob is a
|
|
33
|
+
* literal pattern interpreted by that read-only program itself.
|
|
34
34
|
*/
|
|
35
|
-
const
|
|
35
|
+
const UNQUOTED_SHELL_METACHARACTERS = /[;&|<>(){}\n\r#]/;
|
|
36
|
+
const EXPANSION_CHARACTERS = /[$`\\]/;
|
|
37
|
+
const UNQUOTED_GLOB_CHARACTERS = new Set(["*", "?", "[", "]"]);
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* Tilde expansion
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* Tilde expansion is refused only at the start of an unquoted word. `HEAD~3`
|
|
41
|
+
* is ordinary revision syntax and must survive, while `~/notes` and
|
|
42
|
+
* `~user/x` resolve to a path this module never sees.
|
|
41
43
|
*/
|
|
42
|
-
const TILDE_EXPANSION = /(^|\s)~/;
|
|
43
|
-
|
|
44
|
-
/** Quotes hide word boundaries from the tokenizer below, so they are refused too. */
|
|
45
|
-
const QUOTES = /["']/;
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
46
|
* Read-only git subcommands. Absent on purpose: `config`, `stash`, `tag`,
|
|
@@ -194,6 +192,94 @@ const GH_FLAGS = new Set([
|
|
|
194
192
|
"--comments",
|
|
195
193
|
]);
|
|
196
194
|
|
|
195
|
+
const RG_FLAGS = new Set([
|
|
196
|
+
"-n",
|
|
197
|
+
"--line-number",
|
|
198
|
+
"-i",
|
|
199
|
+
"--ignore-case",
|
|
200
|
+
"-l",
|
|
201
|
+
"--files-with-matches",
|
|
202
|
+
"-c",
|
|
203
|
+
"--count",
|
|
204
|
+
"-w",
|
|
205
|
+
"-F",
|
|
206
|
+
"--fixed-strings",
|
|
207
|
+
"-e",
|
|
208
|
+
"--regexp",
|
|
209
|
+
"-g",
|
|
210
|
+
"--glob",
|
|
211
|
+
"-t",
|
|
212
|
+
"--type",
|
|
213
|
+
"--files",
|
|
214
|
+
"--hidden",
|
|
215
|
+
"--no-ignore",
|
|
216
|
+
"-A",
|
|
217
|
+
"-B",
|
|
218
|
+
"-C",
|
|
219
|
+
"--after-context",
|
|
220
|
+
"--before-context",
|
|
221
|
+
"--context",
|
|
222
|
+
"-m",
|
|
223
|
+
"--max-count",
|
|
224
|
+
"-o",
|
|
225
|
+
"--only-matching",
|
|
226
|
+
"--sort",
|
|
227
|
+
"--json",
|
|
228
|
+
"--color",
|
|
229
|
+
"-H",
|
|
230
|
+
"-N",
|
|
231
|
+
"--no-filename",
|
|
232
|
+
"-v",
|
|
233
|
+
"--invert-match",
|
|
234
|
+
"-u",
|
|
235
|
+
"-uu",
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
const FD_FLAGS = new Set([
|
|
239
|
+
"-e",
|
|
240
|
+
"--extension",
|
|
241
|
+
"-t",
|
|
242
|
+
"--type",
|
|
243
|
+
"-d",
|
|
244
|
+
"--max-depth",
|
|
245
|
+
"--min-depth",
|
|
246
|
+
"-H",
|
|
247
|
+
"--hidden",
|
|
248
|
+
"-I",
|
|
249
|
+
"--no-ignore",
|
|
250
|
+
"-g",
|
|
251
|
+
"--glob",
|
|
252
|
+
"-F",
|
|
253
|
+
"--fixed-strings",
|
|
254
|
+
"-p",
|
|
255
|
+
"--full-path",
|
|
256
|
+
"-a",
|
|
257
|
+
"--absolute-path",
|
|
258
|
+
"-l",
|
|
259
|
+
"--list-details",
|
|
260
|
+
"--color",
|
|
261
|
+
"-0",
|
|
262
|
+
"-S",
|
|
263
|
+
"--size",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const LS_FLAGS = new Set([
|
|
267
|
+
"-l",
|
|
268
|
+
"-a",
|
|
269
|
+
"-A",
|
|
270
|
+
"-h",
|
|
271
|
+
"-t",
|
|
272
|
+
"-r",
|
|
273
|
+
"-R",
|
|
274
|
+
"-d",
|
|
275
|
+
"-1",
|
|
276
|
+
"-S",
|
|
277
|
+
"-F",
|
|
278
|
+
"--color",
|
|
279
|
+
]);
|
|
280
|
+
const WC_FLAGS = new Set(["-l", "-w", "-c", "-m"]);
|
|
281
|
+
const HEAD_TAIL_FLAGS = new Set(["-n", "-c", "--lines", "--bytes"]);
|
|
282
|
+
|
|
197
283
|
/** `-5`, `-20`: git's count shorthand, which is a number rather than a flag. */
|
|
198
284
|
const NUMERIC_SHORTHAND = /^-\d+$/;
|
|
199
285
|
|
|
@@ -208,6 +294,83 @@ const refuse = (reason: string): BashPlanDecision => ({
|
|
|
208
294
|
reason,
|
|
209
295
|
});
|
|
210
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Tokenize words without pretending to be a shell parser. Quoted spans are
|
|
299
|
+
* removed and become literal text; no expansion or command composition is
|
|
300
|
+
* supported. The validation happens while tokenizing so an unquoted glob can
|
|
301
|
+
* never be mistaken for a literal program argument.
|
|
302
|
+
*/
|
|
303
|
+
function tokenize(command: string) {
|
|
304
|
+
const words: string[] = [];
|
|
305
|
+
let word = "";
|
|
306
|
+
let inWord = false;
|
|
307
|
+
let quote: "'" | '"' | undefined;
|
|
308
|
+
let wordStartsUnquoted = false;
|
|
309
|
+
|
|
310
|
+
for (const character of command) {
|
|
311
|
+
if (quote) {
|
|
312
|
+
if (character === quote) {
|
|
313
|
+
quote = undefined;
|
|
314
|
+
} else if (EXPANSION_CHARACTERS.test(character)) {
|
|
315
|
+
return refuse(
|
|
316
|
+
`plan mode rejected expansion character ${JSON.stringify(character)} — remove expansion syntax and pass literal arguments instead`,
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
word += character;
|
|
320
|
+
}
|
|
321
|
+
inWord = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (EXPANSION_CHARACTERS.test(character)) {
|
|
326
|
+
return refuse(
|
|
327
|
+
`plan mode rejected expansion character ${JSON.stringify(character)} — remove expansion syntax and pass literal arguments instead`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (UNQUOTED_SHELL_METACHARACTERS.test(character)) {
|
|
331
|
+
return refuse(
|
|
332
|
+
`plan mode rejected unquoted shell metacharacter ${JSON.stringify(character)} — run a single plain command without shell composition`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (UNQUOTED_GLOB_CHARACTERS.has(character)) {
|
|
336
|
+
return refuse(
|
|
337
|
+
"plan mode will not run an unquoted glob because the shell would expand it — quote it instead, e.g. --glob '*.ts'",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (character === "'" || character === '"') {
|
|
341
|
+
quote = character;
|
|
342
|
+
inWord = true;
|
|
343
|
+
if (!word) wordStartsUnquoted = false;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (/\s/.test(character)) {
|
|
347
|
+
if (inWord) {
|
|
348
|
+
words.push(word);
|
|
349
|
+
word = "";
|
|
350
|
+
inWord = false;
|
|
351
|
+
wordStartsUnquoted = false;
|
|
352
|
+
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!inWord) wordStartsUnquoted = true;
|
|
356
|
+
if (wordStartsUnquoted && word.length === 0 && character === "~") {
|
|
357
|
+
return refuse(
|
|
358
|
+
"plan mode does not run tilde-expanded paths — use a path relative to the project instead",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
word += character;
|
|
362
|
+
inWord = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (quote) {
|
|
366
|
+
return refuse(
|
|
367
|
+
`plan mode rejected an unterminated ${quote === "'" ? "single" : "double"} quote — close the quote or pass a literal argument instead`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (inWord) words.push(word);
|
|
371
|
+
return { allowed: true as const, words };
|
|
372
|
+
}
|
|
373
|
+
|
|
211
374
|
/** Split `--flag=value` into the flag part the allowlists are keyed on. */
|
|
212
375
|
function flagName(word: string) {
|
|
213
376
|
const eq = word.indexOf("=");
|
|
@@ -223,16 +386,27 @@ function scanArguments(
|
|
|
223
386
|
words: readonly string[],
|
|
224
387
|
allowed: ReadonlySet<string>,
|
|
225
388
|
program: string,
|
|
389
|
+
/**
|
|
390
|
+
* Whether `-la` may stand for `-l -a`. Only the file-inspection programs opt
|
|
391
|
+
* in: git and gh keep their historical one-flag-per-word rule, so widening
|
|
392
|
+
* the tokenizer cannot quietly widen their surface too.
|
|
393
|
+
*/
|
|
394
|
+
allowShortClusters = false,
|
|
226
395
|
): BashPlanDecision {
|
|
227
396
|
for (const word of words) {
|
|
228
397
|
if (word === "--") break;
|
|
229
398
|
if (!word.startsWith("-")) continue;
|
|
230
399
|
if (NUMERIC_SHORTHAND.test(word)) continue;
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
400
|
+
if (allowed.has(flagName(word))) continue;
|
|
401
|
+
const isAllowedCluster =
|
|
402
|
+
allowShortClusters &&
|
|
403
|
+
!word.startsWith("--") &&
|
|
404
|
+
word.length > 2 &&
|
|
405
|
+
[...word.slice(1)].every((character) => allowed.has(`-${character}`));
|
|
406
|
+
if (isAllowedCluster) continue;
|
|
407
|
+
return refuse(
|
|
408
|
+
`plan mode does not recognize "${word}" as a read-only ${program} option — use only the allowlisted ${program} flags`,
|
|
409
|
+
);
|
|
236
410
|
}
|
|
237
411
|
return { allowed: true };
|
|
238
412
|
}
|
|
@@ -249,21 +423,10 @@ export function planBashDecision(command: unknown): BashPlanDecision {
|
|
|
249
423
|
const text = command.trim();
|
|
250
424
|
if (!text) return refuse("plan mode received an empty command");
|
|
251
425
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
if (QUOTES.test(text)) {
|
|
258
|
-
return refuse("plan mode only runs unquoted commands while planning");
|
|
259
|
-
}
|
|
260
|
-
if (TILDE_EXPANSION.test(text)) {
|
|
261
|
-
return refuse(
|
|
262
|
-
"plan mode does not run commands with `~` paths — give a path relative to the project instead",
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const [program, ...rest] = text.split(/\s+/);
|
|
426
|
+
const tokenized = tokenize(text);
|
|
427
|
+
if (!("words" in tokenized)) return tokenized;
|
|
428
|
+
const [program, ...rest] = tokenized.words;
|
|
429
|
+
if (!program) return refuse("plan mode received an empty command");
|
|
267
430
|
|
|
268
431
|
/*
|
|
269
432
|
* The subcommand must be the FIRST word, never "the first word that is not a
|
|
@@ -300,14 +463,28 @@ export function planBashDecision(command: unknown): BashPlanDecision {
|
|
|
300
463
|
return scanArguments(args, GH_FLAGS, "gh");
|
|
301
464
|
}
|
|
302
465
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
466
|
+
const readOnlyPrograms = new Map([
|
|
467
|
+
["rg", RG_FLAGS],
|
|
468
|
+
["fd", FD_FLAGS],
|
|
469
|
+
["ls", LS_FLAGS],
|
|
470
|
+
["wc", WC_FLAGS],
|
|
471
|
+
["head", HEAD_TAIL_FLAGS],
|
|
472
|
+
["tail", HEAD_TAIL_FLAGS],
|
|
473
|
+
]);
|
|
474
|
+
const flags = readOnlyPrograms.get(program);
|
|
475
|
+
if (flags) {
|
|
476
|
+
if (
|
|
477
|
+
program === "tail" &&
|
|
478
|
+
rest.some((word) => word === "-f" || word === "--follow")
|
|
479
|
+
) {
|
|
480
|
+
return refuse(
|
|
481
|
+
'plan mode refuses "tail -f/--follow" because it can block forever — use a finite tail command instead',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return scanArguments(rest, flags, program, true);
|
|
485
|
+
}
|
|
486
|
+
|
|
310
487
|
return refuse(
|
|
311
|
-
`plan mode
|
|
488
|
+
`plan mode does not allow "${program}" — use read-only git and gh investigation commands; available commands are git, gh, rg, fd, ls, wc, head or tail, plus the read/grep/fd tools`,
|
|
312
489
|
);
|
|
313
490
|
}
|
|
@@ -36,6 +36,10 @@ import {
|
|
|
36
36
|
PLAN_MODE_CHANNEL,
|
|
37
37
|
type PlanModeState,
|
|
38
38
|
} from "../shared/plan-mode-state.ts";
|
|
39
|
+
import {
|
|
40
|
+
OPENPI_TOOL_SURFACE,
|
|
41
|
+
patchOwnedTools,
|
|
42
|
+
} from "../shared/tool-surface.ts";
|
|
39
43
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
40
44
|
import { planBashDecision } from "./bash-policy.ts";
|
|
41
45
|
|
|
@@ -138,8 +142,12 @@ export const PLAN_READY_ACTIONS = {
|
|
|
138
142
|
continue: "Continue planning",
|
|
139
143
|
current: "Implement in this session",
|
|
140
144
|
fresh: "Start a fresh session",
|
|
145
|
+
off: "Turn plan mode off",
|
|
141
146
|
} as const;
|
|
142
147
|
|
|
148
|
+
/** Menu label for the same effect as `/plan done`. */
|
|
149
|
+
const FINALIZE_NOW = "Finalize now";
|
|
150
|
+
|
|
143
151
|
export function buildPlanImplementationPrompt(plan: string) {
|
|
144
152
|
return [
|
|
145
153
|
"Implement the approved plan below. Re-check the repository state before editing, follow the project instructions, and verify the finished change.",
|
|
@@ -231,6 +239,12 @@ export function planToolCallDecision(
|
|
|
231
239
|
export default function planMode(pi: ExtensionAPI) {
|
|
232
240
|
let planning = false;
|
|
233
241
|
let readyPlan: string | undefined;
|
|
242
|
+
const syncPlanTool = () =>
|
|
243
|
+
patchOwnedTools(pi, "plan", {
|
|
244
|
+
...(planning && !readyPlan
|
|
245
|
+
? { enable: OPENPI_TOOL_SURFACE.plan.deferred }
|
|
246
|
+
: { disable: OPENPI_TOOL_SURFACE.plan.deferred }),
|
|
247
|
+
});
|
|
234
248
|
|
|
235
249
|
/**
|
|
236
250
|
* Publish the stance and reflect it in the footer. Every place `planning`
|
|
@@ -242,15 +256,12 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
242
256
|
hasUI: boolean;
|
|
243
257
|
ui: { setStatus: (key: string, value?: string) => void };
|
|
244
258
|
}) => {
|
|
259
|
+
syncPlanTool();
|
|
245
260
|
pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
|
|
246
261
|
if (!ctx.hasUI) return;
|
|
247
262
|
ctx.ui.setStatus(
|
|
248
263
|
"plan-mode",
|
|
249
|
-
readyPlan
|
|
250
|
-
? "plan mode · ready"
|
|
251
|
-
: planning
|
|
252
|
-
? "plan mode · read-only"
|
|
253
|
-
: undefined,
|
|
264
|
+
readyPlan ? "plan ready" : planning ? "plan mode" : undefined,
|
|
254
265
|
);
|
|
255
266
|
};
|
|
256
267
|
|
|
@@ -360,9 +371,25 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
360
371
|
implementHere(ctx);
|
|
361
372
|
} else if (choice === PLAN_READY_ACTIONS.fresh) {
|
|
362
373
|
await implementFresh(ctx);
|
|
374
|
+
} else if (choice === PLAN_READY_ACTIONS.off) {
|
|
375
|
+
clearPlan(ctx);
|
|
376
|
+
ctx.ui.notify("Plan mode is off.", "info");
|
|
363
377
|
}
|
|
364
378
|
};
|
|
365
379
|
|
|
380
|
+
const requestPlanFinalization = () => {
|
|
381
|
+
pi.sendMessage(
|
|
382
|
+
{
|
|
383
|
+
customType: "plan-finalize-requested",
|
|
384
|
+
content:
|
|
385
|
+
"Finalize the plan now. Resolve any remaining material ambiguity with ask_user; otherwise call plan_ready alone with the complete implementation-ready Markdown plan. Do not implement it.",
|
|
386
|
+
display: true,
|
|
387
|
+
details: {},
|
|
388
|
+
},
|
|
389
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
366
393
|
pi.registerTool({
|
|
367
394
|
name: "plan_ready",
|
|
368
395
|
label: "Plan Ready",
|
|
@@ -451,16 +478,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
451
478
|
ctx.ui.notify("Plan mode is not active.", "warning");
|
|
452
479
|
return;
|
|
453
480
|
}
|
|
454
|
-
|
|
455
|
-
{
|
|
456
|
-
customType: "plan-finalize-requested",
|
|
457
|
-
content:
|
|
458
|
-
"Finalize the plan now. Resolve any remaining material ambiguity with ask_user; otherwise call plan_ready alone with the complete implementation-ready Markdown plan. Do not implement it.",
|
|
459
|
-
display: true,
|
|
460
|
-
details: {},
|
|
461
|
-
},
|
|
462
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
463
|
-
);
|
|
481
|
+
requestPlanFinalization();
|
|
464
482
|
return;
|
|
465
483
|
}
|
|
466
484
|
|
|
@@ -470,10 +488,28 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
470
488
|
}
|
|
471
489
|
|
|
472
490
|
if (planning) {
|
|
473
|
-
ctx.
|
|
474
|
-
|
|
475
|
-
|
|
491
|
+
if (!ctx.hasUI) {
|
|
492
|
+
ctx.ui.notify(
|
|
493
|
+
"Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
|
|
494
|
+
"info",
|
|
495
|
+
);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const choice = await ctx.ui.select(
|
|
499
|
+
"Plan Mode — choose what happens next",
|
|
500
|
+
[PLAN_READY_ACTIONS.continue, FINALIZE_NOW, PLAN_READY_ACTIONS.off],
|
|
476
501
|
);
|
|
502
|
+
if (choice === PLAN_READY_ACTIONS.continue) {
|
|
503
|
+
ctx.ui.notify(
|
|
504
|
+
"Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
|
|
505
|
+
"info",
|
|
506
|
+
);
|
|
507
|
+
} else if (choice === FINALIZE_NOW) {
|
|
508
|
+
requestPlanFinalization();
|
|
509
|
+
} else if (choice === PLAN_READY_ACTIONS.off) {
|
|
510
|
+
clearPlan(ctx);
|
|
511
|
+
ctx.ui.notify("Plan mode is off.", "info");
|
|
512
|
+
}
|
|
477
513
|
return;
|
|
478
514
|
}
|
|
479
515
|
|
|
@@ -499,7 +535,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
499
535
|
return {
|
|
500
536
|
block: true as const,
|
|
501
537
|
reason:
|
|
502
|
-
"The plan is ready and the write gate remains closed. Wait for the user to choose the next action with `/plan`; do not call more tools.",
|
|
538
|
+
"The plan is ready and the write gate remains closed. Wait for the user to choose the next action with `/plan` or turn it off with `/plan off`; do not call more tools.",
|
|
503
539
|
};
|
|
504
540
|
}
|
|
505
541
|
const batchDecision =
|
|
@@ -532,6 +568,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
532
568
|
pi.on("session_shutdown", () => {
|
|
533
569
|
planning = false;
|
|
534
570
|
readyPlan = undefined;
|
|
571
|
+
syncPlanTool();
|
|
535
572
|
// Broadcast without a ctx: subagents keeps its own copy of the stance, and
|
|
536
573
|
// leaving it armed would restrict children in whatever session comes next.
|
|
537
574
|
pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type SubagentRoleModels,
|
|
11
11
|
} from "../shared/subagent-roles.ts";
|
|
12
12
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
13
|
+
import {
|
|
14
|
+
OPENPI_SETUP_EPISODE_CHANNEL,
|
|
15
|
+
type OpenPiSetupEpisodeState,
|
|
16
|
+
} from "../shared/setup-episode-state.ts";
|
|
17
|
+
import { patchOwnedTools } from "../shared/tool-surface.ts";
|
|
13
18
|
import {
|
|
14
19
|
formatPiIntercomStatus,
|
|
15
20
|
inspectPiIntercom,
|
|
@@ -18,6 +23,7 @@ import {
|
|
|
18
23
|
} from "./intercom.ts";
|
|
19
24
|
import {
|
|
20
25
|
applyFooterConfig,
|
|
26
|
+
CAPABILITY_DISCOVERY_MODES,
|
|
21
27
|
DETAIL_DISPLAYS,
|
|
22
28
|
FOOTER_ITEMS,
|
|
23
29
|
FOOTER_LAYOUT_ITEMS,
|
|
@@ -33,6 +39,7 @@ import {
|
|
|
33
39
|
REASONING_LEVELS,
|
|
34
40
|
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
35
41
|
type FooterLayoutItem,
|
|
42
|
+
type CapabilityDiscoveryMode,
|
|
36
43
|
type FooterPreset,
|
|
37
44
|
type FooterStyle,
|
|
38
45
|
type MyPiSetupConfig,
|
|
@@ -102,12 +109,12 @@ export function buildInteractiveSetupPrompt(options: {
|
|
|
102
109
|
}) {
|
|
103
110
|
const configurationState = options.savedConfigExists
|
|
104
111
|
? [
|
|
105
|
-
"This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Next-action suggestions, Workflow limits, UI/Footer, result detail display, Post-edit, Agent role models, or review everything.",
|
|
112
|
+
"This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Capability discovery, Next-action suggestions, Workflow limits, UI/Footer, result detail display, Post-edit, Agent role models, or review everything.",
|
|
106
113
|
"If the user keeps the current settings, do not call configure_my_pi_setup. If they choose a category, ask only the follow-up needed for that category.",
|
|
107
114
|
]
|
|
108
115
|
: [
|
|
109
116
|
"This is the first setup. Explain the available choices and their impact in the user's language, then collect the initial preferences.",
|
|
110
|
-
"Prefer one ask_user call with up to three independent questions covering Next-action suggestions,
|
|
117
|
+
"Prefer one ask_user call with up to three independent questions covering Capability discovery plus Workflow limits, Next-action suggestions, and UI/Footer/result display. Explain that Post-edit defaults off; keep it off unless the user opts in, then ask only for the command. Explain that built-in Agent roles used by subagent_spawn and workflow agent_type inherit the parent model unless the user assigns an available model to a role.",
|
|
111
118
|
];
|
|
112
119
|
|
|
113
120
|
return [
|
|
@@ -122,16 +129,19 @@ export function buildInteractiveSetupPrompt(options: {
|
|
|
122
129
|
...configurationState,
|
|
123
130
|
"",
|
|
124
131
|
"Before asking, briefly explain what can be configured and the practical impact:",
|
|
132
|
+
"- Capability discovery: explicit is the safe default and keeps OpenPI model tools absent until the user asks for a capability. adaptive is opt-in and keeps only the small openpi_load_tools gateway visible, allowing the model to load Subagents, Workflows, background terminals, structured search, or Session tracking when it judges them useful. Loaded groups remain session-stable, and normal permission, concurrency, and workflow limits still apply.",
|
|
125
133
|
"- Next-action suggestions: disabled, or model-generated after a fully settled main-agent run. A suggestion appears as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. Right accepts it without submitting, and any other editor input dismisses it. Enabling requires an available provider/model and reasoning level and adds one small model call per settled run.",
|
|
126
134
|
"- Workflow fan-out: concurrency controls simultaneous agents and resource pressure; max agent calls controls the total capacity of one workflow. Valid ranges are 1-64 and 1-1024.",
|
|
127
|
-
"- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (
|
|
135
|
+
"- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with cwd/git/pr on the left and model/context/cost on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Nerd Font only affects powerline separator glyphs; text stays readable without it. Changes apply immediately in the active TUI session.",
|
|
128
136
|
"- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
|
|
129
137
|
"- Post-edit command: one optional shell command (maximum 500 characters) run in the background after a turn with successful Write/Edit operations (e.g. `npm run format`). Off by default, interactive TUI sessions only, failures surface as a notification. This is a single command, not an event-hook system.",
|
|
130
|
-
"- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full
|
|
138
|
+
"- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use folded previews. Ctrl+O expands compact output by default. Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
|
|
131
139
|
"- Agent role models: built-in explorer, implementer, reviewer, and advisor roles are shared by subagent_spawn and workflow agent_type, and inherit the parent model by default. Assign only an available registry model to an individual role when needed; clearing that role returns it to inheritance. Custom agent-type files still override a built-in role's complete definition.",
|
|
132
140
|
"- Intercom: optional cross-session messaging is installed only after a native setup confirmation. It stays parent-only; Direct/Workflow children and Replay cannot use it. The status above is informational for this model-guided step—do not install packages or edit its config yourself.",
|
|
133
141
|
"",
|
|
134
142
|
"Natural-language configuration examples the user might ask for:",
|
|
143
|
+
'- "let the model discover OpenPI capabilities when useful" → capability_discovery=adaptive',
|
|
144
|
+
'- "only use OpenPI capabilities when I ask" → capability_discovery=explicit',
|
|
135
145
|
'- "switch footer to powerline" → ui_footer_preset=powerline',
|
|
136
146
|
'- "use mono powerline" → ui_footer_preset=powerline-mono',
|
|
137
147
|
'- "compact footer" → ui_footer_preset=compact',
|
|
@@ -151,6 +161,16 @@ const safeSetupNotice = (value: unknown, maximum = 500) =>
|
|
|
151
161
|
.trim()
|
|
152
162
|
.slice(0, maximum);
|
|
153
163
|
|
|
164
|
+
export function buildSetupSuccessText(
|
|
165
|
+
currentConfiguration: string,
|
|
166
|
+
normalizationNote = "",
|
|
167
|
+
) {
|
|
168
|
+
return [
|
|
169
|
+
`Updated OpenPI setup. ${currentConfiguration}${normalizationNote}`,
|
|
170
|
+
"This setup episode is complete; configure_my_pi_setup is now hidden. Do not call it again. Do not edit configuration files directly. If the user requests another configuration change, tell them to run /openpi-setup <request> to start a new setup episode.",
|
|
171
|
+
].join(" ");
|
|
172
|
+
}
|
|
173
|
+
|
|
154
174
|
export function shouldOfferPiIntercom(options: {
|
|
155
175
|
readonly request: string;
|
|
156
176
|
readonly status: PiIntercomStatus;
|
|
@@ -231,13 +251,72 @@ async function maybeOfferPiIntercom(
|
|
|
231
251
|
}
|
|
232
252
|
}
|
|
233
253
|
|
|
254
|
+
export const CONFIGURE_MY_PI_SETUP_TOOL_NAME = "configure_my_pi_setup";
|
|
255
|
+
|
|
256
|
+
type SetupEpisode = "idle" | "armed" | "active";
|
|
257
|
+
|
|
258
|
+
function showConfigureTool(pi: ExtensionAPI) {
|
|
259
|
+
patchOwnedTools(pi, "setup", {
|
|
260
|
+
enable: [CONFIGURE_MY_PI_SETUP_TOOL_NAME],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hideConfigureTool(pi: ExtensionAPI) {
|
|
265
|
+
patchOwnedTools(pi, "setup", {
|
|
266
|
+
disable: [CONFIGURE_MY_PI_SETUP_TOOL_NAME],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
234
270
|
export default function openPiSetup(pi: ExtensionAPI) {
|
|
271
|
+
let episode: SetupEpisode = "idle";
|
|
272
|
+
const publishEpisode = () =>
|
|
273
|
+
pi.events.emit(OPENPI_SETUP_EPISODE_CHANNEL, {
|
|
274
|
+
active: episode !== "idle",
|
|
275
|
+
} satisfies OpenPiSetupEpisodeState);
|
|
276
|
+
|
|
277
|
+
const endEpisode = () => {
|
|
278
|
+
episode = "idle";
|
|
279
|
+
hideConfigureTool(pi);
|
|
280
|
+
publishEpisode();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
pi.on("session_start", () => {
|
|
284
|
+
endEpisode();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
pi.on("agent_start", () => {
|
|
288
|
+
if (episode === "armed") {
|
|
289
|
+
episode = "active";
|
|
290
|
+
publishEpisode();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
pi.on("tool_execution_end", (event) => {
|
|
295
|
+
if (
|
|
296
|
+
episode === "active" &&
|
|
297
|
+
event.toolName === CONFIGURE_MY_PI_SETUP_TOOL_NAME &&
|
|
298
|
+
!event.isError
|
|
299
|
+
) {
|
|
300
|
+
endEpisode();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
pi.on("agent_settled", () => {
|
|
305
|
+
if (episode === "active") endEpisode();
|
|
306
|
+
});
|
|
307
|
+
|
|
235
308
|
pi.registerTool({
|
|
236
309
|
name: "configure_my_pi_setup",
|
|
237
310
|
label: "Configure OpenPI",
|
|
238
311
|
description:
|
|
239
|
-
"Apply a user-requested configuration change for this Pi setup. Configures next-action suggestions, workflow fan-out, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to
|
|
312
|
+
"Apply a user-requested configuration change for this Pi setup. Configures capability discovery (explicit or opt-in adaptive), next-action suggestions, workflow fan-out, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to the capability gateway and active TUI footer.",
|
|
240
313
|
parameters: Type.Object({
|
|
314
|
+
capability_discovery: Type.Optional(
|
|
315
|
+
StringEnum(CAPABILITY_DISCOVERY_MODES, {
|
|
316
|
+
description:
|
|
317
|
+
"Capability adoption policy. explicit keeps OpenPI tools absent until the user asks for a capability; adaptive keeps only the small openpi_load_tools gateway visible so the model may load a useful group on its own. Adaptive can start expensive work such as Subagents or Workflows, so it is opt-in. Omit to preserve the current value.",
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
241
320
|
suggestions_enabled: Type.Optional(
|
|
242
321
|
Type.Boolean({
|
|
243
322
|
description:
|
|
@@ -316,7 +395,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
316
395
|
subagent_result_display: Type.Optional(
|
|
317
396
|
StringEnum(DETAIL_DISPLAYS, {
|
|
318
397
|
description:
|
|
319
|
-
"How completed Subagent results render by default: full
|
|
398
|
+
"How completed Subagent results render by default: full shows complete output; compact shows only bounded status rows while app.tools.expand reveals the full child report. Omit to preserve the current value.",
|
|
320
399
|
}),
|
|
321
400
|
),
|
|
322
401
|
bash_tool_display: Type.Optional(
|
|
@@ -406,6 +485,12 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
406
485
|
);
|
|
407
486
|
|
|
408
487
|
const config: MyPiSetupConfig = {
|
|
488
|
+
capabilities: {
|
|
489
|
+
discovery:
|
|
490
|
+
(params.capability_discovery as
|
|
491
|
+
| CapabilityDiscoveryMode
|
|
492
|
+
| undefined) ?? current.capabilities.discovery,
|
|
493
|
+
},
|
|
409
494
|
suggestions: {
|
|
410
495
|
enabled: suggestionsEnabled,
|
|
411
496
|
...(model ? { model } : {}),
|
|
@@ -457,9 +542,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
457
542
|
: "";
|
|
458
543
|
if (ctx.hasUI) ctx.ui.notify(`${text}${note}`, "info");
|
|
459
544
|
return {
|
|
460
|
-
content: [
|
|
461
|
-
{ type: "text", text: `Updated OpenPI setup. ${text}${note}` },
|
|
462
|
-
],
|
|
545
|
+
content: [{ type: "text", text: buildSetupSuccessText(text, note) }],
|
|
463
546
|
details: config,
|
|
464
547
|
};
|
|
465
548
|
},
|
|
@@ -490,7 +573,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
490
573
|
"Current configuration:",
|
|
491
574
|
currentConfiguration,
|
|
492
575
|
"",
|
|
493
|
-
"Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. Nerd Font only affects powerline separator glyphs. Changes apply immediately in the active TUI session. Intercom installation is handled only by the native setup confirmation; do not install packages or edit its config yourself.",
|
|
576
|
+
"Capability discovery is explicit by default; adaptive is an opt-in that keeps only openpi_load_tools visible so the model may load useful groups. Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. Nerd Font only affects powerline separator glyphs. Changes apply immediately in the active TUI session. Intercom installation is handled only by the native setup confirmation; do not install packages or edit its config yourself.",
|
|
494
577
|
"",
|
|
495
578
|
"Use configure_my_pi_setup to apply only the requested OpenPI-owned changes and preserve everything else. Interpret model names from the available Pi registry. Do not edit configuration files directly.",
|
|
496
579
|
]
|
|
@@ -501,6 +584,9 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
501
584
|
savedConfigExists,
|
|
502
585
|
});
|
|
503
586
|
|
|
587
|
+
episode = "armed";
|
|
588
|
+
showConfigureTool(pi);
|
|
589
|
+
publishEpisode();
|
|
504
590
|
pi.sendUserMessage(
|
|
505
591
|
prompt.join("\n"),
|
|
506
592
|
ctx.isIdle() ? undefined : { deliverAs: "followUp" },
|