@mariozechner/pi-coding-agent 0.45.3 → 0.45.5

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 (52) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/extensions/loader.d.ts.map +1 -1
  7. package/dist/core/extensions/loader.js +7 -9
  8. package/dist/core/extensions/loader.js.map +1 -1
  9. package/dist/core/model-registry.d.ts +4 -0
  10. package/dist/core/model-registry.d.ts.map +1 -1
  11. package/dist/core/model-registry.js +6 -0
  12. package/dist/core/model-registry.js.map +1 -1
  13. package/dist/core/model-resolver.d.ts.map +1 -1
  14. package/dist/core/model-resolver.js +1 -0
  15. package/dist/core/model-resolver.js.map +1 -1
  16. package/dist/core/sdk.d.ts.map +1 -1
  17. package/dist/core/sdk.js +7 -5
  18. package/dist/core/sdk.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.js +3 -4
  25. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  26. package/dist/modes/interactive/theme/light.json +9 -9
  27. package/dist/utils/image-convert.d.ts.map +1 -1
  28. package/dist/utils/image-convert.js +11 -4
  29. package/dist/utils/image-convert.js.map +1 -1
  30. package/dist/utils/image-resize.d.ts +1 -1
  31. package/dist/utils/image-resize.d.ts.map +1 -1
  32. package/dist/utils/image-resize.js +47 -25
  33. package/dist/utils/image-resize.js.map +1 -1
  34. package/dist/utils/vips.d.ts +11 -0
  35. package/dist/utils/vips.d.ts.map +1 -0
  36. package/dist/utils/vips.js +35 -0
  37. package/dist/utils/vips.js.map +1 -0
  38. package/docs/extensions.md +18 -17
  39. package/docs/sdk.md +21 -48
  40. package/examples/README.md +5 -2
  41. package/examples/extensions/README.md +19 -2
  42. package/examples/extensions/plan-mode/README.md +65 -0
  43. package/examples/extensions/plan-mode/index.ts +340 -0
  44. package/examples/extensions/plan-mode/utils.ts +168 -0
  45. package/examples/extensions/question.ts +211 -13
  46. package/examples/extensions/questionnaire.ts +427 -0
  47. package/examples/extensions/summarize.ts +195 -0
  48. package/examples/extensions/with-deps/package-lock.json +2 -2
  49. package/examples/extensions/with-deps/package.json +1 -1
  50. package/examples/sdk/README.md +3 -4
  51. package/package.json +5 -5
  52. package/examples/extensions/plan-mode.ts +0 -548
package/docs/sdk.md CHANGED
@@ -735,12 +735,12 @@ import {
735
735
  discoverAuthStorage,
736
736
  discoverModels,
737
737
  discoverSkills,
738
- discoverHooks,
739
- discoverCustomTools,
738
+ discoverExtensions,
740
739
  discoverContextFiles,
741
740
  discoverPromptTemplates,
742
741
  loadSettings,
743
742
  buildSystemPrompt,
743
+ createEventBus,
744
744
  } from "@mariozechner/pi-coding-agent";
745
745
 
746
746
  // Auth and Models
@@ -754,19 +754,16 @@ const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
754
754
  // Skills
755
755
  const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
756
756
 
757
- // Hooks (async - loads TypeScript)
758
- // Pass eventBus to share pi.events across hooks/tools
757
+ // Extensions (async - loads TypeScript)
758
+ // Pass eventBus to share pi.events across extensions
759
759
  const eventBus = createEventBus();
760
- const hooks = await discoverHooks(eventBus, cwd, agentDir);
761
-
762
- // Custom tools (async - loads TypeScript)
763
- const tools = await discoverCustomTools(eventBus, cwd, agentDir);
760
+ const { extensions, errors } = await discoverExtensions(eventBus, cwd, agentDir);
764
761
 
765
762
  // Context files
766
763
  const contextFiles = discoverContextFiles(cwd, agentDir);
767
764
 
768
765
  // Prompt templates
769
- const commands = discoverPromptTemplates(cwd, agentDir);
766
+ const templates = discoverPromptTemplates(cwd, agentDir);
770
767
 
