@oh-my-pi/pi-coding-agent 8.4.0 → 8.4.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +6 -6
  3. package/scripts/format-prompts.ts +65 -23
  4. package/src/commit/agentic/prompts/session-user.md +0 -1
  5. package/src/commit/agentic/prompts/split-confirm.md +1 -1
  6. package/src/commit/agentic/prompts/system.md +1 -1
  7. package/src/commit/prompts/analysis-system.md +23 -26
  8. package/src/commit/prompts/analysis-user.md +1 -1
  9. package/src/commit/prompts/changelog-system.md +1 -2
  10. package/src/commit/prompts/changelog-user.md +1 -2
  11. package/src/commit/prompts/file-observer-system.md +1 -3
  12. package/src/commit/prompts/file-observer-user.md +1 -2
  13. package/src/commit/prompts/reduce-system.md +16 -16
  14. package/src/commit/prompts/reduce-user.md +1 -1
  15. package/src/commit/prompts/summary-retry.md +1 -2
  16. package/src/commit/prompts/summary-system.md +10 -10
  17. package/src/commit/prompts/summary-user.md +1 -1
  18. package/src/commit/prompts/types-description.md +1 -1
  19. package/src/config/keybindings.ts +3 -0
  20. package/src/config/settings-manager.ts +5 -0
  21. package/src/internal-urls/index.ts +1 -0
  22. package/src/internal-urls/plan-protocol.ts +95 -0
  23. package/src/modes/components/status-line/presets.ts +7 -7
  24. package/src/modes/components/status-line/segments.ts +16 -0
  25. package/src/modes/components/status-line/types.ts +4 -0
  26. package/src/modes/components/status-line-segment-editor.ts +1 -0
  27. package/src/modes/components/status-line.ts +16 -2
  28. package/src/modes/controllers/command-controller.ts +42 -0
  29. package/src/modes/controllers/event-controller.ts +13 -0
  30. package/src/modes/controllers/input-controller.ts +16 -0
  31. package/src/modes/interactive-mode.ts +219 -1
  32. package/src/modes/theme/theme.ts +7 -0
  33. package/src/modes/types.ts +7 -0
  34. package/src/patch/index.ts +9 -3
  35. package/src/plan-mode/state.ts +6 -0
  36. package/src/prompts/agents/explore.md +1 -1
  37. package/src/prompts/agents/frontmatter.md +1 -1
  38. package/src/prompts/agents/init.md +1 -1
  39. package/src/prompts/agents/plan.md +1 -12
  40. package/src/prompts/agents/reviewer.md +7 -7
  41. package/src/prompts/agents/task.md +1 -2
  42. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  43. package/src/prompts/compaction/branch-summary.md +3 -1
  44. package/src/prompts/compaction/compaction-summary.md +3 -1
  45. package/src/prompts/compaction/compaction-turn-prefix.md +2 -1
  46. package/src/prompts/compaction/compaction-update-summary.md +3 -1
  47. package/src/prompts/review-request.md +4 -1
  48. package/src/prompts/system/custom-system-prompt.md +8 -8
  49. package/src/prompts/system/file-operations.md +1 -1
  50. package/src/prompts/system/plan-mode-active.md +136 -0
  51. package/src/prompts/system/plan-mode-approved.md +11 -0
  52. package/src/prompts/system/plan-mode-reference.md +13 -0
  53. package/src/prompts/system/plan-mode-subagent.md +38 -0
  54. package/src/prompts/system/summarization-system.md +1 -1
  55. package/src/prompts/system/system-prompt.md +17 -27
  56. package/src/prompts/system/title-system.md +1 -1
  57. package/src/prompts/system/ttsr-interrupt.md +1 -1
  58. package/src/prompts/system/web-search.md +1 -1
  59. package/src/prompts/tools/ask.md +1 -3
  60. package/src/prompts/tools/bash.md +1 -1
  61. package/src/prompts/tools/calculator.md +1 -1
  62. package/src/prompts/tools/enter-plan-mode.md +73 -0
  63. package/src/prompts/tools/exit-plan-mode.md +23 -0
  64. package/src/prompts/tools/fetch.md +1 -1
  65. package/src/prompts/tools/find.md +1 -1
  66. package/src/prompts/tools/gemini-image.md +1 -1
  67. package/src/prompts/tools/grep.md +1 -1
  68. package/src/prompts/tools/lsp.md +1 -1
  69. package/src/prompts/tools/patch.md +1 -3
  70. package/src/prompts/tools/python.md +2 -4
  71. package/src/prompts/tools/read.md +1 -1
  72. package/src/prompts/tools/replace.md +16 -16
  73. package/src/prompts/tools/ssh.md +1 -4
  74. package/src/prompts/tools/task.md +1 -3
  75. package/src/prompts/tools/todo-write.md +13 -16
  76. package/src/prompts/tools/web-search.md +1 -1
  77. package/src/prompts/tools/write.md +1 -1
  78. package/src/sdk.ts +61 -10
  79. package/src/session/agent-session.ts +267 -0
  80. package/src/task/executor.ts +1 -0
  81. package/src/task/index.ts +18 -4
  82. package/src/tools/enter-plan-mode.ts +76 -0
  83. package/src/tools/exit-plan-mode.ts +62 -0
  84. package/src/tools/find.ts +5 -2
  85. package/src/tools/grep.ts +13 -12
  86. package/src/tools/index.ts +19 -1
  87. package/src/tools/plan-mode-guard.ts +46 -0
  88. package/src/tools/read.ts +8 -4
  89. package/src/tools/write.ts +3 -2
  90. package/src/utils/tools-manager.ts +38 -9
  91. package/src/web/search/providers/perplexity.ts +3 -1
  92. package/src/web/search/types.ts +3 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.4.1] - 2026-01-25
