@astralyn/pi 0.84.1 → 0.84.3

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.
Files changed (163) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli/args.js +1 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +2 -3
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/startup-ui.d.ts.map +1 -1
  8. package/dist/cli/startup-ui.js +3 -5
  9. package/dist/cli/startup-ui.js.map +1 -1
  10. package/dist/config.d.ts +5 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +1 -25
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +29 -408
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/compaction-controller.d.ts +89 -0
  19. package/dist/core/compaction-controller.d.ts.map +1 -0
  20. package/dist/core/compaction-controller.js +428 -0
  21. package/dist/core/compaction-controller.js.map +1 -0
  22. package/dist/core/extensions/index.d.ts +1 -1
  23. package/dist/core/extensions/index.d.ts.map +1 -1
  24. package/dist/core/extensions/index.js.map +1 -1
  25. package/dist/core/extensions/loader.js +3 -3
  26. package/dist/core/extensions/loader.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +1 -15
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/provider-attribution.js +2 -2
  31. package/dist/core/provider-attribution.js.map +1 -1
  32. package/dist/core/settings-manager.js +1 -1
  33. package/dist/core/settings-manager.js.map +1 -1
  34. package/dist/core/telemetry.d.ts +1 -1
  35. package/dist/core/telemetry.d.ts.map +1 -1
  36. package/dist/core/telemetry.js +1 -1
  37. package/dist/core/telemetry.js.map +1 -1
  38. package/dist/extensions/deepwiki/index.d.ts.map +1 -1
  39. package/dist/extensions/deepwiki/index.js +23 -1
  40. package/dist/extensions/deepwiki/index.js.map +1 -1
  41. package/dist/extensions/deepwiki/render.d.ts +1 -1
  42. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  43. package/dist/extensions/deepwiki/render.js +4 -2
  44. package/dist/extensions/deepwiki/render.js.map +1 -1
  45. package/dist/extensions/index.d.ts.map +1 -1
  46. package/dist/extensions/index.js +0 -4
  47. package/dist/extensions/index.js.map +1 -1
  48. package/dist/extensions/question/index.d.ts.map +1 -1
  49. package/dist/extensions/question/index.js +0 -1
  50. package/dist/extensions/question/index.js.map +1 -1
  51. package/dist/extensions/subagent/index.d.ts.map +1 -1
  52. package/dist/extensions/subagent/index.js +20 -2
  53. package/dist/extensions/subagent/index.js.map +1 -1
  54. package/dist/extensions/todo/schema.d.ts +1 -1
  55. package/dist/extensions/todo/state.js +5 -5
  56. package/dist/extensions/todo/state.js.map +1 -1
  57. package/dist/modes/interactive/components/extension-selector.d.ts +0 -2
  58. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/extension-selector.js +1 -1
  60. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/settings-selector.js +1 -1
  62. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  63. package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
  64. package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
  65. package/dist/modes/interactive/components/tool-chrome.js +81 -0
  66. package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
  67. package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
  68. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/tool-execution.js +11 -108
  70. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  72. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.js +35 -52
  74. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  75. package/docs/bundled/README.md +0 -2
  76. package/docs/bundled/extensions/subagent.md +1 -1
  77. package/docs/bundled/tool-presentation.md +2 -2
  78. package/docs/docs.json +0 -8
  79. package/docs/extensions.md +6 -23
  80. package/docs/settings.md +1 -1
  81. package/docs/usage.md +1 -2
  82. package/examples/extensions/README.md +2 -0
  83. package/examples/extensions/plan-mode/README.md +66 -0
  84. package/examples/extensions/plan-mode/index.ts +390 -0
  85. package/examples/extensions/plan-mode/utils.ts +168 -0
  86. package/examples/extensions/subagent/README.md +175 -0
  87. package/examples/extensions/subagent/agents/planner.md +37 -0
  88. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  89. package/examples/extensions/subagent/agents/scout.md +50 -0
  90. package/examples/extensions/subagent/agents/worker.md +24 -0
  91. package/examples/extensions/subagent/agents.ts +126 -0
  92. package/examples/extensions/subagent/index.ts +1009 -0
  93. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  94. package/examples/extensions/subagent/prompts/implement.md +10 -0
  95. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +1 -1
  98. package/dist/extensions/biu/index.d.ts +0 -19
  99. package/dist/extensions/biu/index.d.ts.map +0 -1
  100. package/dist/extensions/biu/index.js +0 -350
  101. package/dist/extensions/biu/index.js.map +0 -1
  102. package/dist/extensions/biu/prompts.d.ts +0 -26
  103. package/dist/extensions/biu/prompts.d.ts.map +0 -1
  104. package/dist/extensions/biu/prompts.js +0 -260
  105. package/dist/extensions/biu/prompts.js.map +0 -1
  106. package/dist/extensions/biu/workspace.d.ts +0 -122
  107. package/dist/extensions/biu/workspace.d.ts.map +0 -1
  108. package/dist/extensions/biu/workspace.js +0 -404
  109. package/dist/extensions/biu/workspace.js.map +0 -1
  110. package/dist/extensions/rewind/anchor.d.ts +0 -29
  111. package/dist/extensions/rewind/anchor.d.ts.map +0 -1
  112. package/dist/extensions/rewind/anchor.js +0 -35
  113. package/dist/extensions/rewind/anchor.js.map +0 -1
  114. package/dist/extensions/rewind/config.d.ts +0 -16
  115. package/dist/extensions/rewind/config.d.ts.map +0 -1
  116. package/dist/extensions/rewind/config.js +0 -69
  117. package/dist/extensions/rewind/config.js.map +0 -1
  118. package/dist/extensions/rewind/engine.d.ts +0 -93
  119. package/dist/extensions/rewind/engine.d.ts.map +0 -1
  120. package/dist/extensions/rewind/engine.js +0 -778
  121. package/dist/extensions/rewind/engine.js.map +0 -1
  122. package/dist/extensions/rewind/gc.d.ts +0 -35
  123. package/dist/extensions/rewind/gc.d.ts.map +0 -1
  124. package/dist/extensions/rewind/gc.js +0 -169
  125. package/dist/extensions/rewind/gc.js.map +0 -1
  126. package/dist/extensions/rewind/index.d.ts +0 -3
  127. package/dist/extensions/rewind/index.d.ts.map +0 -1
  128. package/dist/extensions/rewind/index.js +0 -287
  129. package/dist/extensions/rewind/index.js.map +0 -1
  130. package/dist/extensions/rewind/menu.d.ts +0 -13
  131. package/dist/extensions/rewind/menu.d.ts.map +0 -1
  132. package/dist/extensions/rewind/menu.js +0 -123
  133. package/dist/extensions/rewind/menu.js.map +0 -1
  134. package/dist/extensions/rewind/paths.d.ts +0 -4
  135. package/dist/extensions/rewind/paths.d.ts.map +0 -1
  136. package/dist/extensions/rewind/paths.js +0 -15
  137. package/dist/extensions/rewind/paths.js.map +0 -1
  138. package/dist/extensions/rewind/restore.d.ts +0 -54
  139. package/dist/extensions/rewind/restore.d.ts.map +0 -1
  140. package/dist/extensions/rewind/restore.js +0 -94
  141. package/dist/extensions/rewind/restore.js.map +0 -1
  142. package/dist/extensions/rewind/snapshot.d.ts +0 -45
  143. package/dist/extensions/rewind/snapshot.d.ts.map +0 -1
  144. package/dist/extensions/rewind/snapshot.js +0 -21
  145. package/dist/extensions/rewind/snapshot.js.map +0 -1
  146. package/dist/extensions/rewind/storage.d.ts +0 -12
  147. package/dist/extensions/rewind/storage.d.ts.map +0 -1
  148. package/dist/extensions/rewind/storage.js +0 -33
  149. package/dist/extensions/rewind/storage.js.map +0 -1
  150. package/dist/extensions/rewind/text.d.ts +0 -7
  151. package/dist/extensions/rewind/text.d.ts.map +0 -1
  152. package/dist/extensions/rewind/text.js +0 -19
  153. package/dist/extensions/rewind/text.js.map +0 -1
  154. package/dist/extensions/rewind/tool-path.d.ts +0 -3
  155. package/dist/extensions/rewind/tool-path.d.ts.map +0 -1
  156. package/dist/extensions/rewind/tool-path.js +0 -9
  157. package/dist/extensions/rewind/tool-path.js.map +0 -1
  158. package/dist/utils/coalescing-terminal.d.ts +0 -132
  159. package/dist/utils/coalescing-terminal.d.ts.map +0 -1
  160. package/dist/utils/coalescing-terminal.js +0 -292
  161. package/dist/utils/coalescing-terminal.js.map +0 -1
  162. package/docs/bundled/extensions/biu.md +0 -63
  163. package/docs/bundled/extensions/rewind.md +0 -86