771
768
  // Settings (global + project merged)
772
769
  const settings = loadSettings(cwd, agentDir);
@@ -816,8 +813,8 @@ import {
816
813
  SettingsManager,
817
814
  readTool,
818
815
  bashTool,
819
- type HookFactory,
820
- type CustomTool,
816
+ type ExtensionFactory,
817
+ type ToolDefinition,
821
818
  } from "@mariozechner/pi-coding-agent";
822
819
 
823
820
  // Set up auth storage (custom location)
@@ -831,16 +828,16 @@ if (process.env.MY_KEY) {
831
828
  // Model registry (no custom models.json)
832
829
  const modelRegistry = new ModelRegistry(authStorage);
833
830
 
834
- // Inline hook
835
- const auditHook: HookFactory = (api) => {
836
- api.on("tool_call", async (event) => {
831
+ // Inline extension
832
+ const auditExtension: ExtensionFactory = (pi) => {
833
+ pi.on("tool_call", async (event) => {
837
834
  console.log(`[Audit] ${event.toolName}`);
838
835
  return undefined;
839
836
  });
840
837
  };
841
838
 
842
839
  // Inline tool
843
- const statusTool: CustomTool = {
840
+ const statusTool: ToolDefinition = {
844
841
  name: "status",
845
842
  label: "Status",
846
843
  description: "Get system status",
@@ -872,8 +869,8 @@ const { session } = await createAgentSession({
872
869
  systemPrompt: "You are a minimal assistant. Be concise.",
873
870
 
874
871
  tools: [readTool, bashTool],
875
- customTools: [{ tool: statusTool }],
876
- hooks: [{ factory: auditHook }],
872
+ customTools: [statusTool],
873
+ extensions: [auditExtension],
877
874
  skills: [],
878
875
  contextFiles: [],
879
876
  promptTemplates: [],
@@ -961,7 +958,7 @@ The SDK is preferred when:
961
958
  - You want type safety
962
959
  - You're in the same Node.js process
963
960
  - You need direct access to agent state
964
- - You want to customize tools/hooks programmatically
961
+ - You want to customize tools/extensions programmatically
965
962
 
966
963
  RPC mode is preferred when:
967
964
  - You're integrating from another language
@@ -984,12 +981,11 @@ discoverModels
984
981
 
985
982
  // Discovery
986
983
  discoverSkills
987
- discoverHooks
988
- discoverCustomTools
984
+ discoverExtensions
989
985
  discoverContextFiles
990
986
  discoverPromptTemplates
991
987
 
992
- // Event Bus (for shared hook/tool communication)
988
+ // Event Bus (for shared extension communication)
993
989
  createEventBus
994
990
 
995
991
  // Helpers
@@ -1015,8 +1011,9 @@ createGrepTool, createFindTool, createLsTool
1015
1011
  // Types
1016
1012
  type CreateAgentSessionOptions
1017
1013
  type CreateAgentSessionResult
1018
- type CustomTool
1019
- type HookFactory
1014
+ type ExtensionFactory
1015
+ type ExtensionAPI
1016
+ type ToolDefinition
1020
1017
  type Skill
1021
1018
  type PromptTemplate
1022
1019
  type Settings
@@ -1024,28 +1021,4 @@ type SkillsSettings
1024
1021
  type Tool
1025
1022
  ```
1026
1023
 
1027
- For hook types, import from the hooks subpath:
1028
-
1029
- ```typescript
1030
- import type {
1031
- HookAPI,
1032
- HookMessage,
1033
- HookFactory,
1034
- HookEventContext,
1035
- HookCommandContext,
1036
- ToolCallEvent,
1037
- ToolResultEvent,
1038
- } from "@mariozechner/pi-coding-agent/hooks";
1039
- ```
1040
-
1041
- For message utilities:
1042
-
1043
- ```typescript
1044
- import { isHookMessage, createHookMessage } from "@mariozechner/pi-coding-agent";
1045
- ```
1046
-
1047
- For config utilities:
1048
-
1049
- ```typescript
1050
- import { getAgentDir } from "@mariozechner/pi-coding-agent/config";
1051
- ```
1024
+ For extension types, see [extensions.md](extensions.md) for the full API.
@@ -10,9 +10,12 @@ Programmatic usage via `createAgentSession()`. Shows how to customize models, pr
10
10
  ### [extensions/](extensions/)
11
11
  Example extensions demonstrating:
12
12
  - Lifecycle event handlers (tool interception, safety gates, context modifications)
13
- - Custom tools (todo lists, subagents)
13
+ - Custom tools (todo lists, questions, subagents, output truncation)
14
14
  - Commands and keyboard shortcuts
15
- - External integrations (git, file watchers)
15
+ - Custom UI (footers, headers, editors, overlays)
16
+ - Git integration (checkpoints, auto-commit)
17
+ - System prompt modifications and custom compaction
18
+ - External integrations (SSH, file watchers, system theme sync)
16
19
 
17
20
  ## Documentation
18
21
 
@@ -30,8 +30,10 @@ cp permission-gate.ts ~/.pi/agent/extensions/
30
30
  |-----------|-------------|
31
31
  | `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
32
32
  | `hello.ts` | Minimal custom tool example |
33
- | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
33
+ | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI |
34
+ | `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
34
35
  | `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
36
+ | `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
35
37
  | `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
36
38
  | `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
37
39
 
@@ -40,16 +42,24 @@ cp permission-gate.ts ~/.pi/agent/extensions/
40
42
  | Extension | Description |
41
43
  |-----------|-------------|
42
44
  | `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
43
- | `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
45
+ | `plan-mode/` | Claude Code-style plan mode for read-only exploration with `/plan` command and step tracking |
44
46
  | `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
45
47
  | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
46
48
  | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
47
49
  | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
50
+ | `model-status.ts` | Shows model changes in status bar via `model_select` hook |
48
51
  | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
49
52
  | `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
50
53
  | `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
51
54
  | `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
55
+ | `rainbow-editor.ts` | Animated rainbow text effect via custom editor |
52
56
  | `notify.ts` | Desktop notifications via OSC 777 when agent finishes (Ghostty, iTerm2, WezTerm) |
57
+ | `summarize.ts` | Summarize conversation with GPT-5.2 and show in transient UI |
58
+ | `custom-footer.ts` | Custom footer with git branch and token stats via `ctx.ui.setFooter()` |
59
+ | `custom-header.ts` | Custom header via `ctx.ui.setHeader()` |
60
+ | `overlay-test.ts` | Test overlay rendering with inline text inputs |
61
+ | `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` |
62
+ | `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
53
63
 
54
64
  ### Git Integration
55
65
 
@@ -63,8 +73,15 @@ cp permission-gate.ts ~/.pi/agent/extensions/
63
73
  | Extension | Description |
64
74
  |-----------|-------------|
65
75
  | `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
76
+ | `claude-rules.ts` | Scans `.claude/rules/` folder and lists rules in system prompt |
66
77
  | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
67
78
 
79
+ ### System Integration
80
+
81
+ | Extension | Description |
82
+ |-----------|-------------|
83
+ | `mac-system-theme.ts` | Syncs pi theme with macOS dark/light mode |
84
+
68
85
  ### External Dependencies
69
86
 
70
87
  | Extension | Description |
@@ -0,0 +1,65 @@
1
+ # Plan Mode Extension
2
+
3
+ Read-only exploration mode for safe code analysis.
4
+
5
+ ## Features
6
+
7
+ - **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question
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
+ - `Shift+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
+ - Only read-only tools available
41
+ - Bash commands filtered through allowlist
42
+ - Agent creates a plan without making changes
43
+
44
+ ### Execution Mode
45
+ - Full tool access restored
46
+ - Agent executes steps in order
47
+ - `[DONE:n]` markers track completion
48
+ - Widget shows progress
49
+
50
+ ### Command Allowlist
51
+
52
+ Safe commands (allowed):
53
+ - File inspection: `cat`, `head`, `tail`, `less`, `more`
54
+ - Search: `grep`, `find`, `rg`, `fd`
55
+ - Directory: `ls`, `pwd`, `tree`
56
+ - Git read: `git status`, `git log`, `git diff`, `git branch`
57
+ - Package info: `npm list`, `npm outdated`, `yarn info`
58
+ - System info: `uname`, `whoami`, `date`, `uptime`
59
+
60
+ Blocked commands:
61
+ - File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
62
+ - Git write: `git add`, `git commit`, `git push`
63
+ - Package install: `npm install`, `yarn add`, `pip install`
64
+ - System: `sudo`, `kill`, `reboot`
65
+ - Editors: `vim`, `nano`, `code`
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Plan Mode Extension
3
+ *
4
+ * Read-only exploration mode for safe code analysis.
5
+ * When enabled, only read-only tools are available.
6
+ *
7
+ * Features:
8
+ * - /plan command or Shift+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 { AgentMessage } from "@mariozechner/pi-agent-core";
16
+ import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
17
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
+ import { Key } from "@mariozechner/pi-tui";
19
+ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
20
+
21
+ // Tools
22
+ const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
23
+ const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
24
+
25
+ // Type guard for assistant messages
26
+ function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
27
+ return m.role === "assistant" && Array.isArray(m.content);
28
+ }
29
+
30
+ // Extract text content from an assistant message
31
+ function getTextContent(message: AssistantMessage): string {
32
+ return message.content
33
+ .filter((block): block is TextContent => block.type === "text")
34
+ .map((block) => block.text)
35
+ .join("\n");
36
+ }
37
+
38
+ export default function planModeExtension(pi: ExtensionAPI): void {
39
+ let planModeEnabled = false;
40
+ let executionMode = false;
41
+ let todoItems: TodoItem[] = [];
42
+
43
+ pi.registerFlag("plan", {
44
+ description: "Start in plan mode (read-only exploration)",
45
+ type: "boolean",
46
+ default: false,
47
+ });
48
+
49
+ function updateStatus(ctx: ExtensionContext): void {
50
+ // Footer status
51
+ if (executionMode && todoItems.length > 0) {
52
+ const completed = todoItems.filter((t) => t.completed).length;
53
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
54
+ } else if (planModeEnabled) {
55
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
56
+ } else {
57
+ ctx.ui.setStatus("plan-mode", undefined);
58
+ }
59
+
60
+ // Widget showing todo list
61
+ if (executionMode && todoItems.length > 0) {
62
+ const lines = todoItems.map((item) => {
63
+ if (item.completed) {
64
+ return (
65
+ ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
66
+ );
67
+ }
68
+ return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
69
+ });
70
+ ctx.ui.setWidget("plan-todos", lines);
71
+ } else {
72
+ ctx.ui.setWidget("plan-todos", undefined);
73
+ }
74
+ }
75
+
76
+ function togglePlanMode(ctx: ExtensionContext): void {
77
+ planModeEnabled = !planModeEnabled;
78
+ executionMode = false;
79
+ todoItems = [];
80
+
81
+ if (planModeEnabled) {
82
+ pi.setActiveTools(PLAN_MODE_TOOLS);
83
+ ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
84
+ } else {
85
+ pi.setActiveTools(NORMAL_MODE_TOOLS);
86
+ ctx.ui.notify("Plan mode disabled. Full access restored.");
87
+ }
88
+ updateStatus(ctx);
89
+ }
90
+
91
+ function persistState(): void {
92
+ pi.appendEntry("plan-mode", {
93
+ enabled: planModeEnabled,
94
+ todos: todoItems,
95
+ executing: executionMode,
96
+ });
97
+ }
98
+
99
+ pi.registerCommand("plan", {
100
+ description: "Toggle plan mode (read-only exploration)",
101
+ handler: async (_args, ctx) => togglePlanMode(ctx),
102
+ });
103
+
104
+ pi.registerCommand("todos", {
105
+ description: "Show current plan todo list",
106
+ handler: async (_args, ctx) => {
107
+ if (todoItems.length === 0) {
108
+ ctx.ui.notify("No todos. Create a plan first with /plan", "info");
109
+ return;
110
+ }
111
+ const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
112
+ ctx.ui.notify(`Plan Progress:\n${list}`, "info");
113
+ },
114
+ });
115
+
116
+ pi.registerShortcut(Key.shift("p"), {
117
+ description: "Toggle plan mode",
118
+ handler: async (ctx) => togglePlanMode(ctx),
119
+ });
120
+
121
+ // Block destructive bash commands in plan mode
122
+ pi.on("tool_call", async (event) => {
123
+ if (!planModeEnabled || event.toolName !== "bash") return;
124
+
125
+ const command = event.input.command as string;
126
+ if (!isSafeCommand(command)) {
127
+ return {
128
+ block: true,
129
+ reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`,
130
+ };
131
+ }
132
+ });
133
+
134
+ // Filter out stale plan mode context when not in plan mode
135
+ pi.on("context", async (event) => {
136
+ if (planModeEnabled) return;
137
+
138
+ return {
139
+ messages: event.messages.filter((m) => {
140
+ const msg = m as AgentMessage & { customType?: string };
141
+ if (msg.customType === "plan-mode-context") return false;
142
+ if (msg.role !== "user") return true;
143
+
144
+ const content = msg.content;
145
+ if (typeof content === "string") {
146
+ return !content.includes("[PLAN MODE ACTIVE]");
147
+ }
148
+ if (Array.isArray(content)) {
149
+ return !content.some(
150
+ (c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
151
+ );
152
+ }
153
+ return true;
154
+ }),
155
+ };
156
+ });
157
+
158
+ // Inject plan/execution context before agent starts
159
+ pi.on("before_agent_start", async () => {
160
+ if (planModeEnabled) {
161
+ return {
162
+ message: {
163
+ customType: "plan-mode-context",
164
+ content: `[PLAN MODE ACTIVE]
165
+ You are in plan mode - a read-only exploration mode for safe code analysis.
166
+
167
+ Restrictions:
168
+ - You can only use: read, bash, grep, find, ls, questionnaire
169
+ - You CANNOT use: edit, write (file modifications are disabled)
170
+ - Bash is restricted to an allowlist of read-only commands
171
+
172
+ Ask clarifying questions using the questionnaire tool.
173
+ Use brave-search skill via bash for web research.
174
+
175
+ Create a detailed numbered plan under a "Plan:" header:
176
+
177
+ Plan:
178
+ 1. First step description
179
+ 2. Second step description
180
+ ...
181
+
182
+ Do NOT attempt to make changes - just describe what you would do.`,
183
+ display: false,
184
+ },
185
+ };
186
+ }
187
+
188
+ if (executionMode && todoItems.length > 0) {
189
+ const remaining = todoItems.filter((t) => !t.completed);
190
+ const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
191
+ return {
192
+ message: {
193
+ customType: "plan-execution-context",
194
+ content: `[EXECUTING PLAN - Full tool access enabled]
195
+
196
+ Remaining steps:
197
+ ${todoList}
198
+
199
+ Execute each step in order.
200
+ After completing a step, include a [DONE:n] tag in your response.`,
201
+ display: false,
202
+ },
203
+ };
204
+ }
205
+ });
206
+
207
+ // Track progress after each turn
208
+ pi.on("turn_end", async (event, ctx) => {
209
+ if (!executionMode || todoItems.length === 0) return;
210
+ if (!isAssistantMessage(event.message)) return;
211
+
212
+ const text = getTextContent(event.message);
213
+ if (markCompletedSteps(text, todoItems) > 0) {
214
+ updateStatus(ctx);
215
+ }
216
+ persistState();
217
+ });
218
+
219
+ // Handle plan completion and plan mode UI
220
+ pi.on("agent_end", async (event, ctx) => {
221
+ // Check if execution is complete
222
+ if (executionMode && todoItems.length > 0) {
223
+ if (todoItems.every((t) => t.completed)) {
224
+ const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
225
+ pi.sendMessage(
226
+ { customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
227
+ { triggerTurn: false },
228
+ );
229
+ executionMode = false;
230
+ todoItems = [];
231
+ pi.setActiveTools(NORMAL_MODE_TOOLS);
232
+ updateStatus(ctx);
233
+ persistState(); // Save cleared state so resume doesn't restore old execution mode
234
+ }
235
+ return;
236
+ }
237
+
238
+ if (!planModeEnabled || !ctx.hasUI) return;
239
+
240
+ // Extract todos from last assistant message
241
+ const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
242
+ if (lastAssistant) {
243
+ const extracted = extractTodoItems(getTextContent(lastAssistant));
244
+ if (extracted.length > 0) {
245
+ todoItems = extracted;
246
+ }
247
+ }
248
+
249
+ // Show plan steps and prompt for next action
250
+ if (todoItems.length > 0) {
251
+ const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
252
+ pi.sendMessage(
253
+ {
254
+ customType: "plan-todo-list",
255
+ content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
256
+ display: true,
257
+ },
258
+ { triggerTurn: false },
259
+ );
260
+ }
261
+
262
+ const choice = await ctx.ui.select("Plan mode - what next?", [
263
+ todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan",
264
+ "Stay in plan mode",
265
+ "Refine the plan",
266
+ ]);
267
+
268
+ if (choice?.startsWith("Execute")) {
269
+ planModeEnabled = false;
270
+ executionMode = todoItems.length > 0;
271
+ pi.setActiveTools(NORMAL_MODE_TOOLS);
272
+ updateStatus(ctx);
273
+
274
+ const execMessage =
275
+ todoItems.length > 0
276
+ ? `Execute the plan. Start with: ${todoItems[0].text}`
277
+ : "Execute the plan you just created.";
278
+ pi.sendMessage(
279
+ { customType: "plan-mode-execute", content: execMessage, display: true },
280
+ { triggerTurn: true },
281
+ );
282
+ } else if (choice === "Refine the plan") {
283
+ const refinement = await ctx.ui.editor("Refine the plan:", "");
284
+ if (refinement?.trim()) {
285
+ pi.sendUserMessage(refinement.trim());
286
+ }
287
+ }
288
+ });
289
+
290
+ // Restore state on session start/resume
291
+ pi.on("session_start", async (_event, ctx) => {
292
+ if (pi.getFlag("plan") === true) {
293
+ planModeEnabled = true;
294
+ }
295
+
296
+ const entries = ctx.sessionManager.getEntries();
297
+
298
+ // Restore persisted state
299
+ const planModeEntry = entries
300
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
301
+ .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
302
+
303
+ if (planModeEntry?.data) {
304
+ planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
305
+ todoItems = planModeEntry.data.todos ?? todoItems;
306
+ executionMode = planModeEntry.data.executing ?? executionMode;
307
+ }
308
+
309
+ // On resume: re-scan messages to rebuild completion state
310
+ // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
311
+ const isResume = planModeEntry !== undefined;
312
+ if (isResume && executionMode && todoItems.length > 0) {
313
+ // Find the index of the last plan-mode-execute entry (marks when current execution started)
314
+ let executeIndex = -1;
315
+ for (let i = entries.length - 1; i >= 0; i--) {
316
+ const entry = entries[i] as { type: string; customType?: string };
317
+ if (entry.customType === "plan-mode-execute") {
318
+ executeIndex = i;
319
+ break;
320
+ }
321
+ }
322
+
323
+ // Only scan messages after the execute marker
324
+ const messages: AssistantMessage[] = [];
325
+ for (let i = executeIndex + 1; i < entries.length; i++) {
326
+ const entry = entries[i];
327
+ if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
328
+ messages.push(entry.message as AssistantMessage);
329
+ }
330
+ }
331
+ const allText = messages.map(getTextContent).join("\n");
332
+ markCompletedSteps(allText, todoItems);
333
+ }
334
+
335
+ if (planModeEnabled) {
336
+ pi.setActiveTools(PLAN_MODE_TOOLS);
337
+ }
338
+ updateStatus(ctx);
339
+ });
340
+ }