@tt-a1i/openpi 0.2.0 → 0.3.1

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 CHANGED
@@ -5,12 +5,12 @@
5
5
  <h1 align="center">OpenPI</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Small harness. Deep extensions. Clean context.</strong>
8
+ <strong>Pi at the core. Power on demand.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://pi.dev">Pi</a> 加一层可靠运行时:后台执行、隔离 Subagent、可恢复 Workflow、持续任务与可观测终端。<br />
13
- 不替换 Pi,不替你选模型,也不把另一套 Agent 平台塞进来。
12
+ 默认像 <a href="https://pi.dev">Pi</a> 一样轻;任务需要时,一句话展开后台执行、隔离 Subagent、可恢复 Workflow 与持续任务。<br />
13
+ 不替换 Pi,不重写 Agent loop,不让高级能力长期占据每一次对话。
14
14
  </p>
15
15
 
16
16
  <p align="center">
@@ -23,6 +23,7 @@
23
23
 
24
24
  <p align="center">
25
25
  <a href="#30-秒开始"><strong>30 秒开始</strong></a> ·
26
+ <a href="#默认轻按需强">设计</a> ·
26
27
  <a href="#openpi-解决什么">解决什么</a> ·
27
28
  <a href="#能力地图">能力地图</a> ·
28
29
  <a href="#运行模型">运行模型</a> ·
@@ -38,6 +39,27 @@
38
39
 
39
40
  ---
40
41
 
42
+ ## 默认轻,按需强
43
+
44
+ **OpenPI 最强的地方,不是工具多,而是复杂度只在值得的时候出现。**
45
+
46
+ 普通编码任务继续走 Pi 原生路径:`read`、`bash`、`edit`、`write`,完整历史、Session compaction、工具输出边界、显式 Bash timeout 与 Provider loop。OpenPI 不额外投影历史,不改写测试超时,也不向模型塞恢复提示;只保留独立的工作区删除保护。
47
+
48
+ 任务一旦需要长期进程、并行调研、隔离实现、多阶段协作或跨回合推进,高级能力仍然完整存在。用户直接提出需求,OpenPI 就在当轮加载对应能力;没用到的能力不会常驻模型工具面。
49
+
50
+ > **轻路径不缴复杂度税,重任务不缺工程能力。** 这不是一套替代 Pi 的 Agent Runtime,而是一组遵守 Pi 生命周期、Session、Provider、模型与 Trust 边界的 Pi-native 深扩展。
51
+
52
+ | 使用场景 | 模型看到什么 | OpenPI 的行为 |
53
+ | ---------------------------- | ------------------------------------------------- | ------------------------------------------------- |
54
+ | 普通编码任务 | Pi 原生 `read` / `bash` / `edit` / `write` | 默认不常驻任何 OpenPI 模型工具 |
55
+ | 用户明确要求委派或高级能力 | 仅与意图匹配的能力组 | 在当轮开始前直接加载,不要求用户记住工具名 |
56
+ | 用户主动开启 `adaptive` | 一个小型 `openpi_load_tools` 网关 | 主模型判断确有收益时,可自主加载一个能力组 |
57
+ | 后台任务或子 Agent 已经运行 | 对应的状态、等待、继续与停止工具 | 管理面随真实资源出现,资源结束后按生命周期收敛 |
58
+
59
+ 这套设计保住了两件通常很难同时拥有的东西:Pi 的清爽基本面,以及完整工程工作台的能力上限。
60
+
61
+ ---
62
+
41
63
  ## 30 秒开始
42
64
 