6
+
7
+ ### Added
8
+ - Added core plan mode with plan file approval workflow and tool gating
9
+ - Added plan:// internal URLs for plan file access and subagent plan-mode system prompt
10
+ - Added plan mode toggle shortcut with paused status indicator
11
+
12
+ ### Fixed
13
+ - Fixed plan reference injection and workflow prompt parameters for plan mode
14
+ - Fixed tool downloads hanging on slow/blocked GitHub by adding timeouts and zip extraction fallback
15
+ - Fixed missing UI notification when tools are downloaded or installed on demand
5
16
  ## [8.4.0] - 2026-01-25
6
17
 
7
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.4.0",
3
+ "version": "8.4.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -75,11 +75,11 @@
75
75
  "test": "bun test"
76
76
  },
77
77
  "dependencies": {
78
- "@oh-my-pi/omp-stats": "8.4.0",
79
- "@oh-my-pi/pi-agent-core": "8.4.0",
80
- "@oh-my-pi/pi-ai": "8.4.0",
81
- "@oh-my-pi/pi-tui": "8.4.0",
82
- "@oh-my-pi/pi-utils": "8.4.0",
78
+ "@oh-my-pi/omp-stats": "8.4.1",
79
+ "@oh-my-pi/pi-agent-core": "8.4.1",
80
+ "@oh-my-pi/pi-ai": "8.4.1",
81
+ "@oh-my-pi/pi-tui": "8.4.1",
82
+ "@oh-my-pi/pi-utils": "8.4.1",
83
83
  "@openai/agents": "^0.4.3",
84
84
  "@sinclair/typebox": "^0.34.46",
85
85
  "ajv": "^8.17.1",
@@ -3,16 +3,22 @@
3
3
  * Format prompt files (mixed XML + Markdown + Handlebars).
4
4
  *
5
5
  * Rules:
6
- * 1. No blank line between "text:" and following list/block
6
+ * 1. No blank line before list items
7
7
  * 2. No blank line after opening XML tag or Handlebars block
8
8
  * 3. No blank line before closing XML tag or Handlebars block
9
- * 4. Collapse 2+ blank lines to single blank line
10
- * 5. Trim trailing whitespace (preserve indentation)
11
- * 6. Ensure single newline at EOF
9
+ * 4. Strip leading whitespace from closing XML tags and Handlebars (lines starting with {{)
10
+ * 5. Compact markdown tables (remove padding)
11
+ * 6. Collapse 2+ blank lines to single blank line
12
+ * 7. Trim trailing whitespace (preserve indentation)
13
+ * 8. No trailing newline at EOF
12
14
  */
13
15
  import { Glob } from "bun";
14
16
 
15
17
  const PROMPTS_DIR = new URL("../src/prompts/", import.meta.url).pathname;
18
+ const COMMIT_PROMPTS_DIR = new URL("../src/commit/prompts/", import.meta.url).pathname;
19
+ const AGENTIC_PROMPTS_DIR = new URL("../src/commit/agentic/prompts/", import.meta.url).pathname;
20
+
21
+ const PROMPT_DIRS = [PROMPTS_DIR, COMMIT_PROMPTS_DIR, AGENTIC_PROMPTS_DIR];
16
22
 
17
23
  // Opening XML tag (not self-closing, not closing)
18
24
  const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
@@ -22,12 +28,38 @@ const CLOSING_XML = /^<\/([a-z_-]+)>$/;
22
28
  const OPENING_HBS = /^\{\{#/;
23
29
  // Handlebars block end: {{/if}}, {{/has}}, {{/list}}, etc.
24
30
  const CLOSING_HBS = /^\{\{\//;
25
- // Line ending with colon (intro to a list) - handles **bold:** too
26
- const ENDS_WITH_COLON = /:\**\s*$/;
27
- // List item or Handlebars conditional that acts like list
28
- const LIST_OR_BLOCK = /^(\s*)[-*]|\d+\.\s|^\{\{#/;
31
+ // List item (- or * or 1.)
32
+ const LIST_ITEM = /^[-*]|\d+\.\s/;
29
33
  // Code fence
30
34
  const CODE_FENCE = /^```/;
35
+ // Table row
36
+ const TABLE_ROW = /^\|.*\|$/;
37
+ // Table separator (|---|---|)
38
+ const TABLE_SEP = /^\|[-:\s|]+\|$/;
39
+
40
+ /** Compact a table row by trimming cell padding */
41
+ function compactTableRow(line: string): string {
42
+ // Split by |, trim each cell, rejoin
43
+ const cells = line.split("|");
44
+ return cells.map((c) => c.trim()).join("|");
45
+ }
46
+
47
+ /** Compact a table separator row */
48
+ function compactTableSep(line: string): string {
49
+ // Normalize to minimal |---|---|
50
+ const cells = line.split("|").filter((c) => c.trim());
51
+ const normalized = cells.map((c) => {
52
+ const trimmed = c.trim();
53
+ // Preserve alignment markers
54
+ const left = trimmed.startsWith(":");
55
+ const right = trimmed.endsWith(":");
56
+ if (left && right) return ":---:";
57
+ if (left) return ":---";
58
+ if (right) return "---:";
59
+ return "---";
60
+ });
61
+ return "|" + normalized.join("|") + "|";
62
+ }
31
63
 
32
64
  function formatPrompt(content: string): string {
33
65
  const lines = content.split("\n");
@@ -37,9 +69,6 @@ function formatPrompt(content: string): string {
37
69
  for (let i = 0; i < lines.length; i++) {
38
70
  let line = lines[i];
39
71
 
40
- // Trim trailing whitespace (preserve leading)
41
- line = line.trimEnd();
42
-
43
72
  const trimmed = line.trim();
44
73
 
45
74
  // Track code blocks - don't modify inside them
@@ -54,6 +83,20 @@ function formatPrompt(content: string): string {
54
83
  continue;
55
84
  }
56
85
 
86
+ // Strip leading whitespace from closing XML tags and Handlebars
87
+ if (CLOSING_XML.test(trimmed) || trimmed.startsWith("{{")) {
88
+ line = trimmed;
89
+ } else if (TABLE_SEP.test(trimmed)) {
90
+ // Compact table separator
91
+ line = compactTableSep(trimmed);
92
+ } else if (TABLE_ROW.test(trimmed)) {
93
+ // Compact table row
94
+ line = compactTableRow(trimmed);
95
+ } else {
96
+ // Trim trailing whitespace (preserve leading for non-closing-tags)
97
+ line = line.trimEnd();
98
+ }
99
+
57
100
  const isBlank = trimmed === "";
58
101
 
59
102
  // Skip blank lines that violate our rules
@@ -61,8 +104,8 @@ function formatPrompt(content: string): string {
61
104
  const prevLine = result[result.length - 1]?.trim() ?? "";
62
105
  const nextLine = lines[i + 1]?.trim() ?? "";
63
106
 
64
- // Rule 1: No blank between "text:" and list/block
65
- if (ENDS_WITH_COLON.test(prevLine) && LIST_OR_BLOCK.test(nextLine)) {
107
+ // Rule 1: No blank line before list items
108
+ if (LIST_ITEM.test(nextLine)) {
66
109
  continue;
67
110
  }
68
111
 
@@ -93,11 +136,10 @@ function formatPrompt(content: string): string {
93
136
  result.push(line);
94
137
  }
95
138
 
96
- // Rule 6: Single newline at EOF
139
+ // Rule 8: No trailing newline at EOF
97
140
  while (result.length > 0 && result[result.length - 1].trim() === "") {
98
141
  result.pop();
99
142
  }
100
- result.push("");
101
143
 
102
144
  return result.join("\n");
103
145
  }
@@ -106,24 +148,24 @@ async function main() {
106
148
  const glob = new Glob("**/*.md");
107
149
  const files: string[] = [];
108
150
  let changed = 0;
151
+ const check = process.argv.includes("--check");
109
152
 
110
- for await (const path of glob.scan(PROMPTS_DIR)) {
111
- files.push(path);
153
+ for (const dir of PROMPT_DIRS) {
154
+ for await (const path of glob.scan(dir)) {
155
+ files.push(`${dir}${path}`);
156
+ }
112
157
  }
113
158
 
114
- const check = process.argv.includes("--check");
115
-
116
- for (const relativePath of files) {
117
- const fullPath = `${PROMPTS_DIR}${relativePath}`;
159
+ for (const fullPath of files) {
118
160
  const original = await Bun.file(fullPath).text();
119
161
  const formatted = formatPrompt(original);
120
162
 
121
163
  if (original !== formatted) {
122
164
  if (check) {
123
- console.log(`Would format: ${relativePath}`);
165
+ console.log(`Would format: ${fullPath}`);
124
166
  } else {
125
167
  await Bun.write(fullPath, formatted);
126
- console.log(`Formatted: ${relativePath}`);
168
+ console.log(`Formatted: ${fullPath}`);
127
169
  }
128
170
  changed++;
129
171
  }
@@ -19,7 +19,6 @@ You may include entries from this list in the propose_changelog `deletions` fiel
19
19
  {{name}}:
20
20
  {{#list items prefix="- " join="\n"}}{{this}}{{/list}}
21
21
  {{/each}}
22
-
23
22
  {{/each}}
24
23
  {{/if}}
25
24
 
@@ -1 +1 @@
1
- Split commit plan has {{count}} commits. Proceed? (y/N):
1
+ Split commit plan has {{count}} commits. Proceed? (y/N):
@@ -37,4 +37,4 @@ Tool guidance:
37
37
  ## Changelog Requirements
38
38
 
39
39
  If changelog targets are provided, you MUST call `propose_changelog` before finishing.
40
- If you propose a split commit plan, include changelog target files in the relevant commit changes.
40
+ If you propose a split commit plan, include changelog target files in the relevant commit changes.
@@ -4,7 +4,6 @@ You are a senior release engineer who writes precise, changelog-ready commit cla
4
4
 
5
5
  <instructions>
6
6
  Classify this git diff into conventional commit format. Get this right — it affects release notes and semantic versioning.
7
-
8
7
  ## 1. Determine Scope
9
8
 
10
9
  Apply scope when 60%+ of line changes target a single component:
@@ -16,7 +15,6 @@ Use null for: cross-cutting changes, no dominant component, project-wide refacto
16
15
  Forbidden scopes (use null): src, lib, include, tests, benches, examples, docs, project name, app, main, entire, all, misc.
17
16
 
18
17
  Prefer scopes from <common_scopes> over inventing new ones.
19
-
20
18
  ## 2. Generate Details (0-6 items)
21
19
 
22
20
  Each detail:
@@ -39,17 +37,16 @@ Priority: user-visible -> perf/security -> architecture -> internal.
39
37
  Exclude: import changes, whitespace, formatting, trivial renames, debug prints, comment-only, file moves without modification.
40
38
 
41
39
  State only visible rationale. If unclear, use neutral: "Updated logic for correctness."
42
-
43
40
  ## 3. Assign Changelog Metadata
44
41
 
45
- | Condition | changelog_category |
46
- |-----------|--------------------|
47
- | New public API, feature, capability | "Added" |
48
- | Modified existing behavior | "Changed" |
49
- | Bug fix, correction | "Fixed" |
50
- | Feature marked for removal | "Deprecated" |
51
- | Feature/API removed | "Removed" |
52
- | Security fix or improvement | "Security" |
42
+ |Condition|changelog_category|
43
+ |---|---|
44
+ |New public API, feature, capability|"Added"|
45
+ |Modified existing behavior|"Changed"|
46
+ |Bug fix, correction|"Fixed"|
47
+ |Feature marked for removal|"Deprecated"|
48
+ |Feature/API removed|"Removed"|
49
+ |Security fix or improvement|"Security"|
53
50
 
54
51
  user_visible: true for: new features, APIs, breaking changes, user-affecting bug fixes, user-facing docs, security fixes.
55
52
 
@@ -62,20 +59,20 @@ Omit changelog_category when user_visible is false.
62
59
  Call create_conventional_analysis with:
63
60
 
64
61
  {
65
- "type": "feat|fix|refactor|docs|test|chore|style|perf|build|ci|revert",
66
- "scope": "component-name" | null,
67
- "details": [
68
- {
69
- "text": "Past-tense description ending with period.",
70
- "changelog_category": "Added|Changed|Fixed|Deprecated|Removed|Security",
71
- "user_visible": true
72
- },
73
- {
74
- "text": "Internal change description.",
75
- "user_visible": false
76
- }
77
- ],
78
- "issue_refs": []
62
+ "type": "feat|fix|refactor|docs|test|chore|style|perf|build|ci|revert",
63
+ "scope": "component-name" | null,
64
+ "details": [
65
+ {
66
+ "text": "Past-tense description ending with period.",
67
+ "changelog_category": "Added|Changed|Fixed|Deprecated|Removed|Security",
68
+ "user_visible": true
69
+ },
70
+ {
71
+ "text": "Internal change description.",
72
+ "user_visible": false
73
+ }
74
+ ],
75
+ "issue_refs": []
79
76
  }
80
77
  </output_format>
81
78
 
@@ -152,4 +149,4 @@ Call create_conventional_analysis with:
152
149
  </example>
153
150
  </examples>
154
151
 
155
- Be thorough. This matters.
152
+ Be thorough. This matters.
@@ -38,4 +38,4 @@
38
38
 
39
39
  <diff>
40
40
  {{ diff }}
41
- </diff>
41
+ </diff>
@@ -2,7 +2,6 @@ You are an expert changelog writer who analyzes git diffs and produces Keep a Ch
2
2
 
3
3
  <instructions>
4
4
  Analyze the diff and return JSON changelog entries.
5
-
6
5
  1. Identify user-visible changes only
7
6
  2. Categorize each change (Added, Changed, Deprecated, Removed, Fixed, Security, Breaking Changes)
8
7
  3. Write entries starting with past-tense verb describing user impact
@@ -53,4 +52,4 @@ Return ONLY valid JSON. No markdown fences, no explanation.
53
52
 
54
53
  With entries: {"entries": {"Added": ["entry 1"], "Fixed": ["entry 2"]}}
55
54
  No changelog-worthy changes: {"entries": {}}
56
- </output_format>
55
+ </output_format>
@@ -3,7 +3,6 @@ Changelog: {{ changelog_path }}
3
3
  {{#if is_package_changelog}}Scope: Package-level changelog. Omit package name prefix from entries.{{/if}}
4
4
  </context>
5
5
  {{#if existing_entries}}
6
-
7
6
  <existing_entries>
8
7
  Already documented—skip these:
9
8
  {{ existing_entries }}
@@ -16,4 +15,4 @@ Already documented—skip these:
16
15
 
17
16
  <diff>
18
17
  {{ diff }}
19
- </diff>
18
+ </diff>
@@ -2,7 +2,6 @@
2
2
 
3
3
  <instructions>
4
4
  Extract factual observations from the diff. This matters—be precise.
5
-
6
5
  1. Use past-tense verb + specific target + optional purpose
7
6
  2. Max 100 characters per observation
8
7
  3. Consolidate related changes (e.g., "renamed 5 helper functions")
@@ -17,10 +16,9 @@ Exclude: import reordering, whitespace/formatting, comment-only changes, debug s
17
16
 
18
17
  <output_format>
19
18
  Plain list, no preamble, no summary, no markdown formatting.
20
-
21
19
  - added 'parse_config()' function for TOML configuration loading
22
20
  - removed deprecated 'legacy_init()' and all callers
23
21
  - changed 'Connection::new()' to accept '&Config' instead of individual params
24
22
  </output_format>
25
23
 
26
- Observations only. Classification happens in reduce phase.
24
+ Observations only. Classification happens in reduce phase.
@@ -2,8 +2,7 @@
2
2
  {{ diff }}
3
3
  </file>
4
4
  {{#if context_header}}
5
-
6
5
  <related_files>
7
6
  {{ context_header }}
8
7
  </related_files>
9
- {{/if}}
8
+ {{/if}}
@@ -41,20 +41,20 @@ Input observations:
41
41
 
42
42
  Output:
43
43
  {
44
- "type": "fix",
45
- "scope": "api",
46
- "details": [
47
- {
48
- "text": "Added token refresh guard to prevent duplicate refreshes.",
49
- "changelog_category": "Fixed",
50
- "user_visible": true
51
- },
52
- {
53
- "text": "Introduced retry wrapper for 429 responses.",
54
- "changelog_category": "Fixed",
55
- "user_visible": true
56
- }
57
- ],
58
- "issue_refs": []
44
+ "type": "fix",
45
+ "scope": "api",
46
+ "details": [
47
+ {
48
+ "text": "Added token refresh guard to prevent duplicate refreshes.",
49
+ "changelog_category": "Fixed",
50
+ "user_visible": true
51
+ },
52
+ {
53
+ "text": "Introduced retry wrapper for 429 responses.",
54
+ "changelog_category": "Fixed",
55
+ "user_visible": true
56
+ }
57
+ ],
58
+ "issue_refs": []
59
59
  }
60
- </example>
60
+ </example>
@@ -14,4 +14,4 @@
14
14
 
15
15
  <scope_candidates>
16
16
  {{ scope_candidates }}
17
- </scope_candidates>
17
+ </scope_candidates>
@@ -1,4 +1,3 @@
1
1
  {{#if base_context}}
2
2
  {{ base_context }}
3
-
4
- {{/if}}Previous summary failed validation: {{ errors }}
3
+ {{/if}}Previous summary failed validation: {{ errors }}
@@ -15,15 +15,15 @@ Get this right.
15
15
  </instructions>
16
16
 
17
17
  <verb_reference>
18
- | Type | Use instead |
19
- |----------|-------------------------------------------------|
20
- | feat | added, introduced, implemented, enabled |
21
- | fix | corrected, resolved, patched, addressed |
22
- | refactor | restructured, reorganized, migrated, simplified |
23
- | perf | optimized, reduced, eliminated, accelerated |
24
- | docs | documented, clarified, expanded |
25
- | build | upgraded, pinned, configured |
26
- | chore | cleaned, removed, renamed, organized |
18
+ |Type|Use instead|
19
+ |---|---|
20
+ |feat|added, introduced, implemented, enabled|
21
+ |fix|corrected, resolved, patched, addressed|
22
+ |refactor|restructured, reorganized, migrated, simplified|
23
+ |perf|optimized, reduced, eliminated, accelerated|
24
+ |docs|documented, clarified, expanded|
25
+ |build|upgraded, pinned, configured|
26
+ |chore|cleaned, removed, renamed, organized|
27
27
  </verb_reference>
28
28
 
29
29
  <examples>
@@ -49,4 +49,4 @@ comprehensive, various, several, improved, enhanced, quickly, simply, basically,
49
49
 
50
50
  <output_format>
51
51
  Output the description text only. Include motivation, name specifics, stay focused.
52
- </output_format>
52
+ </output_format>
@@ -10,4 +10,4 @@
10
10
 
11
11
  <diff_stat>
12
12
  {{ stat }}
13
- </diff_stat>
13
+ </diff_stat>
@@ -1,2 +1,2 @@
1
1
  Types: feat, fix, refactor, perf, docs, test, build, ci, chore, style, revert.
2
- Format: <type>(<scope>): <summary> with past-tense summary.
2
+ Format: <type>(<scope>): <summary> with past-tense summary.
@@ -23,6 +23,7 @@ export type AppAction =
23
23
  | "cycleModelForward"
24
24
  | "cycleModelBackward"
25
25
  | "selectModel"
26
+ | "togglePlanMode"
26
27
  | "expandTools"
27
28
  | "toggleThinking"
28
29
  | "externalEditor"
@@ -55,6 +56,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
55
56
  cycleModelForward: "ctrl+p",
56
57
  cycleModelBackward: "shift+ctrl+p",
57
58
  selectModel: "ctrl+l",
59
+ togglePlanMode: "alt+shift+p",
58
60
  historySearch: "ctrl+r",
59
61
  expandTools: "ctrl+o",
60
62
  toggleThinking: "ctrl+t",
@@ -82,6 +84,7 @@ const APP_ACTIONS: AppAction[] = [
82
84
  "cycleModelForward",
83
85
  "cycleModelBackward",
84
86
  "selectModel",
87
+ "togglePlanMode",
85
88
  "historySearch",
86
89
  "expandTools",
87
90
  "toggleThinking",
@@ -155,6 +155,7 @@ export interface TodoCompletionSettings {
155
155
  export type StatusLineSegmentId =
156
156
  | "pi"
157
157
  | "model"
158
+ | "plan_mode"
158
159
  | "path"
159
160
  | "git"
160
161
  | "subagents"
@@ -547,6 +548,10 @@ export class SettingsManager {
547
548
  return { ...this.settings };
548
549
  }
549
550
 
551
+ getPlansDirectory(_cwd: string = this.cwd ?? process.cwd()): string {
552
+ return path.join(getAgentDir(), "plans");
553
+ }
554
+
550
555
  /**
551
556
  * Access the underlying agent storage (null for in-memory settings).
552
557
  */
@@ -22,6 +22,7 @@
22
22
  export { AgentProtocolHandler, type AgentProtocolOptions } from "./agent-protocol";
23
23
  export { ArtifactProtocolHandler, type ArtifactProtocolOptions } from "./artifact-protocol";
24
24
  export { applyQuery, parseQuery, pathToQuery } from "./json-query";
25
+ export { PlanProtocolHandler, type PlanProtocolOptions, resolvePlanUrlToPath } from "./plan-protocol";
25
26
  export { InternalUrlRouter } from "./router";
26
27
  export { RuleProtocolHandler, type RuleProtocolOptions } from "./rule-protocol";
27
28
  export { SkillProtocolHandler, type SkillProtocolOptions } from "./skill-protocol";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Protocol handler for plan:// URLs.
3
+ *
4
+ * Resolves plan references to plan files under the plans directory.
5
+ *
6
+ * URL forms:
7
+ * - plan://<sessionId> (defaults to plan.md)
8
+ * - plan://<sessionId>/plan.md
9
+ * - plan://<plan-id>.md (resolves directly under plans dir)
10
+ */
11
+ import * as path from "node:path";
12
+ import { isEnoent } from "@oh-my-pi/pi-utils";
13
+ import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
14
+
15
+ export interface PlanProtocolOptions {
16
+ getPlansDirectory: (cwd?: string) => string;
17
+ cwd: string;
18
+ }
19
+
20
+ function parsePlanUrl(input: string): InternalUrl {
21
+ let parsed: URL;
22
+ try {
23
+ parsed = new URL(input);
24
+ } catch {
25
+ throw new Error(`Invalid URL: ${input}`);
26
+ }
27
+
28
+ const hostMatch = input.match(/^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)/i);
29
+ let rawHost = hostMatch ? hostMatch[2] : parsed.hostname;
30
+ try {
31
+ rawHost = decodeURIComponent(rawHost);
32
+ } catch {
33
+ // Leave rawHost as-is if decoding fails.
34
+ }
35
+ (parsed as InternalUrl).rawHost = rawHost;
36
+ return parsed as InternalUrl;
37
+ }
38
+
39
+ function normalizeRelativePath(host: string, pathname: string): string {
40
+ const trimmedHost = host.replace(/^\/+/, "").replace(/\/+$/, "");
41
+ const trimmedPath = pathname.replace(/^\/+/, "");
42
+
43
+ if (!trimmedHost) {
44
+ throw new Error("plan:// URL requires a session or plan identifier");
45
+ }
46
+
47
+ if (trimmedPath) {
48
+ return path.join(trimmedHost, trimmedPath);
49
+ }
50
+
51
+ if (trimmedHost.endsWith(".md")) {
52
+ return trimmedHost;
53
+ }
54
+
55
+ return path.join(trimmedHost, "plan.md");
56
+ }
57
+
58
+ export function resolvePlanUrlToPath(input: string | InternalUrl, options: PlanProtocolOptions): string {
59
+ const url = typeof input === "string" ? parsePlanUrl(input) : input;
60
+ const host = url.rawHost || url.hostname;
61
+ const relativePath = normalizeRelativePath(host, url.pathname ?? "");
62
+ const plansDir = path.resolve(options.getPlansDirectory(options.cwd));
63
+ const resolved = path.resolve(plansDir, relativePath);
64
+
65
+ if (resolved !== plansDir && !resolved.startsWith(`${plansDir}${path.sep}`)) {
66
+ throw new Error("plan:// URL escapes the plans directory");
67
+ }
68
+
69
+ return resolved;
70
+ }
71
+
72
+ export class PlanProtocolHandler implements ProtocolHandler {
73
+ readonly scheme = "plan";
74
+
75
+ constructor(private readonly options: PlanProtocolOptions) {}
76
+
77
+ async resolve(url: InternalUrl): Promise<InternalResource> {
78
+ const planPath = resolvePlanUrlToPath(url, this.options);
79
+ try {
80
+ const content = await Bun.file(planPath).text();
81
+ return {
82
+ url: url.href,
83
+ content,
84
+ contentType: "text/markdown",
85
+ size: Buffer.byteLength(content, "utf-8"),
86
+ sourcePath: planPath,
87
+ };
88
+ } catch (error) {
89
+ if (isEnoent(error)) {
90
+ throw new Error(`Plan file not found: ${url.href}`);
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ }