@oh-my-pi/pi-coding-agent 13.3.13 → 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +97 -7
  2. package/examples/sdk/README.md +22 -0
  3. package/package.json +7 -7
  4. package/src/capability/index.ts +1 -11
  5. package/src/commit/analysis/index.ts +4 -4
  6. package/src/config/settings-schema.ts +18 -15
  7. package/src/config/settings.ts +2 -20
  8. package/src/discovery/index.ts +1 -11
  9. package/src/exa/index.ts +1 -10
  10. package/src/extensibility/custom-commands/index.ts +2 -15
  11. package/src/extensibility/custom-tools/index.ts +3 -18
  12. package/src/extensibility/custom-tools/loader.ts +28 -5
  13. package/src/extensibility/custom-tools/types.ts +18 -1
  14. package/src/extensibility/extensions/index.ts +9 -130
  15. package/src/extensibility/extensions/types.ts +2 -1
  16. package/src/extensibility/hooks/index.ts +3 -14
  17. package/src/extensibility/plugins/index.ts +6 -31
  18. package/src/index.ts +28 -220
  19. package/src/internal-urls/docs-index.generated.ts +3 -2
  20. package/src/internal-urls/index.ts +11 -16
  21. package/src/mcp/index.ts +11 -37
  22. package/src/mcp/tool-bridge.ts +3 -42
  23. package/src/mcp/transports/index.ts +2 -2
  24. package/src/modes/components/extensions/index.ts +3 -3
  25. package/src/modes/components/index.ts +35 -40
  26. package/src/modes/interactive-mode.ts +4 -1
  27. package/src/modes/rpc/rpc-mode.ts +1 -7
  28. package/src/modes/theme/theme.ts +11 -10
  29. package/src/modes/types.ts +1 -1
  30. package/src/patch/index.ts +4 -20
  31. package/src/prompts/system/system-prompt.md +18 -4
  32. package/src/prompts/tools/ast-edit.md +33 -0
  33. package/src/prompts/tools/ast-grep.md +34 -0
  34. package/src/prompts/tools/bash.md +2 -2
  35. package/src/prompts/tools/hashline.md +1 -0
  36. package/src/prompts/tools/resolve.md +8 -0
  37. package/src/sdk.ts +27 -7
  38. package/src/session/agent-session.ts +25 -36
  39. package/src/session/session-manager.ts +0 -30
  40. package/src/slash-commands/builtin-registry.ts +4 -2
  41. package/src/stt/index.ts +3 -3
  42. package/src/task/types.ts +2 -2
  43. package/src/tools/ast-edit.ts +480 -0
  44. package/src/tools/ast-grep.ts +435 -0
  45. package/src/tools/bash.ts +3 -2
  46. package/src/tools/gemini-image.ts +3 -3
  47. package/src/tools/grep.ts +26 -8
  48. package/src/tools/index.ts +55 -57
  49. package/src/tools/pending-action.ts +33 -0
  50. package/src/tools/render-utils.ts +10 -0
  51. package/src/tools/renderers.ts +6 -4
  52. package/src/tools/resolve.ts +156 -0
  53. package/src/tools/submit-result.ts +1 -1
  54. package/src/web/search/index.ts +6 -4
  55. package/src/web/search/providers/anthropic.ts +2 -2
  56. package/src/web/search/providers/base.ts +3 -0
  57. package/src/web/search/providers/exa.ts +11 -5
  58. package/src/web/search/providers/gemini.ts +112 -24
  59. package/src/patch/normative.ts +0 -72
  60. package/src/prompts/tools/ast-find.md +0 -20
  61. package/src/prompts/tools/ast-replace.md +0 -21
  62. package/src/tools/ast-find.ts +0 -316
  63. package/src/tools/ast-replace.ts +0 -294
@@ -1,41 +1,36 @@
1
1
  // UI Components barrel export