43
65
  ```bash
@@ -310,7 +332,7 @@ cwd model thinking context cache cost throughput git PR
310
332
  - 支持 `powerline`、`powerline-mono`、`compact`,也支持自定义多行布局;
311
333
  - 终端变窄时按优先级隐藏次要指标,不机械截断尾部;
312
334
  - Subagent 与 Workflow 活动时自动出现,空闲时不占空间;
313
- - Bash、Write/Edit 与 Subagent 结果可独立选择 `full` 或 `compact`;
335
+ - Bash、Write/Edit 与 Subagent 结果可独立选择 `full` 或 `compact`;Subagent 的 compact 模式只显示状态摘要,不暴露原始子 Agent 正文;
314
336
  - 折叠内容用 Pi 的 `app.tools.expand` 快捷键临时展开,默认 `Ctrl+O`;
315
337
  - Git 状态本地刷新;只有显式运行 `/pr` 才查询 GitHub PR。
316
338
 
@@ -375,7 +397,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
375
397
  | Next-action Suggestion | 关闭;启用时显式选择 Registry 模型与 reasoning |
376
398
  | Workflow 并发 / 总调用 | 8 / 128;硬上限 64 / 1024 |
377
399
  | 大型 Header | 关闭 |
378
- | Dashboard Footer | 开启;单行 `powerline` |
400
+ | Dashboard Footer | 开启;单行 `plain` |
379
401
  | Subagent / Bash / Write/Edit | `full` / `compact` / `compact` |
380
402
  | Post-edit 命令 | 关闭;单条命令最多 500 字符 |
381
403
  | 内置角色模型 | 全部继承父模型 |
@@ -469,7 +491,7 @@ Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 Op
469
491
  <details>
470
492
  <summary><strong>Subagent 会阻塞主 Agent 吗?</strong></summary>
471
493
 
472
- `subagent_spawn` 立即返回,结束后自动回传。只有显式调用 `subagent_wait` 才会等待;它只适合下一步确实依赖结果的场景。
494
+ `subagent_spawn` 立即返回,结束后自动回传并重新唤醒主 Agent。交互会话没有其他工作时,主 Agent 应结束当前轮、让用户继续交互;“下一步依赖结果”本身不是阻塞理由。只有用户明确要求当前回复等完,或非交互自动化必须在同一次调用中返回完整结果时,才应调用 `subagent_wait`。
473
495
 
474
496
  </details>
475
497
 
package/SETUP.md CHANGED
@@ -54,7 +54,7 @@ Use the single canonical package-owned command. `/my-pi-setup` remains a compati
54
54
  /openpi-setup 清除 explorer 的模型,让它继承父模型
55
55
  ```
56
56
 