@@ -9,10 +9,8 @@ These hidden built-ins use the same public Extension API available to external e
9
9
  | Feature | Tool or command | Purpose |
10
10
  |---|---|---|
11
11
  | [`llama.cpp`](../llama-cpp.md) | `/llama` | Manage models served by the local llama.cpp router |
12
- | [Biu](extensions/biu.md) | `/biu`, `biu://` paths | Carry a project cycle through plan, optional decomposition, execution, and archive |
13
12
  | [DeepWiki](extensions/deepwiki.md) | `deepwiki` | Query indexed public GitHub repository documentation |
14
13
  | [Question](extensions/question.md) | `question` | Ask structured questions through native interactive UI |
15
- | [Rewind](extensions/rewind.md) | `/rewind`, `/tree` lifecycle | Create and restore project snapshots around session operations |
16
14
  | [Router](extensions/router.md) | `/router` | Configure and probe Codex-style routing endpoints |
17
15
  | [Statusline](extensions/statusline.md) | Footer status | Show concise extension-managed activity state |
18
16
  | [Subagent](extensions/subagent.md) | `subagent`, `/agents` | Delegate bounded work to isolated child Pi sessions |
@@ -85,7 +85,7 @@ They apply only to future subagent runs and never change the parent session's `/
85
85
  - Parent abort, `/reload`, `/new`, `/resume`, `/fork`, and session shutdown abort active child sessions. Queued workers do not survive the call.