2
- export { AssistantMessageComponent } from "./assistant-message";
3
- export { BashExecutionComponent } from "./bash-execution";
4
- export { BorderedLoader } from "./bordered-loader";
5
- export { BranchSummaryMessageComponent } from "./branch-summary-message";
6
- export { CompactionSummaryMessageComponent } from "./compaction-summary-message";
7
- export { CountdownTimer } from "./countdown-timer";
8
- export { CustomEditor } from "./custom-editor";
9
- export { CustomMessageComponent } from "./custom-message";
10
- export { type RenderDiffOptions, renderDiff } from "./diff";
11
- export { DynamicBorder } from "./dynamic-border";
12
- export { FooterComponent } from "./footer";
13
- export { HookEditorComponent } from "./hook-editor";
14
- export { HookInputComponent, type HookInputOptions } from "./hook-input";
15
- export { HookMessageComponent } from "./hook-message";
16
- export { HookSelectorComponent } from "./hook-selector";
17
- export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding-hints";
18
- export { LoginDialogComponent } from "./login-dialog";
19
- export { ModelSelectorComponent } from "./model-selector";
20
- export { OAuthSelectorComponent } from "./oauth-selector";
21
- export { QueueModeSelectorComponent } from "./queue-mode-selector";
22
- export { ReadToolGroupComponent } from "./read-tool-group";
23
- export { SessionSelectorComponent } from "./session-selector";
24
- export {
25
- type SettingsCallbacks,
26
- type SettingsRuntimeContext,
27
- SettingsSelectorComponent,
28
- type StatusLinePreviewSettings,
29
- } from "./settings-selector";
30
- export { ShowImagesSelectorComponent } from "./show-images-selector";
31
- export { StatusLineComponent } from "./status-line";
32
- export { ThemeSelectorComponent } from "./theme-selector";
33
- export { ThinkingSelectorComponent } from "./thinking-selector";
34
- export { TodoReminderComponent } from "./todo-reminder";
35
- export { ToolExecutionComponent, type ToolExecutionHandle, type ToolExecutionOptions } from "./tool-execution";
36
- export { TreeSelectorComponent } from "./tree-selector";
37
- export { TtsrNotificationComponent } from "./ttsr-notification";
38
- export { UserMessageComponent } from "./user-message";
39
- export { UserMessageSelectorComponent } from "./user-message-selector";
40
- export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate";
41
- export { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome";
2
+ export * from "./assistant-message";
3
+ export * from "./bash-execution";
4
+ export * from "./bordered-loader";
5
+ export * from "./branch-summary-message";
6
+ export * from "./compaction-summary-message";
7
+ export * from "./countdown-timer";
8
+ export * from "./custom-editor";
9
+ export * from "./custom-message";
10
+ export * from "./diff";
11
+ export * from "./dynamic-border";
12
+ export * from "./footer";
13
+ export * from "./hook-editor";
14
+ export * from "./hook-input";
15
+ export * from "./hook-message";
16
+ export * from "./hook-selector";
17
+ export * from "./keybinding-hints";
18
+ export * from "./login-dialog";
19
+ export * from "./model-selector";
20
+ export * from "./oauth-selector";
21
+ export * from "./queue-mode-selector";
22
+ export * from "./read-tool-group";
23
+ export * from "./session-selector";
24
+ export * from "./settings-selector";
25
+ export * from "./show-images-selector";
26
+ export * from "./status-line";
27
+ export * from "./theme-selector";
28
+ export * from "./thinking-selector";
29
+ export * from "./todo-reminder";
30
+ export * from "./tool-execution";
31
+ export * from "./tree-selector";
32
+ export * from "./ttsr-notification";
33
+ export * from "./user-message";
34
+ export * from "./user-message-selector";
35
+ export * from "./visual-truncate";
36
+ export * from "./welcome";
@@ -702,7 +702,7 @@ export class InteractiveMode implements InteractiveModeContext {
702
702
  await this.session.prompt(prompt, { synthetic: true });
703
703
  }
704
704
 
705
- async handlePlanModeCommand(): Promise<void> {
705
+ async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
706
706
  if (this.planModeEnabled) {
707
707
  const confirmed = await this.showHookConfirm(
708
708
  "Exit plan mode?",
@@ -713,6 +713,9 @@ export class InteractiveMode implements InteractiveModeContext {
713
713
  return;
714
714
  }
715
715
  await this.#enterPlanMode();
716
+ if (initialPrompt) {
717
+ this.onInputCallback?.({ text: initialPrompt });
718
+ }
716
719
  }
717
720
 
718
721
  async handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void> {
@@ -23,13 +23,7 @@ import type {
23
23
  } from "./rpc-types";
24
24
 
25
25
  // Re-export types for consumers
26
- export type {
27
- RpcCommand,
28
- RpcExtensionUIRequest,
29
- RpcExtensionUIResponse,
30
- RpcResponse,
31
- RpcSessionState,
32
- } from "./rpc-types";
26
+ export type * from "./rpc-types";
33
27
 
34
28
  /**
35
29
  * Run in RPC mode.
@@ -1629,25 +1629,26 @@ function detectTerminalBackground(): "dark" | "light" {
1629
1629
  if (terminalReportedAppearance) {
1630
1630
  return terminalReportedAppearance;
1631
1631
  }
1632
- // macOS: query system appearance via CoreFoundation (native, no shell).
1633
- // Uses cached observer value, or falls back to CFPreferencesCopyAppValue.
1634
- // Works on all terminals including Warp which lacks Mode 2031 / OSC 11.
1635
- const macAppearance = macOSReportedAppearance ?? detectMacOSAppearance();
1636
- if (macAppearance) {
1637
- return macAppearance;
1638
- }
1639
- // Fallback: COLORFGBG environment variable (static, set once at terminal launch)
1632
+ // COLORFGBG is set by the terminal emulator to reflect the actual profile colors.
1633
+ // Check it before macOS system appearance because the terminal profile may differ
1634
+ // from the OS-level dark/light setting (e.g. dark terminal on macOS light mode).
1640
1635
  const colorfgbg = Bun.env.COLORFGBG || "";
1641
1636
  if (colorfgbg) {
1642
1637
  const parts = colorfgbg.split(";");
1643
1638
  if (parts.length >= 2) {
1644
1639
  const bg = parseInt(parts[1], 10);
1645
1640
  if (!Number.isNaN(bg)) {
1646
- const result = bg < 8 ? "dark" : "light";
1647
- return result;
1641
+ return bg < 8 ? "dark" : "light";
1648
1642
  }
1649
1643
  }
1650
1644
  }
1645
+ // macOS: query system appearance via CoreFoundation (native, no shell).
1646
+ // Uses cached observer value, or falls back to CFPreferencesCopyAppValue.
1647
+ // Works on all terminals including Warp which lacks Mode 2031 / OSC 11.
1648
+ const macAppearance = macOSReportedAppearance ?? detectMacOSAppearance();
1649
+ if (macAppearance) {
1650
+ return macAppearance;
1651
+ }
1651
1652
  return "dark";
1652
1653
  }
1653
1654
 
@@ -197,7 +197,7 @@ export interface InteractiveModeContext {
197
197
  toggleThinkingBlockVisibility(): void;
198
198
  openExternalEditor(): void;
199
199
  registerExtensionShortcuts(): void;
200
- handlePlanModeCommand(): Promise<void>;
200
+ handlePlanModeCommand(initialPrompt?: string): Promise<void>;
201
201
  handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
202
202
 
203
203
  // Hook UI methods
@@ -48,30 +48,14 @@ import { EditMatchError } from "./types";
48
48
  // Application
49
49
  export { applyPatch, defaultFileSystem, previewPatch } from "./applicator";
50
50
  // Diff generation
51
- export {
52
- computeEditDiff,
53
- computeHashlineDiff,
54
- computePatchDiff,
55
- generateDiffString,
56
- generateUnifiedDiffString,
57
- replaceText,
58
- } from "./diff";
51
+ export * from "./diff";
59
52
 
60
53
  // Fuzzy matching
61
- export { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch as findEditMatch, findMatch, seekSequence } from "./fuzzy";
54
+ export * from "./fuzzy";
62
55
  // Hashline
63
- export {
64
- applyHashlineEdits,
65
- computeLineHash,
66
- formatHashLines,
67
- HashlineMismatchError,
68
- parseTag,
69
- streamHashLinesFromLines,
70
- streamHashLinesFromUtf8,
71
- validateLineRef,
72
- } from "./hashline";
56
+ export * from "./hashline";
73
57
  // Normalization
74
- export { adjustIndentation, detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
58
+ export * from "./normalize";
75
59
  // Parsing
76
60
  export { normalizeCreateContent, normalizeDiff, parseHunks as parseDiffHunks } from "./parser";
77
61
  export type { EditRenderContext, EditToolDetails } from "./shared";
@@ -154,13 +154,28 @@ Semantic questions **MUST** be answered with semantic tools.
154
154
  - Can the server propose fixes/imports/refactors? → `lsp code_actions` (list first, then apply with `apply: true` + `query`)
155
155
  {{/has}}
156
156
 
157
- {{#ifAny (includes tools "ast_find") (includes tools "ast_replace")}}
157
+ {{#ifAny (includes tools "ast_grep") (includes tools "ast_edit")}}
158
158
  ### AST tools for structural code work
159
159
 
160
160
  When AST tools are available, syntax-aware operations take priority over text hacks.
161
- {{#has tools "ast_find"}}- Use `ast_find` for structural discovery (call shapes, declarations, syntax patterns) before text grep when code structure matters{{/has}}
162
- {{#has tools "ast_replace"}}- Use `ast_replace` for structural codemods/replacements; do not use bash `sed`/`perl`/`awk` for syntax-level rewrites{{/has}}
161
+ {{#has tools "ast_grep"}}- Use `ast_grep` for structural discovery (call shapes, declarations, syntax patterns) before text grep when code structure matters{{/has}}
162
+ {{#has tools "ast_edit"}}- Use `ast_edit` for structural codemods/replacements; do not use bash `sed`/`perl`/`awk` for syntax-level rewrites{{/has}}
163
163
  - Use `grep` for plain text/regex lookup only when AST shape is irrelevant
164
+
165
+ #### Pattern syntax
166
+
167
+ Patterns match **AST structure, not text** — whitespace and formatting are irrelevant. `foo( x, y )` and `foo(x,y)` are the same pattern.
168
+
169
+ |Syntax|Name|Matches|
170
+ |---|---|---|
171
+ |`$VAR`|Capture|One AST node, bound as `$VAR`|
172
+ |`$_`|Wildcard|One AST node, not captured|
173
+ |`$$$VAR`|Variadic capture|Zero or more nodes, bound as `$VAR`|
174
+ |`$$$`|Variadic wildcard|Zero or more nodes, not captured|
175
+
176
+ Metavariable names **MUST** be UPPERCASE (`$A`, `$FUNC`, `$MY_VAR`). Lowercase `$var` is invalid.
177
+
178
+ When a metavariable appears multiple times in one pattern, all occurrences must match **identical** code: `$A == $A` matches `x == x` but not `x == y`.
164
179
  {{/ifAny}}
165
180
  {{#if eagerTasks}}
166
181
  <eager-tasks>
@@ -244,7 +259,6 @@ Justify sequential work; default parallel. Cannot articulate why B depends on A
244
259
  - You **MUST NOT** yield without proof when non-trivial work, self-assessment is deceptive: tests, linters, type checks, repro steps… exhaust all external verification.
245
260
  ## 8. Handoff
246
261
  Before finishing, you **MUST**:
247
- - List all commands run and confirm they passed.
248
262
  - Summarize changes with file and line references.
249
263
  - Call out TODOs, follow-up work, or uncertainties — no surprises are **PERMITTED**.
250
264
 
@@ -0,0 +1,33 @@
1
+ Performs structural AST-aware rewrites via native ast-grep.
2
+
3
+ <instruction>
4
+ - Use for codemods and structural rewrites where plain text replace is unsafe
5
+ - Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns)
6
+ - Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path` narrow
7
+ - Always returns a preview; after reviewing, call `resolve` with `action: "apply"` or `action: "discard"`
8
+ - Treat parse issues as a scoping signal: tighten `path`/`lang` before retrying
9
+ - Metavariables captured in each rewrite pattern (`$A`, `$$$ARGS`) are substituted into that entry's rewrite template
10
+ - Each matched rewrite is a 1:1 structural substitution; you cannot split one capture into multiple nodes or merge multiple captures into one node
11
+ </instruction>
12
+
13
+ <output>
14
+ - Returns replacement summary, per-file replacement counts, and change previews
15
+ - Reports whether changes were applied or only previewed
16
+ - Includes parse issues when files cannot be processed
17
+ </output>
18
+
19
+ <examples>
20
+ - Rename a call site across a directory, preview first:
21
+ `{"ops":[{"pat":"oldApi($$$ARGS)","out":"newApi($$$ARGS)"}],"lang":"typescript","path":"src/"}`
22
+ - Multi-op codemod preview before resolving:
23
+ `{"ops":[{"pat":"require($A)","out":"import $A"},{"pat":"module.exports = $E","out":"export default $E"}],"lang":"javascript","path":"src/"}`
24
+ - Swap two arguments using captures:
25
+ `{"ops":[{"pat":"assertEqual($A, $B)","out":"assertEqual($B, $A)"}],"lang":"typescript","path":"tests/"}`
26
+ </examples>
27
+
28
+ <critical>
29
+ - `ops` **MUST** contain at least one concrete `{ pat, out }` entry
30
+ - If the path pattern spans multiple languages, set `lang` explicitly for deterministic rewrites
31
+ - Review preview output, then use the `resolve` tool to apply or discard (with a reason)
32
+ - For one-off local text edits, prefer the Edit tool instead of AST edit
33
+ </critical>
@@ -0,0 +1,34 @@
1
+ Performs structural code search using AST matching via native ast-grep.
2
+
3
+ <instruction>
4
+ - Use this when syntax shape matters more than raw text (calls, declarations, specific language constructs)
5
+ - Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns)
6
+ - Default to language-scoped search in mixed repositories: pair `path` glob + explicit `lang` to avoid parse-noise from non-source files
7
+ - `patterns` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
8
+ - Multiple patterns run in one native pass; results are merged and then `offset`/`limit` are applied to the combined match set
9
+ - Use `selector` only for contextual pattern mode; otherwise provide direct patterns
10
+ - For variadic arguments/fields, use `$$$NAME` (not `$$NAME`)
11
+ - Patterns match AST structure, not text — whitespace/formatting differences are ignored
12
+ - When the same metavariable appears multiple times, all occurrences must match identical code
13
+ </instruction>
14
+
15
+ <output>
16
+ - Returns grouped matches with file path, byte range, line/column ranges, and metavariable captures
17
+ - Includes summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
18
+ </output>
19
+
20
+ <examples>
21
+ - Find all console logging calls in one pass (multi-pattern, scoped):
22
+ `{"patterns":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
23
+ - Capture and inspect metavariable bindings from a pattern:
24
+ `{"patterns":["require($MOD)"],"lang":"javascript","path":"src/"}`
25
+ - Contextual pattern with selector — match only the identifier `foo`, not the whole call:
26
+ `{"patterns":["foo()"],"selector":"identifier","lang":"typescript","path":"src/utils.ts"}`
27
+ </examples>
28
+
29
+ <critical>
30
+ - `patterns` is required
31
+ - Set `lang` explicitly to constrain matching when path pattern spans mixed-language trees
32
+ - Avoid repo-root AST scans when the target is language-specific; narrow `path` first
33
+ - If exploration is broad/open-ended across subsystems, use Task tool with explore subagent first
34
+ </critical>
@@ -35,8 +35,8 @@ You **MUST** use specialized tools instead of bash for ALL file operations:
35
35
  |`ls dir/`|`read(path="dir/")`|
36
36
  |`cat <<'EOF' > file`|`write(path="file", content="...")`|
37
37
  |`sed -i 's/old/new/' file`|`edit(path="file", edits=[...])`|
38
- - If `ast_find` / `ast_replace` tools are available in the session, you **MUST** use them for structural code search/rewrites instead of bash `grep`/`sed`/`awk`/`perl` pipelines
39
- - Bash is for command execution, not syntax-aware code transformation; prefer `ast_find` for discovery and `ast_replace` for codemods
38
+ - If `ast_grep` / `ast_edit` tools are available in the session, you **MUST** use them for structural code search/rewrites instead of bash `grep`/`sed`/`awk`/`perl` pipelines
39
+ - Bash is for command execution, not syntax-aware code transformation; prefer `ast_grep` for discovery and `ast_edit` for codemods
40
40
  - You **MUST NOT** use Bash for these operations like read, grep, find, edit, write, where specialized tools exist.
41
41
  - You **MUST NOT** use `2>&1` | `2>/dev/null` pattern, stdout and stderr are already merged.
42
42
  - You **MUST NOT** use `| head -n 50` or `| tail -n 100` pattern, use `head` and `tail` parameters instead.
@@ -247,4 +247,5 @@ Good — anchors to structural line:
247
247
  - Every tag **MUST** be copied exactly from fresh tool result as `N#ID`.
248
248
  - You **MUST** re-read after each edit call before issuing another on same file.
249
249
  - Formatting is a batch operation. You **MUST** never use this tool for formatting.
250
+ - `lines` entries **MUST** be literal file content with real space indentation. (`\\t` in JSON inserts a literal backslash-t into the file, not a tab.)
250
251
  </critical>
@@ -0,0 +1,8 @@
1
+ Resolves a pending preview action by either applying or discarding it.
2
+ - `action` is required:
3
+ - `"apply"` persists the pending changes.
4
+ - `"discard"` rejects the pending changes.
5
+ - `reason` is required and must explain why you chose to apply or discard.
6
+
7
+ This tool is only valid when a pending action exists (typically after a preview step).
8
+ If no pending action exists, the call fails with an error.
package/src/sdk.ts CHANGED
@@ -84,9 +84,11 @@ import {
84
84
  FindTool,
85
85
  GrepTool,
86
86
  getSearchTools,
87
+ HIDDEN_TOOLS,
87
88
  loadSshTool,
88
89
  PythonTool,
89
90
  ReadTool,
91
+ ResolveTool,
90
92
  setPreferredImageProvider,
91
93
  setPreferredSearchProvider,
92
94
  type Tool,
@@ -96,6 +98,8 @@ import {
96
98
  } from "./tools";
97
99
  import { ToolContextStore } from "./tools/context";
98
100
  import { getGeminiImageTools } from "./tools/gemini-image";
101
+ import { wrapToolWithMetaNotice } from "./tools/output-meta";
102
+ import { PendingActionStore } from "./tools/pending-action";
99
103
  import { EventBus } from "./utils/event-bus";
100
104
 
101
105
  // Types
@@ -205,13 +209,7 @@ export type { PromptTemplate } from "./config/prompt-templates";
205
209
  export { Settings, type SkillsSettings } from "./config/settings";
206
210
  export type { CustomCommand, CustomCommandFactory } from "./extensibility/custom-commands/types";
207
211
  export type { CustomTool, CustomToolFactory } from "./extensibility/custom-tools/types";
208
- export type {
209
- ExtensionAPI,
210
- ExtensionCommandContext,
211
- ExtensionContext,
212
- ExtensionFactory,
213
- ToolDefinition,
214
- } from "./extensibility/extensions";
212
+ export type * from "./extensibility/extensions";
215
213
  export type { Skill } from "./extensibility/skills";
216
214
  export type { FileSlashCommand } from "./extensibility/slash-commands";
217
215
  export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
@@ -222,6 +220,7 @@ export {
222
220
  BashTool,
223
221
  // Tool classes and factories
224
222
  BUILTIN_TOOLS,
223
+ HIDDEN_TOOLS,
225
224
  createTools,
226
225
  EditTool,
227
226
  FindTool,
@@ -229,6 +228,7 @@ export {
229
228
  loadSshTool,
230
229
  PythonTool,
231
230
  ReadTool,
231
+ ResolveTool,
232
232
  WriteTool,
233
233
  type ToolSession,
234
234
  };
@@ -776,6 +776,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
776
776
  })
777
777
  : undefined;
778
778
 
779
+ const pendingActionStore = new PendingActionStore();
779
780
  const toolSession: ToolSession = {
780
781
  cwd,
781
782
  hasUI: options.hasUI ?? false,
@@ -816,6 +817,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
816
817
  authStorage,
817
818
  modelRegistry,
818
819
  asyncJobManager,
820
+ pendingActionStore,
819
821
  };
820
822
 
821
823
  // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, local://)
@@ -924,6 +926,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
924
926
  [],
925
927
  cwd,
926
928
  builtInToolNames,
929
+ pendingActionStore,
927
930
  );
928
931
  for (const { path, error } of discoveredCustomTools.errors) {
929
932
  logger.error("Custom tool load failed", { path, error });
@@ -1109,6 +1112,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1109
1112
  toolRegistry.delete("edit");
1110
1113
  }
1111
1114
 
1115
+ const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
1116
+ if (!hasDeferrableTools) {
1117
+ toolRegistry.delete("resolve");
1118
+ } else if (!toolRegistry.has("resolve")) {
1119
+ const resolveTool = await logger.timeAsync("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
1120
+ if (resolveTool) {
1121
+ toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool) as AgentTool);
1122
+ }
1123
+ }
1124
+
1112
1125
  let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
1113
1126
  const cursorExecHandlers = new CursorExecHandlers({
1114
1127
  cwd,
@@ -1307,6 +1320,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1307
1320
  return result;
1308
1321
  },
1309
1322
  intentTracing: !!intentField,
1323
+ getToolChoice: () => {
1324
+ if (pendingActionStore.hasPending) {
1325
+ return { type: "function", name: "resolve" };
1326
+ }
1327
+ return undefined;
1328
+ },
1310
1329
  });
1311
1330
  cursorEventEmitter = event => agent.emitExternalEvent(event);
1312
1331
 
@@ -1343,6 +1362,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1343
1362
  forceCopilotAgentInitiator,
1344
1363
  obfuscator,
1345
1364
  asyncJobManager,
1365
+ pendingActionStore,
1346
1366
  });
1347
1367
 
1348
1368
  if (model?.api === "openai-codex-responses") {
@@ -86,6 +86,7 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
86
86
  import type { SecretObfuscator } from "../secrets/obfuscator";
87
87
  import { outputMeta } from "../tools/output-meta";
88
88
  import { resolveToCwd } from "../tools/path-utils";
89
+ import type { PendingActionStore } from "../tools/pending-action";
89
90
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
90
91
  import { parseCommandArgs } from "../utils/command-args";
91
92
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -177,6 +178,8 @@ export interface AgentSessionConfig {
177
178
  forceCopilotAgentInitiator?: boolean;
178
179
  /** Secret obfuscator for deobfuscating streaming edit content */
179
180
  obfuscator?: SecretObfuscator;
181
+ /** Pending action store for preview/apply workflows */
182
+ pendingActionStore?: PendingActionStore;
180
183
  }
181
184
 
182
185
  /** Options for AgentSession.prompt() */
@@ -368,6 +371,7 @@ export class AgentSession {
368
371
  #streamingEditFileCache = new Map<string, string>();
369
372
  #promptInFlight = false;
370
373
  #obfuscator: SecretObfuscator | undefined;
374
+ #pendingActionStore: PendingActionStore | undefined;
371
375
  #promptGeneration = 0;
372
376
  #providerSessionState = new Map<string, ProviderSessionState>();
373
377
 
@@ -392,6 +396,7 @@ export class AgentSession {
392
396
  this.#forceCopilotAgentInitiator = config.forceCopilotAgentInitiator ?? false;
393
397
  this.#obfuscator = config.obfuscator;
394
398
  this.agent.providerSessionState = this.#providerSessionState;
399
+ this.#pendingActionStore = config.pendingActionStore;
395
400
  this.#syncTodoPhasesFromBranch();
396
401
 
397
402
  // Always subscribe to agent events for internal handling
@@ -651,17 +656,12 @@ export class AgentSession {
651
656
  }
652
657
 
653
658
  if (event.message.role === "toolResult") {
654
- const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
659
+ const { toolName, details, isError, content } = event.message as {
655
660
  toolName?: string;
656
- toolCallId?: string;
657
661
  details?: { path?: string; phases?: TodoPhase[] };
658
- $normative?: Record<string, unknown>;
659
662
  isError?: boolean;
660
663
  content?: Array<TextContent | ImageContent>;
661
664
  };
662
- if ($normative && toolCallId && this.settings.get("normativeRewrite")) {
663
- await this.#rewriteToolCallArgs(toolCallId, $normative);
664
- }
665
665
  // Invalidate streaming edit cache when edit tool completes to prevent stale data
666
666
  if (toolName === "edit" && details?.path) {
667
667
  this.#invalidateFileCacheForPath(details.path);
@@ -688,6 +688,22 @@ export class AgentSession {
688
688
  { deliverAs: "nextTurn" },
689
689
  );
690
690
  }
691
+ if (!isError && this.#pendingActionStore?.hasPending) {
692
+ const reminderText = [
693
+ "<system-reminder>",
694
+ "This is a preview. Call the `resolve` tool to apply or discard these changes.",
695
+ "</system-reminder>",
696
+ ].join("\n");
697
+ await this.sendCustomMessage(
698
+ {
699
+ customType: "resolve-reminder",
700
+ content: reminderText,
701
+ display: false,
702
+ details: { toolName },
703
+ },
704
+ { deliverAs: "nextTurn" },
705
+ );
706
+ }
691
707
  }
692
708
  }
693
709
 
@@ -1259,33 +1275,6 @@ export class AgentSession {
1259
1275
  }
1260
1276
  }
1261
1277
 
1262
- /** Rewrite tool call arguments in agent state and persisted session history. */
1263
- async #rewriteToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<void> {
1264
- let updated = false;
1265
- const messages = this.agent.state.messages;
1266
- for (let i = messages.length - 1; i >= 0; i--) {
1267
- const msg = messages[i];
1268
- if (msg.role !== "assistant") continue;
1269
- const assistantMsg = msg as AssistantMessage;
1270
- if (!Array.isArray(assistantMsg.content)) continue;
1271
- for (const block of assistantMsg.content) {
1272
- if (typeof block !== "object" || block === null) continue;
1273
- if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
1274
- const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
1275
- if (toolCall.id === toolCallId) {
1276
- toolCall.arguments = args;
1277
- updated = true;
1278
- break;
1279
- }
1280
- }
1281
- if (updated) break;
1282
- }
1283
-
1284
- if (updated) {
1285
- await this.sessionManager.rewriteAssistantToolCallArgs(toolCallId, args);
1286
- }
1287
- }
1288
-
1289
1278
  /** Emit extension events based on session events */
1290
1279
  async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
1291
1280
  if (!this.#extensionRunner) return;
@@ -3200,7 +3189,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3200
3189
  this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
3201
3190
  // This handles the case where an error was kept after compaction (in the "kept" region).
3202
3191
  // The error shouldn't trigger another compaction since we already compacted.
3203
- // Example: opus fails \u2192 switch to codex \u2192 compact \u2192 switch back to opus \u2192 opus error
3192
+ // Example: opus fails -> switch to codex -> compact -> switch back to opus -> opus error
3204
3193
  // is still in context but shouldn't trigger compaction again.
3205
3194
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
3206
3195
  const errorIsFromBeforeCompaction =
@@ -3213,7 +3202,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3213
3202
  this.agent.replaceMessages(messages.slice(0, -1));
3214
3203
  }
3215
3204
 
3216
- // Try context promotion first \u2014 switch to a larger model and retry without compacting
3205
+ // Try context promotion first - switch to a larger model and retry without compacting
3217
3206
  const promoted = await this.#tryContextPromotion(assistantMessage);
3218
3207
  if (promoted) {
3219
3208
  // Retry on the promoted (larger) model without compacting
@@ -3221,7 +3210,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3221
3210
  return;
3222
3211
  }
3223
3212
 
3224
- // No promotion target available \u2014 fall through to compaction
3213
+ // No promotion target available fall through to compaction
3225
3214
  const compactionSettings = this.settings.getGroup("compaction");
3226
3215
  if (compactionSettings.enabled) {
3227
3216
  await this.#runAutoCompaction("overflow", true);
@@ -1819,36 +1819,6 @@ export class SessionManager {
1819
1819
  await this.#rewriteFile();
1820
1820
  }
1821
1821
 
1822
- /**
1823
- * Rewrite tool call arguments in the most recent assistant message containing the toolCallId.
1824
- * Returns true if a tool call was updated.
1825
- */
1826
- async rewriteAssistantToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<boolean> {
1827
- let updated = false;
1828
- for (let i = this.#fileEntries.length - 1; i >= 0; i--) {
1829
- const entry = this.#fileEntries[i];
1830
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
1831
- const message = entry.message as { content?: unknown };
1832
- if (!Array.isArray(message.content)) continue;
1833
- for (const block of message.content) {
1834
- if (typeof block !== "object" || block === null) continue;
1835
- if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
1836
- const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
1837
- if (toolCall.id === toolCallId) {
1838
- toolCall.arguments = args;
1839
- updated = true;
1840
- break;
1841
- }
1842
- }
1843
- if (updated) break;
1844
- }
1845
-
1846
- if (updated && this.persist && this.#sessionFile) {
1847
- await this.#rewriteFile();
1848
- }
1849
- return updated;
1850
- }
1851
-
1852
1822
  /**
1853
1823
  * Append a custom message entry (for extensions) that participates in LLM context.
1854
1824
  * @param customType Hook identifier for filtering on reload
@@ -75,8 +75,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
75
75
  {
76
76
  name: "plan",
77
77
  description: "Toggle plan mode (agent plans before executing)",
78
- handle: async (_command, runtime) => {
79
- await runtime.ctx.handlePlanModeCommand();
78
+ inlineHint: "[prompt]",
79
+ allowArgs: true,
80
+ handle: async (command, runtime) => {
81
+ await runtime.ctx.handlePlanModeCommand(command.args || undefined);
80
82
  runtime.ctx.editor.setText("");
81
83
  },
82
84
  },
package/src/stt/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { type DownloadProgress, ensureSTTDependencies } from "./downloader";
2
- export { checkDependencies, formatDependencyStatus, type STTDependencyStatus } from "./setup";
3
- export { STTController, type SttState } from "./stt-controller";
1
+ export * from "./downloader";
2
+ export * from "./setup";
3
+ export * from "./stt-controller";
package/src/task/types.ts CHANGED
@@ -34,8 +34,8 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
34
34
  /** Single task item for parallel execution */
35
35
  export const taskItemSchema = Type.Object({
36
36
  id: Type.String({
37
- description: "CamelCase identifier, max 32 chars",
38
- maxLength: 32,
37
+ description: "CamelCase identifier, max 48 chars",
38
+ maxLength: 48,
39
39
  }),
40
40
  description: Type.String({
41
41
  description: "Short one-liner for UI display only — not seen by the subagent",