57
- Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear 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 into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
57
+ Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear 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 into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; compact mode shows only bounded status rows and keeps raw child reports behind `app.tools.expand` (`Ctrl+O` by default). Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand`. An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
58
58
 
59
59
  ## Session Goal and Tasks
60
60
 
@@ -17,7 +17,10 @@ import {
17
17
  resetOpenPiToolSurface,
18
18
  } from "../shared/tool-surface.ts";
19
19
 
20
- const CapabilitySchema = Type.Enum(OPENPI_CAPABILITY_NAMES);
20
+ const CapabilitySchema = Type.Unsafe<OpenPiCapability>({
21
+ type: "string",
22
+ enum: OPENPI_CAPABILITY_NAMES,
23
+ });
21
24
 
22
25
  const OpenPiLoadToolsParameters = Type.Object({
23
26
  groups: Type.Optional(
@@ -25,24 +25,22 @@
25
25
  */
26
26
 
27
27
  /**
28
- * Any of these means the text is more than one plain command — a pipeline, a
29
- * sequence, a redirect, a substitution, a glob, or a background job. Rather
30
- * than parse shell (where every parser bug is a bypass), refuse outright.
31
- *
32
- * `\` is here because a line continuation splices in the next line; `$` covers
33
- * both `$(...)` and a `$VAR` that expands into arguments never inspected here.
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 SHELL_METACHARACTERS = /[;&|<>$`\\!*?{}()[\]\n\r#]/;
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, but only where a shell would actually expand it: at the
39
- * start of a word. `HEAD~3` is ordinary revision syntax and must survive,
40
- * while `~/notes` and `~user/x` resolve to a path this module never sees.
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 (!allowed.has(flagName(word))) {
232
- return refuse(
233
- `plan mode does not recognize "${word}" as a read-only ${program} option, so it will not run this command`,
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
- if (SHELL_METACHARACTERS.test(text)) {
253
- return refuse(
254
- "plan mode only runs a single plain command — no pipes, redirects, substitutions, globs, or chained commands",
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
- * Nothing else is admitted. `ls`, `cat`, `head`, `tail` and `wc` were on an
305
- * earlier version of this list and are gone: plan mode already grants the
306
- * `ls`, `read`, `grep` and `fd`/`rg` TOOLS, so those shell forms added no
307
- * capability while each contributed its own flag grammar to get wrong
308
- * (`file --compile` and `tree -ao` both write files).
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 runs only read-only git and gh investigation commands while planning, not "${program}" use the read, ls, grep or fd tools for files`,
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
  }
@@ -142,8 +142,12 @@ export const PLAN_READY_ACTIONS = {
142
142
  continue: "Continue planning",
143
143
  current: "Implement in this session",
144
144
  fresh: "Start a fresh session",
145
+ off: "Turn plan mode off",
145
146
  } as const;
146
147
 
148
+ /** Menu label for the same effect as `/plan done`. */
149
+ const FINALIZE_NOW = "Finalize now";
150
+
147
151
  export function buildPlanImplementationPrompt(plan: string) {
148
152
  return [
149
153
  "Implement the approved plan below. Re-check the repository state before editing, follow the project instructions, and verify the finished change.",
@@ -257,11 +261,7 @@ export default function planMode(pi: ExtensionAPI) {
257
261
  if (!ctx.hasUI) return;
258
262
  ctx.ui.setStatus(
259
263
  "plan-mode",
260
- readyPlan
261
- ? "plan mode · ready"
262
- : planning
263
- ? "plan mode · read-only"
264
- : undefined,
264
+ readyPlan ? "plan ready" : planning ? "plan mode" : undefined,
265
265
  );
266
266
  };
267
267
 
@@ -371,9 +371,25 @@ export default function planMode(pi: ExtensionAPI) {
371
371
  implementHere(ctx);
372
372
  } else if (choice === PLAN_READY_ACTIONS.fresh) {
373
373
  await implementFresh(ctx);
374
+ } else if (choice === PLAN_READY_ACTIONS.off) {
375
+ clearPlan(ctx);
376
+ ctx.ui.notify("Plan mode is off.", "info");
374
377
  }
375
378
  };
376
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
+
377
393
  pi.registerTool({
378
394
  name: "plan_ready",
379
395
  label: "Plan Ready",
@@ -462,16 +478,7 @@ export default function planMode(pi: ExtensionAPI) {
462
478
  ctx.ui.notify("Plan mode is not active.", "warning");
463
479
  return;
464
480
  }
465
- pi.sendMessage(
466
- {
467
- customType: "plan-finalize-requested",
468
- content:
469
- "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.",
470
- display: true,
471
- details: {},
472
- },
473
- { deliverAs: "followUp", triggerTurn: true },
474
- );
481
+ requestPlanFinalization();
475
482
  return;
476
483
  }
477
484
 
@@ -481,10 +488,28 @@ export default function planMode(pi: ExtensionAPI) {
481
488
  }
482
489
 
483
490
  if (planning) {
484
- ctx.ui.notify(
485
- "Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
486
- "info",
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],
487
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
+ }
488
513
  return;
489
514
  }
490
515
 
@@ -510,7 +535,7 @@ export default function planMode(pi: ExtensionAPI) {
510
535
  return {
511
536
  block: true as const,
512
537
  reason:
513
- "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.",
514
539
  };
515
540
  }
516
541
  const batchDecision =
@@ -132,10 +132,10 @@ export function buildInteractiveSetupPrompt(options: {
132
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.",
133
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.",
134
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.",
135
- "- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (default one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text). 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.",
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.",
136
136
  "- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
137
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.",
138
- "- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full (always expanded) or compact (Claude Code-style folded preview with a hidden-line count). Compact output can still be temporarily expanded with the configured app.tools.expand key (Ctrl+O by default). Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
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.",
139
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.",
140
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.",
141
141
  "",
@@ -395,7 +395,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
395
395
  subagent_result_display: Type.Optional(
396
396
  StringEnum(DETAIL_DISPLAYS, {
397
397
  description:
398
- "How completed Subagent results render by default: full preserves complete output; compact shows a bounded preview that can be expanded with app.tools.expand. Omit to preserve the current value.",
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.",
399
399
  }),
400
400
  ),
401
401
  bash_tool_display: Type.Optional(