86
86
  - Parallel worker failures do not cancel other independent workers.
87
87
  - Transient provider errors auto-retry: the worker session retries retryable stream errors with backoff (visible as a live `Retrying (n/m) in Xs…` countdown), and a failure that produced no work (for example a preflight auth throw) is rerun at the task level up to two more times. Provider and task retries share one absolute-deadline state; at zero it reads `Retrying now…`, and parent abort/session shutdown interrupts backoff immediately. Runs with partial work behind them are never rerun.
88
- - Progress is rendered in Pi's native tool transcript. While a call is running, the native shell rebuilds the Subagent renderer once per second, independently of worker output and the main Working spinner. Silent single and parallel runs therefore keep advancing their own elapsed clocks in collapsed and expanded views, and queued retries keep counting down; the generic shell `Running…` row stays hidden to avoid duplicate progress. Once settled, the final duration is fixed and refresh stops.
88
+ - Progress is rendered in Pi's native tool transcript. While a call is running, the Subagent renderer re-schedules its own repaint once per second, independently of worker output and the main Working spinner. Silent single and parallel runs therefore keep advancing their own elapsed clocks in collapsed and expanded views, and queued retries keep counting down. Once settled, the final duration is fixed and refresh stops.
89
89
  - The collapsed view stays lean in both phases: while running, one bounded intent line (for example `Verifying changes`) carries tool uses, the `ctx:` context watermark, cost, and elapsed time above the current tool marked with `›` and the live output tail; once settled, the sentence-aware result excerpt is followed by a single metrics line (`tok · $ · duration`, plus tool uses and `ctx:` for single runs) and the configured expand hint. Live lines never quote token totals — mid-run totals are cache-inflated. The call-level status dot belongs to the tool shell; run and activity rows only use `›`, `○`, `✓`, `×`, and `■`, and parallel run rows carry stable dim ordinals that match the expanded section numbers. `Ctrl+O` expands each run into a report cover sheet: a status line, a metrics line, a fixed two-line `Prompt` preview (`… continues, N more lines`, honest about the 1KB bound), an `Activity · last n of N` digest whose successful rows are quiet aligned one-liners, a `Working` tail while streaming, and the full `Report` (or `Error`, with `Report · partial` for salvaged output) rendered as Markdown. Expanded batches start with a numbered contents list whose numbers match the `── N · ✓ Agent` section headers.
90
90
  - Failed and aborted batch runs place their bounded reason on a separate error-colored line below the run headline instead of cutting it into the status row. Reasons are capped at 200 characters and keep their hanging indent when wrapped; successful settled excerpts prefer complete leading sentences within their 96-character budget. Rounded durations switch to minute form at 60 seconds without producing `1m 60s`.
91
91
  - Full child transcripts are not stored separately. Bounded run details remain in the parent tool result, so completed calls restore naturally in the parent session tree.
@@ -12,9 +12,9 @@ The `@astralyn/pi` package uses Pi's native tool transcript presentation rather
12
12
  │ final result line
