@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.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 (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ACP-side `ClientBridge` implementation. Wraps `AgentSideConnection` so the
3
+ * `read`/`write`/`bash`/`edit` tools (and the permission gate in
4
+ * `AgentSession`) can route through the client when it advertises the
5
+ * relevant capabilities at `initialize` time.
6
+ */
7
+ import type {
8
+ PermissionOption as AcpPermissionOption,
9
+ TerminalHandle as AcpTerminalHandle,
10
+ AgentSideConnection,
11
+ ClientCapabilities,
12
+ RequestPermissionRequest,
13
+ ToolCallUpdate,
14
+ } from "@agentclientprotocol/sdk";
15
+ import type {
16
+ ClientBridge,
17
+ ClientBridgeCapabilities,
18
+ ClientBridgeCreateTerminalParams,
19
+ ClientBridgePermissionOption,
20
+ ClientBridgePermissionOutcome,
21
+ ClientBridgePermissionToolCall,
22
+ ClientBridgeTerminalHandle,
23
+ } from "../../session/client-bridge";
24
+
25
+ export function createAcpClientBridge(
26
+ connection: AgentSideConnection,
27
+ sessionId: string,
28
+ clientCapabilities: ClientCapabilities | undefined,
29
+ ): ClientBridge {
30
+ const capabilities: ClientBridgeCapabilities = {
31
+ readTextFile: clientCapabilities?.fs?.readTextFile === true,
32
+ writeTextFile: clientCapabilities?.fs?.writeTextFile === true,
33
+ terminal: clientCapabilities?.terminal === true,
34
+ // Permission requests are always usable on the connection; gating is
35
+ // the agent's policy choice rather than a client capability.
36
+ requestPermission: true,
37
+ };
38
+
39
+ const bridge: ClientBridge = { capabilities };
40
+
41
+ if (capabilities.readTextFile) {
42
+ bridge.readTextFile = async params => {
43
+ const response = await connection.readTextFile({
44
+ sessionId,
45
+ path: params.path,
46
+ ...(typeof params.line === "number" ? { line: params.line } : {}),
47
+ ...(typeof params.limit === "number" ? { limit: params.limit } : {}),
48
+ });
49
+ return response.content;
50
+ };
51
+ }
52
+
53
+ if (capabilities.writeTextFile) {
54
+ bridge.writeTextFile = async params => {
55
+ await connection.writeTextFile({
56
+ sessionId,
57
+ path: params.path,
58
+ content: params.content,
59
+ });
60
+ };
61
+ }
62
+
63
+ if (capabilities.terminal) {
64
+ bridge.createTerminal = (params: ClientBridgeCreateTerminalParams) =>
65
+ createTerminalHandle(connection, sessionId, params);
66
+ }
67
+
68
+ bridge.requestPermission = (toolCall, options, signal) =>
69
+ requestPermission(connection, sessionId, toolCall, options, signal);
70
+
71
+ return bridge;
72
+ }
73
+
74
+ async function createTerminalHandle(
75
+ connection: AgentSideConnection,
76
+ sessionId: string,
77
+ params: ClientBridgeCreateTerminalParams,
78
+ ): Promise<ClientBridgeTerminalHandle> {
79
+ const handle = await connection.createTerminal({
80
+ sessionId,
81
+ command: params.command,
82
+ ...(params.args ? { args: params.args } : {}),
83
+ ...(params.env ? { env: params.env } : {}),
84
+ ...(params.cwd ? { cwd: params.cwd } : {}),
85
+ ...(typeof params.outputByteLimit === "number" ? { outputByteLimit: params.outputByteLimit } : {}),
86
+ });
87
+ return wrapTerminalHandle(handle);
88
+ }
89
+
90
+ function wrapTerminalHandle(handle: AcpTerminalHandle): ClientBridgeTerminalHandle {
91
+ return {
92
+ terminalId: handle.id,
93
+ async currentOutput() {
94
+ const out = await handle.currentOutput();
95
+ return {
96
+ output: out.output,
97
+ truncated: out.truncated,
98
+ exitStatus: out.exitStatus ?? null,
99
+ };
100
+ },
101
+ async waitForExit() {
102
+ const status = await handle.waitForExit();
103
+ return { exitCode: status.exitCode ?? null, signal: status.signal ?? null };
104
+ },
105
+ async kill() {
106
+ await handle.kill();
107
+ },
108
+ async release() {
109
+ await handle.release();
110
+ },
111
+ };
112
+ }
113
+
114
+ async function requestPermission(
115
+ connection: AgentSideConnection,
116
+ sessionId: string,
117
+ toolCall: ClientBridgePermissionToolCall,
118
+ options: ClientBridgePermissionOption[],
119
+ signal: AbortSignal | undefined,
120
+ ): Promise<ClientBridgePermissionOutcome> {
121
+ const update: ToolCallUpdate = {
122
+ toolCallId: toolCall.toolCallId,
123
+ title: toolCall.title,
124
+ ...(toolCall.kind ? { kind: toolCall.kind as ToolCallUpdate["kind"] } : {}),
125
+ ...(toolCall.rawInput !== undefined ? { rawInput: toolCall.rawInput } : {}),
126
+ ...(toolCall.locations ? { locations: toolCall.locations } : {}),
127
+ };
128
+ const acpOptions: AcpPermissionOption[] = options.map(option => ({
129
+ optionId: option.optionId,
130
+ name: option.name,
131
+ kind: option.kind,
132
+ }));
133
+ const request: RequestPermissionRequest = {
134
+ sessionId,
135
+ toolCall: update,
136
+ options: acpOptions,
137
+ };
138
+ if (signal?.aborted) {
139
+ return { outcome: "cancelled" };
140
+ }
141
+ const response = await connection.requestPermission(request);
142
+ const outcome = response.outcome;
143
+ if (outcome.outcome === "cancelled") {
144
+ return { outcome: "cancelled" };
145
+ }
146
+ const matched = options.find(option => option.optionId === outcome.optionId);
147
+ return {
148
+ outcome: "selected",
149
+ optionId: outcome.optionId,
150
+ ...(matched ? { kind: matched.kind } : {}),
151
+ };
152
+ }
@@ -6,10 +6,24 @@ import type {
6
6
  ToolKind,
7
7
  } from "@agentclientprotocol/sdk";
8
8
  import type { AgentSessionEvent } from "../../session/agent-session";
9
+ import { resolveToCwd } from "../../tools/path-utils";
9
10
  import type { TodoStatus } from "../../tools/todo-write";
10
11
 
12
+ interface MessageProgress {
13
+ textEmitted: boolean;
14
+ thoughtEmitted: boolean;
15
+ }
16
+
11
17
  interface AcpEventMapperOptions {
12
18
  getMessageId?: (message: unknown) => string | undefined;
19
+ getMessageProgress?: (message: unknown) => MessageProgress | undefined;
20
+ /**
21
+ * Session cwd. Tool call locations sent to ACP clients must be absolute
22
+ * (the editor host needs them to open or focus files). When provided,
23
+ * the mapper resolves raw `path`/`file`/etc. args against this cwd
24
+ * before emitting `ToolCallLocation` entries.
25
+ */
26
+ cwd?: string;
13
27
  }
14
28
 
15
29
  interface ContentArrayContainer {
@@ -127,6 +141,8 @@ export function mapAgentSessionEventToAcpSessionUpdates(
127
141
  switch (event.type) {
128
142
  case "message_update":
129
143
  return mapAssistantMessageUpdate(event, sessionId, options);
144
+ case "message_end":
145
+ return mapAssistantMessageEnd(event, sessionId, options);
130
146
  case "tool_execution_start": {
131
147
  const update: SessionUpdate = {
132
148
  sessionUpdate: "tool_call",
@@ -136,14 +152,16 @@ export function mapAgentSessionEventToAcpSessionUpdates(
136
152
  status: "pending",
137
153
  rawInput: event.args,
138
154
  };
139
- const locations = extractToolLocations(event.args);
155
+ const locations = extractToolLocations(event.args, options.cwd);
140
156
  if (locations.length > 0) {
141
157
  update.locations = locations;
142
158
  }
143
159
  return [toSessionNotification(sessionId, update)];
144
160
  }
145
161
  case "tool_execution_update": {
146
- const content = extractToolCallContent(event.partialResult);
162
+ const terminalContent = extractTerminalToolCallContent(event.partialResult);
163
+ const otherContent = terminalContent.length > 0 ? [] : extractToolCallContent(event.partialResult);
164
+ const content = [...terminalContent, ...otherContent];
147
165
  const update: SessionUpdate = {
148
166
  sessionUpdate: "tool_call_update",
149
167
  toolCallId: event.toolCallId,
@@ -153,10 +171,17 @@ export function mapAgentSessionEventToAcpSessionUpdates(
153
171
  if (content.length > 0) {
154
172
  update.content = content;
155
173
  }
174
+ const locations = extractToolLocations(event.args, options.cwd);
175
+ if (locations.length > 0) {
176
+ update.locations = locations;
177
+ }
156
178
  return [toSessionNotification(sessionId, update)];
157
179
  }
158
180
  case "tool_execution_end": {
159
- const content = extractToolCallContent(event.result);
181
+ const diffContent = extractDiffToolCallContent(event.result);
182
+ const terminalContent = extractTerminalToolCallContent(event.result);
183
+ const otherContent = extractToolCallContent(event.result);
184
+ const content = [...diffContent, ...terminalContent, ...otherContent];
160
185
  const update: SessionUpdate = {
161
186
  sessionUpdate: "tool_call_update",
162
187
  toolCallId: event.toolCallId,
@@ -166,6 +191,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
166
191
  if (content.length > 0) {
167
192
  update.content = content;
168
193
  }
194
+ const locations = extractToolLocationsFromResult(event.result, options.cwd);
195
+ if (locations.length > 0) {
196
+ update.locations = locations;
197
+ }
169
198
  return [toSessionNotification(sessionId, update)];
170
199
  }
171
200
  case "todo_reminder": {
@@ -194,14 +223,31 @@ function mapAssistantMessageUpdate(
194
223
 
195
224
  let sessionUpdate: "agent_message_chunk" | "agent_thought_chunk";
196
225
  let text: string;
226
+ const progress = options.getMessageProgress?.(event.message);
197
227
  switch (event.assistantMessageEvent.type) {
198
228
  case "text_delta":
199
229
  sessionUpdate = "agent_message_chunk";
200
230
  text = event.assistantMessageEvent.delta;
231
+ if (text.length > 0 && progress) {
232
+ progress.textEmitted = true;
233
+ }
201
234
  break;
202
235
  case "thinking_delta":
203
236
  sessionUpdate = "agent_thought_chunk";
204
237
  text = event.assistantMessageEvent.delta;
238
+ if (text.length > 0 && progress) {
239
+ progress.thoughtEmitted = true;
240
+ }
241
+ break;
242
+ case "done":
243
+ if (progress?.textEmitted) {
244
+ return [];
245
+ }
246
+ sessionUpdate = "agent_message_chunk";
247
+ text = extractAssistantMessageText(event.assistantMessageEvent.message);
248
+ if (text.length > 0 && progress) {
249
+ progress.textEmitted = true;
250
+ }
205
251
  break;
206
252
  case "error":
207
253
  sessionUpdate = "agent_message_chunk";
@@ -224,6 +270,33 @@ function mapAssistantMessageUpdate(
224
270
  ];
225
271
  }
226
272
 
273
+ function mapAssistantMessageEnd(
274
+ event: Extract<AgentSessionEvent, { type: "message_end" }>,
275
+ sessionId: string,
276
+ options: AcpEventMapperOptions,
277
+ ): SessionNotification[] {
278
+ if (!isAssistantMessage(event.message)) {
279
+ return [];
280
+ }
281
+ const progress = options.getMessageProgress?.(event.message);
282
+ if (!progress || progress.textEmitted) {
283
+ return [];
284
+ }
285
+ const text = extractAssistantMessageText(event.message);
286
+ if (text.length === 0) {
287
+ return [];
288
+ }
289
+ progress.textEmitted = true;
290
+ const messageId = options.getMessageId?.(event.message);
291
+ return [
292
+ toSessionNotification(sessionId, {
293
+ sessionUpdate: "agent_message_chunk",
294
+ content: { type: "text", text },
295
+ messageId,
296
+ }),
297
+ ];
298
+ }
299
+
227
300
  function toSessionNotification(sessionId: string, update: SessionUpdate): SessionNotification {
228
301
  return { sessionId, update };
229
302
  }
@@ -257,26 +330,104 @@ function buildToolTitle(toolName: string, args: unknown, intent: string | undefi
257
330
  return toolName;
258
331
  }
259
332
 
260
- function extractToolLocations(args: unknown): ToolCallLocation[] {
333
+ /**
334
+ * Resolve a single raw path against cwd for an ACP location. When `cwd` is
335
+ * omitted we pass the value through unchanged (callers without session
336
+ * context, e.g. some legacy entry points and tests); the ACP-side caller
337
+ * always supplies cwd so notifications carry absolute paths.
338
+ */
339
+ function toAcpLocationPath(value: string, cwd?: string): string {
340
+ if (!cwd) return value;
341
+ try {
342
+ return resolveToCwd(value, cwd);
343
+ } catch {
344
+ return value;
345
+ }
346
+ }
347
+
348
+ function extractToolLocations(args: unknown, cwd?: string): ToolCallLocation[] {
261
349
  const locations: ToolCallLocation[] = [];
262
- const path = extractStringProperty<PathContainer>(args, "path");
263
- if (path) {
350
+ const seen = new Set<string>();
351
+ const pushPath = (raw: string | undefined) => {
352
+ if (!raw) return;
353
+ const path = toAcpLocationPath(raw, cwd);
354
+ if (seen.has(path)) return;
355
+ seen.add(path);
264
356
  locations.push({ path });
265
- }
357
+ };
266
358
 
267
- const oldPath = extractStringProperty<OldPathContainer>(args, "oldPath");
268
- if (oldPath && oldPath !== path) {
269
- locations.push({ path: oldPath });
270
- }
359
+ pushPath(extractStringProperty<PathContainer>(args, "path"));
360
+ pushPath(extractStringProperty<OldPathContainer>(args, "oldPath"));
361
+ pushPath(extractStringProperty<NewPathContainer>(args, "newPath"));
271
362
 
272
- const newPath = extractStringProperty<NewPathContainer>(args, "newPath");
273
- if (newPath && newPath !== path && newPath !== oldPath) {
274
- locations.push({ path: newPath });
275
- }
363
+ return locations;
364
+ }
276
365
 
366
+ /** Pull locations from a tool result's details (e.g. EditToolDetails.perFileResults[].path). */
367
+ function extractToolLocationsFromResult(result: unknown, cwd?: string): ToolCallLocation[] {
368
+ if (typeof result !== "object" || result === null) return [];
369
+ const details = (result as { details?: unknown }).details;
370
+ if (typeof details !== "object" || details === null) return [];
371
+ const direct = extractToolLocations(details, cwd);
372
+ const perFile = (details as { perFileResults?: unknown }).perFileResults;
373
+ if (!Array.isArray(perFile)) {
374
+ return direct;
375
+ }
376
+ const seen = new Set(direct.map(loc => loc.path));
377
+ const locations = [...direct];
378
+ for (const entry of perFile) {
379
+ const raw = extractStringProperty<PathContainer>(entry, "path");
380
+ if (!raw) continue;
381
+ const path = toAcpLocationPath(raw, cwd);
382
+ if (seen.has(path)) continue;
383
+ seen.add(path);
384
+ locations.push({ path });
385
+ }
277
386
  return locations;
278
387
  }
279
388
 
389
+ /** Emit a `diff` ToolCallContent for each per-file edit result that carries oldText/newText. */
390
+ function extractDiffToolCallContent(result: unknown): ToolCallContent[] {
391
+ if (typeof result !== "object" || result === null) return [];
392
+ const details = (result as { details?: unknown }).details;
393
+ if (typeof details !== "object" || details === null) return [];
394
+ const blocks: ToolCallContent[] = [];
395
+ const perFile = (details as { perFileResults?: unknown }).perFileResults;
396
+ const entries: unknown[] = Array.isArray(perFile) ? perFile : [details];
397
+ for (const entry of entries) {
398
+ const block = buildDiffContent(entry);
399
+ if (block) blocks.push(block);
400
+ }
401
+ return blocks;
402
+ }
403
+
404
+ function buildDiffContent(entry: unknown): ToolCallContent | undefined {
405
+ if (typeof entry !== "object" || entry === null) return undefined;
406
+ const candidate = entry as { path?: unknown; oldText?: unknown; newText?: unknown; isError?: unknown };
407
+ if (candidate.isError === true) return undefined;
408
+ const path = typeof candidate.path === "string" && candidate.path.length > 0 ? candidate.path : undefined;
409
+ if (!path) return undefined;
410
+ const oldText = typeof candidate.oldText === "string" ? candidate.oldText : undefined;
411
+ const newText = typeof candidate.newText === "string" ? candidate.newText : undefined;
412
+ if (oldText === undefined && newText === undefined) return undefined;
413
+ return {
414
+ type: "diff",
415
+ path,
416
+ oldText: oldText ?? null,
417
+ newText: newText ?? "",
418
+ };
419
+ }
420
+
421
+ /** Emit a `terminal` ToolCallContent when a tool result carries a `details.terminalId` (e.g. bash routed through ACP terminal/*). */
422
+ function extractTerminalToolCallContent(result: unknown): ToolCallContent[] {
423
+ if (typeof result !== "object" || result === null) return [];
424
+ const details = (result as { details?: unknown }).details;
425
+ if (typeof details !== "object" || details === null) return [];
426
+ const terminalId = (details as { terminalId?: unknown }).terminalId;
427
+ if (typeof terminalId !== "string" || terminalId.length === 0) return [];
428
+ return [{ type: "terminal", terminalId }];
429
+ }
430
+
280
431
  function extractToolCallContent(value: unknown): ToolCallContent[] {
281
432
  const richContent = extractStructuredToolCallContent(value);
282
433
  const fallbackText = extractReadableText(value);
@@ -479,6 +630,20 @@ function extractReadableText(value: unknown): string | undefined {
479
630
  return normalizeText(serialized);
480
631
  }
481
632
 
633
+ function extractAssistantMessageText(value: unknown): string {
634
+ if (typeof value !== "object" || value === null || !("content" in value)) {
635
+ return "";
636
+ }
637
+ const content = (value as ContentArrayContainer).content;
638
+ if (!Array.isArray(content)) {
639
+ return "";
640
+ }
641
+ return content
642
+ .map(block => extractStructuredText(block))
643
+ .filter((chunk): chunk is string => typeof chunk === "string" && chunk.length > 0)
644
+ .join("\n");
645
+ }
646
+
482
647
  function extractStructuredText(value: unknown): string | undefined {
483
648
  const text = extractStringProperty<TextLikeContent>(value, "text");
484
649
  if (!text) {
@@ -0,0 +1,37 @@
1
+ export const ACP_TERMINAL_AUTH_FLAG = "--acp-terminal-auth";
2
+
3
+ export interface AcpTerminalAuthArgs {
4
+ args: string[];
5
+ terminalAuth: boolean;
6
+ }
7
+
8
+ export function prepareAcpTerminalAuthArgs(rawArgs: readonly string[]): AcpTerminalAuthArgs {
9
+ const withoutAuthFlag: string[] = [];
10
+ let terminalAuth = false;
11
+ for (const arg of rawArgs) {
12
+ if (arg === ACP_TERMINAL_AUTH_FLAG) {
13
+ terminalAuth = true;
14
+ continue;
15
+ }
16
+ withoutAuthFlag.push(arg);
17
+ }
18
+
19
+ if (!terminalAuth) {
20
+ return { args: withoutAuthFlag, terminalAuth: false };
21
+ }
22
+
23
+ const args: string[] = [];
24
+ for (let i = 0; i < withoutAuthFlag.length; i++) {
25
+ const arg = withoutAuthFlag[i];
26
+ if (arg === "--mode") {
27
+ i++;
28
+ continue;
29
+ }
30
+ if (arg.startsWith("--mode=")) {
31
+ continue;
32
+ }
33
+ args.push(arg);
34
+ }
35
+
36
+ return { args, terminalAuth: true };
37
+ }
@@ -3,8 +3,8 @@ import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } fro
3
3
  import { formatNumber } from "@oh-my-pi/pi-utils";
4
4
  import { settings } from "../../config/settings";
5
5
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
6
+ import { isSilentAbort } from "../../session/messages";
6
7
  import { resolveImageOptions } from "../../tools/render-utils";
7
- import { convertToPng } from "../../utils/image-convert";
8
8
 
9
9
  /**
10
10
  * Component that renders a complete assistant message
@@ -76,14 +76,15 @@ export class AssistantMessageComponent extends Container {
76
76
  const key = `${toolCallId}:${index}`;
77
77
  if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
78
78
  this.#kittyConversionsInFlight.add(key);
79
- convertToPng(image.data, image.mimeType)
80
- .then(converted => {
79
+ new Bun.Image(Buffer.from(image.data, "base64"))
80
+ .png()
81
+ .toBase64()
82
+ .then(data => {
81
83
  this.#kittyConversionsInFlight.delete(key);
82
- if (!converted) return;
83
84
  this.#convertedKittyImages.set(key, {
84
85
  type: "image",
85
- data: converted.data,
86
- mimeType: converted.mimeType,
86
+ data,
87
+ mimeType: "image/png",
87
88
  });
88
89
  if (this.#lastMessage) {
89
90
  this.updateContent(this.#lastMessage);
@@ -184,7 +185,7 @@ export class AssistantMessageComponent extends Container {
184
185
  // But only if there are no tool calls (tool execution components will show the error)
185
186
  const hasToolCalls = message.content.some(c => c.type === "toolCall");
186
187
  if (!hasToolCalls) {
187
- if (message.stopReason === "aborted") {
188
+ if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
188
189
  const abortMessage =
189
190
  message.errorMessage && message.errorMessage !== "Request was aborted"
190
191
  ? message.errorMessage
@@ -201,7 +202,12 @@ export class AssistantMessageComponent extends Container {
201
202
  this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
202
203
  }
203
204
  }
204
- if (message.errorMessage && message.stopReason !== "aborted" && message.stopReason !== "error") {
205
+ if (
206
+ message.errorMessage &&
207
+ !isSilentAbort(message.errorMessage) &&
208
+ message.stopReason !== "aborted" &&
209
+ message.stopReason !== "error"
210
+ ) {
205
211
  this.#contentContainer.addChild(new Spacer(1));
206
212
  this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
207
213
  }
@@ -7,19 +7,23 @@ import {
7
7
  Container,
8
8
  Ellipsis,
9
9
  ImageProtocol,
10
- Loader,
11
- Spacer,
10
+ type Loader,
12
11
  TERMINAL,
13
12
  Text,
14
13
  type TUI,
15
14
  truncateToWidth,
16
15
  visibleWidth,
17
16
  } from "@oh-my-pi/pi-tui";
18
- import { getSymbolTheme, theme } from "../../modes/theme/theme";
19
- import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
17
+ import { theme } from "../../modes/theme/theme";
18
+ import type { TruncationMeta } from "../../tools/output-meta";
20
19
  import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
21
- import { DynamicBorder } from "./dynamic-border";
22
- import { truncateToVisualLines } from "./visual-truncate";
20
+ import {
21
+ buildExecutionFrame,
22
+ buildStatusFooter,
23
+ createCollapsedPreview,
24
+ type ExecutionStatus,
25
+ resolveExecutionStatus,
26
+ } from "./execution-shared";
23
27
 
24
28
  // Preview line limit when not expanded (matches tool execution behavior)
25
29
  const PREVIEW_LINES = 20;
@@ -31,7 +35,7 @@ const CHUNK_THROTTLE_MS = 50;
31
35
 
32
36
  export class BashExecutionComponent extends Container {
33
37
  #outputLines: string[] = [];
34
- #status: "running" | "complete" | "cancelled" | "error" = "running";
38
+ #status: ExecutionStatus = "running";
35
39
  #exitCode: number | undefined = undefined;
36
40
  #loader: Loader;
37
41
  #truncation?: TruncationMeta;
@@ -50,34 +54,14 @@ export class BashExecutionComponent extends Container {
50
54
 
51
55
  // Use dim border for excluded-from-context commands (!! prefix)
52
56
  const colorKey = excludeFromContext ? "dim" : "bashMode";
53
- const borderColor = (str: string) => theme.fg(colorKey, str);
54
-
55
- // Add spacer
56
- this.addChild(new Spacer(1));
57
-
58
- // Top border
59
- this.addChild(new DynamicBorder(borderColor));
60
-
61
- // Content container (holds dynamic content between borders)
62
- this.#contentContainer = new Container();
63
- this.addChild(this.#contentContainer);
57
+ const { contentContainer, loader } = buildExecutionFrame(this, ui, colorKey);
58
+ this.#contentContainer = contentContainer;
59
+ this.#loader = loader;
64
60
 
65
61
  // Command header
66
62
  this.#headerText = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);
67
63
  this.#contentContainer.addChild(this.#headerText);
68
-
69
- // Loader
70
- this.#loader = new Loader(
71
- ui,
72
- spinner => theme.fg(colorKey, spinner),
73
- text => theme.fg("muted", text),
74
- `Running… (esc to cancel)`,
75
- getSymbolTheme().spinnerFrames,
76
- );
77
64
  this.#contentContainer.addChild(this.#loader);
78
-
79
- // Bottom border
80
- this.addChild(new DynamicBorder(borderColor));
81
65
  }
82
66
 
83
67
  /**
@@ -130,11 +114,7 @@ export class BashExecutionComponent extends Container {
130
114
  options?: { output?: string; truncation?: TruncationMeta },
131
115
  ): void {
132
116
  this.#exitCode = exitCode;
133
- this.#status = cancelled
134
- ? "cancelled"
135
- : exitCode !== 0 && exitCode !== undefined && exitCode !== null
136
- ? "error"
137
- : "complete";
117
+ this.#status = resolveExecutionStatus(exitCode, cancelled);
138
118
  this.#truncation = options?.truncation;
139
119
  if (options?.output !== undefined) {
140
120
  this.#setOutput(options.output);
@@ -182,14 +162,7 @@ export class BashExecutionComponent extends Container {
182
162
  } else {
183
163
  // Use shared visual truncation utility, recomputed per render width
184
164
  const styledOutput = previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
185
- const previewText = `\n${styledOutput}`;
186
- this.#contentContainer.addChild({
187
- render: (width: number) => {
188
- const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
189
- return visualLines;
190
- },
191
- invalidate: () => {},
192
- });
165
+ this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
193
166
  }
194
167
  }
195
168
 
@@ -197,26 +170,14 @@ export class BashExecutionComponent extends Container {
197
170
  if (this.#status === "running") {
198
171
  this.#contentContainer.addChild(this.#loader);
199
172
  } else {
200
- const statusParts: string[] = [];
201
-
202
- // Show how many lines are hidden (collapsed preview)
203
- if (hiddenLineCount > 0 && !hasSixelOutput) {
204
- statusParts.push(theme.fg("dim", `… ${hiddenLineCount} more lines (ctrl+o to expand)`));
205
- }
206
-
207
- if (this.#status === "cancelled") {
208
- statusParts.push(theme.fg("warning", "(cancelled)"));
209
- } else if (this.#status === "error") {
210
- statusParts.push(theme.fg("error", `(exit ${this.#exitCode})`));
211
- }
212
-
213
- if (this.#truncation) {
214
- statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
215
- }
216
-
217
- if (statusParts.length > 0) {
218
- this.#contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
219
- }
173
+ const footer = buildStatusFooter({
174
+ status: this.#status,
175
+ exitCode: this.#exitCode,
176
+ truncation: this.#truncation,
177
+ hiddenLineCount,
178
+ suppressHiddenCount: hasSixelOutput,
179
+ });
180
+ if (footer) this.#contentContainer.addChild(footer);
220
181
  }
221
182
  }
222
183