13
13
  ```
14
14
 
15
- The dim result rail continues through every visual line; an empty output line renders as a bare `│`. Calls keep their status in the leading dot: warning while pending or running, green after success, and red after failure. For default-shell tools that are still running after two seconds, the shell adds a live result row such as `Running… (2.1s)` and removes it when the call settles. Tools with purpose-built progress UI can opt out through `rendersOwnProgress`; tools using `renderShell: "self"` continue to own their entire presentation.
15
+ The dim result rail continues through every visual line; an empty output line renders as a bare `│`. Calls keep their status in the leading dot: warning while pending or running, green after success, and red after failure. For bash commands that are still running after two seconds, the shell adds a live result row such as `Running… (2.1s)` and removes it when the call settles. Other tools with live progress render it themselves; tools using `renderShell: "self"` continue to own their entire presentation.
16
16
 
17
- Time-driven renderers use `renderRefreshIntervalMs` separately from `rendersOwnProgress`. The former periodically rebuilds call/result components only during the partial lifecycle; the latter only hides the generic `Running…` row. The native shell shares the shortest needed interval, constrains explicit intervals to 250ms–60s, and stops refreshing on final result, abort, disposal, session replacement, chat rebuild, or shutdown. Renderers compute elapsed/countdown values from absolute timestamps or deadlines, so a delayed repaint never changes the underlying wall-clock meaning.
17
+ Time-driven renderers schedule their own repaints: while a result is partial they arm a timer in renderer state and call the render context's `invalidate()`, clearing the timer on the first settled render. Renderers compute elapsed/countdown values from absolute timestamps or deadlines, so a delayed repaint never changes the underlying wall-clock meaning.
18
18
 
19
19
  ## Implementation boundary
20
20
 
package/docs/docs.json CHANGED
@@ -56,10 +56,6 @@
56
56
  "title": "Overview",
57
57
  "path": "bundled/README.md"
58
58
  },
59
- {
60
- "title": "Biu",
61
- "path": "bundled/extensions/biu.md"
62
- },
63
59
  {
64
60
  "title": "DeepWiki",
65
61
  "path": "bundled/extensions/deepwiki.md"
@@ -68,10 +64,6 @@
68
64
  "title": "Question",
69
65
  "path": "bundled/extensions/question.md"
70
66
  },
71
- {
72
- "title": "Rewind",
73
- "path": "bundled/extensions/rewind.md"
74
- },
75
67
  {
76
68
  "title": "Router",
77
69
  "path": "bundled/extensions/router.md"
@@ -1996,10 +1996,6 @@ pi.registerTool({
1996
1996
  },
1997
1997
 
1998
1998
  // Optional: Custom rendering
1999
- // rendersOwnProgress suppresses only the shell's generic Running… row.
2000
- rendersOwnProgress: true,
2001
- // While the result is partial, rebuild renderCall/renderResult every second.
2002
- renderRefreshIntervalMs: 1000,
2003
1999
  renderCall(args, theme, context) { ... },
2004
2000
  renderResult(result, options, theme, context) { ... },
2005
2001
  });
@@ -2253,12 +2249,7 @@ By default, tool output is wrapped in Pi's native call/result shell, which owns
2253
2249
 
2254
2250
  Set `renderShell: "self"` when the tool should render its own shell instead of using the native shell. This is useful for tools that need complete control over framing or background behavior, for example large previews that must stay visually stable after the tool settles.
2255
2251
 
2256
- Two independent options control live presentation:
2257
-
2258
- - `rendersOwnProgress: true` suppresses only the native shell's generic `Running… (Ns)` row. It does not schedule renderer updates.
2259
- - `renderRefreshIntervalMs` periodically rebuilds `renderCall` and `renderResult` while the tool result is partial. It does not suppress generic progress.
2260
-
2261
- Use both when a tool renders its own elapsed time, retry countdown, or similar progress UI. Refresh intervals are rounded and constrained to 250–60,000ms. The shell may rebuild sooner when another native concern needs it, so the configured interval is the maximum refresh gap rather than an exclusive cadence. Refresh stops when the result settles or the row is disposed.
2252
+ For live progress UI (elapsed time, retry countdowns), schedule repaints from the renderer itself: store a timer handle in `context.state`, arm a `setTimeout` that calls `context.invalidate()` while `options.isPartial` is true, and clear it on the first settled render. `context.invalidate()` is a no-op after the row is disposed, so a trailing timeout is harmless.
2262
2253
 
2263
2254
  Derive displayed time from an absolute timestamp or deadline (`Date.now()`), not by decrementing a counter on every refresh. A refresh is only a repaint opportunity; event-loop stalls and machine sleep must not extend a wall-clock timeout.
2264
2255
 
@@ -2368,8 +2359,7 @@ Custom editors and `ctx.ui.custom()` components receive `keybindings: Keybinding
2368
2359
  - Use `Text` with padding `(0, 0)`. The native shell handles padding.
2369
2360
  - Use `\n` for multi-line content.
2370
2361
  - Handle `isPartial` for streaming progress.
2371
- - Use `renderRefreshIntervalMs` only for values that must change without a new partial result, and compute those values from absolute time.
2372
- - Set `rendersOwnProgress` separately when the renderer replaces the native `Running…` row.
2362
+ - For time-driven renderers, self-schedule repaints with `context.state` timers and `context.invalidate()`, and compute displayed values from absolute time.
2373
2363
  - Support `expanded` for detail on demand.
2374
2364
  - Keep default view compact.
2375
2365
  - Read `context.args` in `renderResult` instead of copying args into `context.state`.
@@ -2542,17 +2532,10 @@ const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
2542
2532
 
2543
2533
  // Options can carry a muted description line, so the action and its
2544
2534
  // trade-off can be scanned separately. select() still resolves to the label.
2545
- const action = await ctx.ui.select(
2546
- "Approve?",
2547
- [
2548
- { label: "Start executing", description: "keep full context" },
2549
- { label: "Compact, then execute", description: "best for long tasks" },
2550
- ],
2551
- {
2552
- subtitle: "Context now 29% full", // muted line under the title
2553
- cancelHint: "go back", // Describe what Esc actually does
2554
- },
2555
- );
2535
+ const action = await ctx.ui.select("Approve?", [
2536
+ { label: "Start executing", description: "keep full context" },
2537
+ { label: "Compact, then execute", description: "best for long tasks" },
2538
+ ]);
2556
2539
 
2557
2540
  // Confirm dialog
2558
2541
  const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
package/docs/settings.md CHANGED
@@ -65,7 +65,7 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
65
65
  | `outputPad` | number | `1` | Horizontal padding for user messages, assistant messages, and thinking (0 or 1) |
66
66
  | `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
67
67
  | `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
68
- | `tuiMode` | string | `"regular"` | Interactive TUI mode: `"regular"` or experimental `"fullscreen"`. Changes from `/settings` apply immediately; `--tui-mode` overrides this setting at startup |
68
+ | `tuiMode` | string | `"fullscreen"` | Interactive TUI mode: `"fullscreen"` or `"regular"`. Changes from `/settings` apply immediately; `--tui-mode` overrides this setting at startup |
69
69
  | `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"` shows it temporarily while scrolling, `"always"` reserves the rightmost column and keeps it visible, and `"hidden"` hides it. Has no effect in regular TUI mode |
70
70
 
71
71
  For VS Code, include `--wait` so pi resumes after the editor exits:
package/docs/usage.md CHANGED
@@ -37,7 +37,6 @@ Type `/` in the editor to open command completion. Extensions can register custo
37
37
  | Command | Description |
38
38
  |---------|-------------|
39
39
  | `/login`, `/logout` | Manage OAuth or API-key credentials |
40
- | [`/biu`](bundled/extensions/biu.md) | Toggle the TUI-only Biu workflow; inside the mode, open its menu |
41
40
  | [`/llama`](llama-cpp.md) | Download, load, and unload llama.cpp router models |
42
41
  | `/model` | Switch models |
43
42
  | `/scoped-models` | Enable/disable models for Ctrl+P cycling |
@@ -234,7 +233,7 @@ pi --no-extensions -e ./my-extension.ts
234
233
  |--------|-------------|
235
234
  | `--system-prompt <text>` | Replace default prompt; context files and skills are still appended |
236
235
  | `--append-system-prompt <text>` | Append to system prompt |
237
- | `--tui-mode <mode>` | TUI mode: `regular` (default) or experimental `fullscreen` |
236
+ | `--tui-mode <mode>` | TUI mode: `fullscreen` (default) or `regular` |
238
237
  | `--verbose` | Force verbose startup |
239
238
  | `-a`, `--approve` | Trust project-local files for this run |
240
239
  | `-na`, `--no-approve` | Ignore project-local files for this run |
@@ -43,12 +43,14 @@ cp permission-gate.ts ~/.pi/agent/extensions/
43
43
  | `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) |
44
44
  | `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
45
45
  | `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
46
+ | `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
46
47
 
47
48
  ### Commands & UI
48
49
 
49
50
  | Extension | Description |
50
51
  |-----------|-------------|
51
52
  | `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
53
+ | `plan-mode/` | Claude Code-style plan mode for read-only exploration with `/plan` command and step tracking |
52
54
  | `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
53
55
  | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
54
56
  | `commands.ts` | Lists available slash commands, optionally filtered by source |
@@ -0,0 +1,66 @@
1
+ # Plan Mode Extension
2
+
3
+ Read-only exploration mode for safe code analysis.
4
+
5
+ ## Features
6
+
7
+ - **Built-in write tools disabled**: Disables edit/write while preserving other active tools
8
+ - **Bash allowlist**: Only read-only bash commands are allowed
9
+ - **Plan extraction**: Extracts numbered steps from `Plan:` sections
10
+ - **Progress tracking**: Widget shows completion status during execution
11
+ - **[DONE:n] markers**: Explicit step completion tracking
12
+ - **Session persistence**: State survives session resume
13
+
14
+ ## Commands
15
+
16
+ - `/plan` - Toggle plan mode
17
+ - `/todos` - Show current plan progress
18
+ - `Ctrl+Alt+P` - Toggle plan mode (shortcut)
19
+
20
+ ## Usage
21
+
22
+ 1. Enable plan mode with `/plan` or `--plan` flag
23
+ 2. Ask the agent to analyze code and create a plan
24
+ 3. The agent should output a numbered plan under a `Plan:` header:
25
+
26
+ ```
27
+ Plan:
28
+ 1. First step description
29
+ 2. Second step description
30
+ 3. Third step description
31
+ ```
32
+
33
+ 4. Choose "Execute the plan" when prompted
34
+ 5. During execution, the agent marks steps complete with `[DONE:n]` tags
35
+ 6. Progress widget shows completion status
36
+
37
+ ## How It Works
38
+
39
+ ### Plan Mode (Read-Only)
40
+ - Built-in edit/write tools disabled
41
+ - Other active tools remain available
42
+ - Bash commands filtered through allowlist
43
+ - Agent creates a plan without making changes
44
+
45
+ ### Execution Mode
46
+ - Full tool access restored
47
+ - Agent executes steps in order
48
+ - `[DONE:n]` markers track completion
49
+ - Widget shows progress
50
+
51
+ ### Command Allowlist
52
+
53
+ Safe commands (allowed):
54
+ - File inspection: `cat`, `head`, `tail`, `less`, `more`
55
+ - Search: `grep`, `find`, `rg`, `fd`
56
+ - Directory: `ls`, `pwd`, `tree`
57
+ - Git read: `git status`, `git log`, `git diff`, `git branch`
58
+ - Package info: `npm list`, `npm outdated`, `yarn info`
59
+ - System info: `uname`, `whoami`, `date`, `uptime`
60
+
61
+ Blocked commands:
62
+ - File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
63
+ - Git write: `git add`, `git commit`, `git push`
64
+ - Package install: `npm install`, `yarn add`, `pip install`
65
+ - System: `sudo`, `kill`, `reboot`
66
+ - Editors: `vim`, `nano`, `code`
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Plan Mode Extension
3
+ *
4
+ * Read-only exploration mode for safe code analysis.
5
+ * When enabled, built-in write tools are disabled.
6
+ *
7
+ * Features:
8
+ * - /plan command or Ctrl+Alt+P to toggle
9
+ * - Bash restricted to allowlisted read-only commands
10
+ * - Extracts numbered plan steps from "Plan:" sections
11
+ * - [DONE:n] markers to complete steps during execution
12
+ * - Progress tracking widget during execution
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@astralyn/pi";
16
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
17
+ import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
18
+ import { Key } from "@earendil-works/pi-tui";
19
+ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts";
20
+
21
+ // Tools
22
+ const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
23
+ const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
24
+ const PLAN_MODE_DISABLED_TOOLS = new Set<string>(["edit", "write"]);
25
+ const PLAN_MANAGED_TOOLS = new Set<string>([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]);
26
+
27
+ interface PlanModeState {
28
+ enabled: boolean;
29
+ todos?: TodoItem[];
30
+ executing?: boolean;
31
+ toolsBeforePlanMode?: string[];
32
+ }
33
+
34
+ // Type guard for assistant messages
35
+ function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
36
+ return m.role === "assistant" && Array.isArray(m.content);
37
+ }
38
+
39
+ // Extract text content from an assistant message
40
+ function getTextContent(message: AssistantMessage): string {
41
+ return message.content
42
+ .filter((block): block is TextContent => block.type === "text")
43
+ .map((block) => block.text)
44
+ .join("\n");
45
+ }
46
+
47
+ export default function planModeExtension(pi: ExtensionAPI): void {
48
+ let planModeEnabled = false;
49
+ let executionMode = false;
50
+ let todoItems: TodoItem[] = [];
51
+ let toolsBeforePlanMode: string[] | undefined;
52
+
53
+ pi.registerFlag("plan", {
54
+ description: "Start in plan mode (read-only exploration)",
55
+ type: "boolean",
56
+ default: false,
57
+ });
58
+
59
+ function updateStatus(ctx: ExtensionContext): void {
60
+ // Footer status
61
+ if (executionMode && todoItems.length > 0) {
62
+ const completed = todoItems.filter((t) => t.completed).length;
63
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
64
+ } else if (planModeEnabled) {
65
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
66
+ } else {
67
+ ctx.ui.setStatus("plan-mode", undefined);
68
+ }
69
+
70
+ // Widget showing todo list
71
+ if (executionMode && todoItems.length > 0) {
72
+ const lines = todoItems.map((item) => {
73
+ if (item.completed) {
74
+ return (
75
+ ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
76
+ );
77
+ }
78
+ return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
79
+ });
80
+ ctx.ui.setWidget("plan-todos", lines);
81
+ } else {
82
+ ctx.ui.setWidget("plan-todos", undefined);
83
+ }
84
+ }
85
+
86
+ function uniqueToolNames(toolNames: string[]): string[] {
87
+ return [...new Set(toolNames)];
88
+ }
89
+
90
+ function getPlanModeTools(activeToolNames: string[]): string[] {
91
+ return uniqueToolNames([
92
+ ...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)),
93
+ ...PLAN_MODE_TOOLS,
94
+ ]);
95
+ }
96
+
97
+ function getNormalModeTools(activeToolNames: string[]): string[] {
98
+ return uniqueToolNames([
99
+ ...NORMAL_MODE_TOOLS,
100
+ ...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)),
101
+ ]);
102
+ }
103
+
104
+ function enablePlanModeTools(): void {
105
+ if (toolsBeforePlanMode === undefined) {
106
+ toolsBeforePlanMode = pi.getActiveTools();
107
+ }
108
+ pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode));
109
+ }
110
+
111
+ function restoreNormalModeTools(): void {
112
+ pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools()));
113
+ toolsBeforePlanMode = undefined;
114
+ }
115
+
116
+ function persistState(): void {
117
+ pi.appendEntry("plan-mode", {
118
+ enabled: planModeEnabled,
119
+ todos: todoItems,
120
+ executing: executionMode,
121
+ toolsBeforePlanMode,
122
+ });
123
+ }
124
+
125
+ function togglePlanMode(ctx: ExtensionContext): void {
126
+ planModeEnabled = !planModeEnabled;
127
+ executionMode = false;
128
+ todoItems = [];
129
+
130
+ if (planModeEnabled) {
131
+ enablePlanModeTools();
132
+ ctx.ui.notify("Plan mode enabled. Built-in write tools disabled.");
133
+ } else {
134
+ restoreNormalModeTools();
135
+ ctx.ui.notify("Plan mode disabled. Full access restored.");
136
+ }
137
+ updateStatus(ctx);
138
+ persistState();
139
+ }
140
+
141
+ pi.registerCommand("plan", {
142
+ description: "Toggle plan mode (read-only exploration)",
143
+ handler: async (_args, ctx) => togglePlanMode(ctx),
144
+ });
145
+
146
+ pi.registerCommand("todos", {
147
+ description: "Show current plan todo list",
148
+ handler: async (_args, ctx) => {
149
+ if (todoItems.length === 0) {
150
+ ctx.ui.notify("No todos. Create a plan first with /plan", "info");
151
+ return;
152
+ }
153
+ const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
154
+ ctx.ui.notify(`Plan Progress:\n${list}`, "info");
155
+ },
156
+ });
157
+
158
+ pi.registerShortcut(Key.ctrlAlt("p"), {
159
+ description: "Toggle plan mode",
160
+ handler: async (ctx) => togglePlanMode(ctx),
161
+ });
162
+
163
+ // Block destructive bash commands in plan mode
164
+ pi.on("tool_call", async (event) => {
165
+ if (!planModeEnabled || event.toolName !== "bash") return;
166
+
167
+ const command = event.input.command as string;
168
+ if (!isSafeCommand(command)) {
169
+ return {
170
+ block: true,
171
+ reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`,
172
+ };
173
+ }
174
+ });
175
+
176
+ // Filter out stale plan mode context when not in plan mode
177
+ pi.on("context", async (event) => {
178
+ if (planModeEnabled) return;
179
+
180
+ return {
181
+ messages: event.messages.filter((m) => {
182
+ const msg = m as AgentMessage & { customType?: string };
183
+ if (msg.customType === "plan-mode-context") return false;
184
+ if (msg.role !== "user") return true;
185
+
186
+ const content = msg.content;
187
+ if (typeof content === "string") {
188
+ return !content.includes("[PLAN MODE ACTIVE]");
189
+ }
190
+ if (Array.isArray(content)) {
191
+ return !content.some(
192
+ (c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
193
+ );
194
+ }
195
+ return true;
196
+ }),
197
+ };
198
+ });
199
+
200
+ // Inject plan/execution context before agent starts
201
+ pi.on("before_agent_start", async () => {
202
+ if (planModeEnabled) {
203
+ return {
204
+ message: {
205
+ customType: "plan-mode-context",
206
+ content: `[PLAN MODE ACTIVE]
207
+ You are in plan mode - a read-only exploration mode for safe code analysis.
208
+
209
+ Restrictions:
210
+ - Built-in edit and write tools are disabled
211
+ - Other currently active tools remain available
212
+ - Bash is restricted to an allowlist of read-only commands
213
+
214
+ Ask clarifying questions using the questionnaire tool.
215
+ Use brave-search skill via bash for web research.
216
+
217
+ Create a detailed numbered plan under a "Plan:" header:
218
+
219
+ Plan:
220
+ 1. First step description
221
+ 2. Second step description
222
+ ...
223
+
224
+ Do NOT attempt to make changes - just describe what you would do.`,
225
+ display: false,
226
+ },
227
+ };
228
+ }
229
+
230
+ if (executionMode && todoItems.length > 0) {
231
+ const remaining = todoItems.filter((t) => !t.completed);
232
+ const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
233
+ return {
234
+ message: {
235
+ customType: "plan-execution-context",
236
+ content: `[EXECUTING PLAN - Full tool access enabled]
237
+
238
+ Remaining steps:
239
+ ${todoList}
240
+
241
+ Execute each step in order.
242
+ After completing a step, include a [DONE:n] tag in your response.`,
243
+ display: false,
244
+ },
245
+ };
246
+ }
247
+ });
248
+
249
+ // Track progress after each turn
250
+ pi.on("turn_end", async (event, ctx) => {
251
+ if (!executionMode || todoItems.length === 0) return;
252
+ if (!isAssistantMessage(event.message)) return;
253
+
254
+ const text = getTextContent(event.message);
255
+ if (markCompletedSteps(text, todoItems) > 0) {
256
+ updateStatus(ctx);
257
+ }
258
+ persistState();
259
+ });
260
+
261
+ // Handle plan completion and plan mode UI
262
+ pi.on("agent_end", async (event, ctx) => {
263
+ // Check if execution is complete
264
+ if (executionMode && todoItems.length > 0) {
265
+ if (todoItems.every((t) => t.completed)) {
266
+ const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
267
+ pi.sendMessage(
268
+ { customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
269
+ { triggerTurn: false },
270
+ );
271
+ executionMode = false;
272
+ todoItems = [];
273
+ updateStatus(ctx);
274
+ persistState(); // Save cleared state so resume doesn't restore old execution mode
275
+ }
276
+ return;
277
+ }
278
+
279
+ if (!planModeEnabled || !ctx.hasUI) return;
280
+
281
+ // Extract todos from last assistant message
282
+ const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
283
+ if (lastAssistant) {
284
+ const extracted = extractTodoItems(getTextContent(lastAssistant));
285
+ if (extracted.length > 0) {
286
+ todoItems = extracted;
287
+ }
288
+ }
289
+
290
+ if (todoItems.length === 0) return;
291
+ persistState();
292
+
293
+ // Show plan steps and prompt for next action
294
+ const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
295
+ const planTodoListMessage = {
296
+ customType: "plan-todo-list",
297
+ content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
298
+ display: true,
299
+ };
300
+
301
+ const choice = await ctx.ui.select("Plan mode - what next?", [
302
+ "Execute the plan (track progress)",
303
+ "Stay in plan mode",
304
+ "Refine the plan",
305
+ ]);
306
+
307
+ if (choice?.startsWith("Execute")) {
308
+ const firstTodoItem = todoItems[0];
309
+ if (!firstTodoItem) return;
310
+
311
+ planModeEnabled = false;
312
+ executionMode = true;
313
+ restoreNormalModeTools();
314
+ updateStatus(ctx);
315
+ persistState();
316
+
317
+ const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n");
318
+ const execMessage = `Execute the plan.
319
+
320
+ Remaining steps:
321
+ ${remainingList}
322
+
323
+ Start with: ${firstTodoItem.text}
324
+ After completing a step, include a [DONE:n] tag in your response.`;
325
+ pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
326
+ pi.sendMessage(
327
+ { customType: "plan-mode-execute", content: execMessage, display: true },
328
+ { triggerTurn: true, deliverAs: "followUp" },
329
+ );
330
+ } else if (choice === "Refine the plan") {
331
+ const refinement = await ctx.ui.editor("Refine the plan:", "");
332
+ if (refinement?.trim()) {
333
+ pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
334
+ pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" });
335
+ }
336
+ }
337
+ });
338
+
339
+ // Restore state on session start/resume
340
+ pi.on("session_start", async (_event, ctx) => {
341
+ if (pi.getFlag("plan") === true) {
342
+ planModeEnabled = true;
343
+ }
344
+
345
+ const entries = ctx.sessionManager.getEntries();
346
+
347
+ // Restore persisted state
348
+ const planModeEntry = entries
349
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
350
+ .pop() as { data?: PlanModeState } | undefined;
351
+
352
+ if (planModeEntry?.data) {
353
+ planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
354
+ todoItems = planModeEntry.data.todos ?? todoItems;
355
+ executionMode = planModeEntry.data.executing ?? executionMode;
356
+ toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode;
357
+ }
358
+
359
+ // On resume: re-scan messages to rebuild completion state
360
+ // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
361
+ const isResume = planModeEntry !== undefined;
362
+ if (isResume && executionMode && todoItems.length > 0) {
363
+ // Find the index of the last plan-mode-execute entry (marks when current execution started)
364
+ let executeIndex = -1;
365
+ for (let i = entries.length - 1; i >= 0; i--) {
366
+ const entry = entries[i] as { type: string; customType?: string };
367
+ if (entry.customType === "plan-mode-execute") {
368
+ executeIndex = i;
369
+ break;
370
+ }
371
+ }
372
+
373
+ // Only scan messages after the execute marker
374
+ const messages: AssistantMessage[] = [];
375
+ for (let i = executeIndex + 1; i < entries.length; i++) {
376
+ const entry = entries[i];
377
+ if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
378
+ messages.push(entry.message as AssistantMessage);
379
+ }
380
+ }
381
+ const allText = messages.map(getTextContent).join("\n");
382
+ markCompletedSteps(allText, todoItems);
383
+ }
384
+
385
+ if (planModeEnabled) {
386
+ enablePlanModeTools();
387
+ }
388
+ updateStatus(ctx);
389
+ });
390
+ }