@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -1,7 +1,7 @@
1
1
  import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
2
2
  import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
3
3
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
4
- import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
4
+ import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
5
5
  import { settings } from "../../config/settings";
6
6
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
7
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -17,14 +17,25 @@ import { getSymbolTheme, theme } from "../../modes/theme/theme";
17
17
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
18
18
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
19
19
  import type { AgentSessionEvent } from "../../session/agent-session";
20
- import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
20
+ import { isSilentAbort, readPendingDisplayTag, resolveAbortLabel } from "../../session/messages";
21
21
  import type { ResolveToolDetails } from "../../tools/resolve";
22
22
  import { interruptHint } from "../shared";
23
+ import { StreamingRevealController } from "./streaming-reveal";
23
24
 
24
25
  type AgentSessionEventKind = AgentSessionEvent["type"];
25
26
 
26
27
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
27
28
 
29
+ /**
30
+ * Loader label shown the instant a user interrupt (Esc) is requested, kept until
31
+ * the agent turn fully unwinds. Esc fires the abort synchronously, but the loop
32
+ * only stops the spinner at `agent_end`, which it cannot reach until every
33
+ * in-flight tool settles its abort in `executeToolCalls` (`Promise.allSettled`).
34
+ * Swapping the steady "Working…" for this acknowledges the keypress instead of
35
+ * reading as an ignored Esc for the seconds a slow tool takes to tear down.
36
+ */
37
+ export const INTERRUPTING_WORKING_MESSAGE = "Interrupting…";
38
+
28
39
  // Events that change foreground streaming state, or that reset a turn. The TUI
29
40
  // eager native-scrollback rebuild mode is recomputed only on these so unrelated
30
41
  // IRC/notices/status refreshes do not toggle scrollback replay policy.
@@ -44,12 +55,19 @@ type AgentSessionEventHandlers = {
44
55
 
45
56
  export class EventController {
46
57
  #lastReadGroup: ReadToolGroupComponent | undefined = undefined;
47
- #lastThinkingCount = 0;
58
+ // Count of visible assistant content blocks (rendered non-empty text/thinking)
59
+ // already seen in the current streaming message. A newly appearing one breaks
60
+ // the read run: the rendered reasoning/answer is a visual separator, so reads
61
+ // after it start a fresh group. Empty/absent thinking — common when a model
62
+ // emits one read per completion — does not break it, so a run of consecutive
63
+ // reads collapses into one group even across completion boundaries.
64
+ #lastVisibleBlockCount = 0;
48
65
  #renderedCustomMessages = new Set<string>();
49
66
  #lastIntent: string | undefined = undefined;
50
67
  #backgroundToolCallIds = new Set<string>();
51
68
  #assistantMessageStreaming = false;
52
69
  #agentTurnActive = false;
70
+ #interrupting = false;
53
71
  #readToolCallArgs = new Map<string, Record<string, unknown>>();
54
72
  #readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
55
73
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
@@ -60,9 +78,15 @@ export class EventController {
60
78
  #pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
61
79
  #idleCompactionTimer?: NodeJS.Timeout;
62
80
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
81
+ #streamingReveal: StreamingRevealController;
63
82
  #handlers: AgentSessionEventHandlers;
64
83
 
65
84
  constructor(private ctx: InteractiveModeContext) {
85
+ this.#streamingReveal = new StreamingRevealController({
86
+ getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
87
+ getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
88
+ requestRender: () => this.ctx.ui.requestRender(),
89
+ });
66
90
  this.#handlers = {
67
91
  agent_start: e => this.#handleAgentStart(e),
68
92
  agent_end: e => this.#handleAgentEnd(e),
@@ -95,6 +119,7 @@ export class EventController {
95
119
  }
96
120
 
97
121
  dispose(): void {
122
+ this.#streamingReveal.stop();
98
123
  this.#cancelIdleCompaction();
99
124
  for (const timer of this.#ircExpiryTimers.values()) {
100
125
  clearTimeout(timer);
@@ -103,12 +128,12 @@ export class EventController {
103
128
  }
104
129
 
105
130
  #resetReadGroup(): void {
131
+ this.#lastReadGroup?.finalize();
106
132
  this.#lastReadGroup = undefined;
107
133
  }
108
134
 
109
135
  #getReadGroup(): ReadToolGroupComponent {
110
136
  if (!this.#lastReadGroup) {
111
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
112
137
  const group = new ReadToolGroupComponent({
113
138
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
114
139
  });
@@ -153,6 +178,7 @@ export class EventController {
153
178
  return true;
154
179
  }
155
180
  #updateWorkingMessageFromIntent(intent: unknown): void {
181
+ if (this.#interrupting) return;
156
182
  // Streamed JSON can deliver non-string `_i` (object, number, boolean) before
157
183
  // schema validation; `?.` only guards null/undefined, so guard the type too.
158
184
  if (typeof intent !== "string") return;
@@ -162,6 +188,19 @@ export class EventController {
162
188
  this.ctx.setWorkingMessage(`${trimmed}${interruptHint()}`);
163
189
  }
164
190
 
191
+ /**
192
+ * Acknowledge a user interrupt (Esc) immediately: switch the loader to
193
+ * `INTERRUPTING_WORKING_MESSAGE` and freeze intent-driven working-message
194
+ * updates for the rest of the turn so a late `tool_execution_start` intent
195
+ * cannot repaint a "Working…/<intent>" line over the acknowledgment. Reset at
196
+ * the next `agent_start`. No-op outside an active turn or if already set.
197
+ */
198
+ notifyInterrupting(): void {
199
+ if (!this.#agentTurnActive || this.#interrupting) return;
200
+ this.#interrupting = true;
201
+ this.ctx.setWorkingMessage(INTERRUPTING_WORKING_MESSAGE);
202
+ }
203
+
165
204
  subscribeToAgent(): void {
166
205
  this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
167
206
  await this.handleEvent(event);
@@ -206,9 +245,11 @@ export class EventController {
206
245
 
207
246
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
208
247
  this.#agentTurnActive = true;
248
+ this.#interrupting = false;
209
249
  this.#lastIntent = undefined;
210
250
  this.#readToolCallArgs.clear();
211
251
  this.#readToolCallAssistantComponents.clear();
252
+ this.#resetReadGroup();
212
253
  this.#assistantMessageStreaming = false;
213
254
  this.#lastAssistantComponent = undefined;
214
255
  // Restore the previous turn's inline error in the transcript before dropping
@@ -299,9 +340,8 @@ export class EventController {
299
340
  this.ctx.addMessageToChat(event.message);
300
341
  this.ctx.ui.requestRender();
301
342
  } else if (event.message.role === "assistant") {
302
- this.#lastThinkingCount = 0;
303
343
  this.#assistantMessageStreaming = true;
304
- this.#resetReadGroup();
344
+ this.#lastVisibleBlockCount = 0;
305
345
  this.ctx.streamingComponent = new AssistantMessageComponent(
306
346
  undefined,
307
347
  this.ctx.hideThinkingBlock,
@@ -311,7 +351,7 @@ export class EventController {
311
351
  );
312
352
  this.ctx.streamingMessage = event.message;
313
353
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
314
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
354
+ this.#streamingReveal.begin(this.ctx.streamingComponent, this.ctx.streamingMessage);
315
355
  this.ctx.ui.requestRender();
316
356
  }
317
357
  }
@@ -355,16 +395,17 @@ export class EventController {
355
395
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
356
396
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
357
397
  this.ctx.streamingMessage = event.message;
358
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
398
+ this.#streamingReveal.setTarget(this.ctx.streamingMessage);
359
399
 
360
- const thinkingCount = this.ctx.streamingMessage.content.filter(
361
- content => content.type === "thinking" && content.thinking.trim(),
400
+ const visibleBlockCount = this.ctx.streamingMessage.content.filter(
401
+ content =>
402
+ (content.type === "text" && content.text.trim().length > 0) ||
403
+ (content.type === "thinking" && content.thinking.trim().length > 0),
362
404
  ).length;
363
- if (thinkingCount > this.#lastThinkingCount) {
405
+ if (visibleBlockCount > this.#lastVisibleBlockCount) {
364
406
  this.#resetReadGroup();
365
- this.#lastThinkingCount = thinkingCount;
407
+ this.#lastVisibleBlockCount = visibleBlockCount;
366
408
  }
367
-
368
409
  for (const content of this.ctx.streamingMessage.content) {
369
410
  if (content.type !== "toolCall") continue;
370
411
  if (content.name === "read") {
@@ -397,7 +438,6 @@ export class EventController {
397
438
  : content.arguments;
398
439
  if (!this.ctx.pendingTools.has(content.id)) {
399
440
  this.#resetReadGroup();
400
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
401
441
  const tool = this.ctx.session.getToolByName(content.name);
402
442
  const component = new ToolExecutionComponent(
403
443
  content.name,
@@ -456,21 +496,20 @@ export class EventController {
456
496
  }
457
497
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
458
498
  this.ctx.streamingMessage = event.message;
499
+ this.#streamingReveal.stop();
459
500
  let errorMessage: string | undefined;
460
501
  const aborted = this.ctx.streamingMessage.stopReason === "aborted";
461
502
  const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage.errorMessage);
462
503
  const ttsrSilenced = aborted && this.ctx.session.isTtsrAbortPending;
463
504
  if (aborted && !silentlyAborted && !ttsrSilenced) {
464
- // Real user-cancel / network / provider abort: surface the standard
465
- // operator-facing label. AgentSession.#handleAgentEvent already stamped
466
- // SILENT_ABORT_MARKER for the plan-compact transition before this
467
- // controller ran, so reaching this branch implies the abort was NOT a
468
- // silent internal transition.
469
- const retryAttempt = this.ctx.session.retryAttempt;
470
- errorMessage =
471
- retryAttempt > 0
472
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
473
- : "Operation aborted";
505
+ // Resolve the operator-facing label: a user-interrupt (Esc) abort
506
+ // carries USER_INTERRUPT_LABEL on errorMessage (threaded through the
507
+ // AbortController), which is preserved verbatim; any other abort with
508
+ // no threaded reason falls back to the retry-aware generic label.
509
+ // AgentSession.#handleAgentEvent already stamped SILENT_ABORT_MARKER for
510
+ // the plan-compact transition before this controller ran, so reaching
511
+ // this branch implies the abort was NOT a silent internal transition.
512
+ errorMessage = resolveAbortLabel(this.ctx.streamingMessage.errorMessage, this.ctx.session.retryAttempt);
474
513
  this.ctx.streamingMessage.errorMessage = errorMessage;
475
514
  }
476
515
  if (silentlyAborted || ttsrSilenced) {
@@ -663,6 +702,7 @@ export class EventController {
663
702
  async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
664
703
  this.#agentTurnActive = false;
665
704
  this.#assistantMessageStreaming = false;
705
+ this.#streamingReveal.stop();
666
706
  if (this.ctx.loadingAnimation) {
667
707
  this.ctx.loadingAnimation.stop();
668
708
  this.ctx.loadingAnimation = undefined;
@@ -689,6 +729,7 @@ export class EventController {
689
729
  );
690
730
  this.#readToolCallArgs.clear();
691
731
  this.#readToolCallAssistantComponents.clear();
732
+ this.#resetReadGroup();
692
733
  this.#lastAssistantComponent = undefined;
693
734
  this.ctx.ui.requestRender();
694
735
  this.#scheduleIdleCompaction();
@@ -832,14 +873,12 @@ export class EventController {
832
873
  async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
833
874
  const component = new TtsrNotificationComponent(event.rules);
834
875
  component.setExpanded(this.ctx.toolOutputExpanded);
835
- this.ctx.chatContainer.addChild(component);
836
- this.ctx.ui.requestRender();
876
+ this.ctx.present(component);
837
877
  }
838
878
 
839
879
  async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
840
880
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
841
- this.ctx.chatContainer.addChild(component);
842
- this.ctx.ui.requestRender();
881
+ this.ctx.present(component);
843
882
  }
844
883
 
845
884
  async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
@@ -892,7 +931,6 @@ export class EventController {
892
931
  }
893
932
 
894
933
  sendCompletionNotification(): void {
895
- if (this.ctx.isBackgrounded === false) return;
896
934
  const notify = settings.get("completion.notify");
897
935
  if (notify === "off") return;
898
936
 
@@ -911,15 +949,4 @@ export class EventController {
911
949
  actions: "focus",
912
950
  });
913
951
  }
914
-
915
- async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
916
- if (event.type !== "agent_end") {
917
- return;
918
- }
919
- if (this.ctx.session.queuedMessageCount > 0 || this.ctx.session.isStreaming) {
920
- return;
921
- }
922
- this.sendCompletionNotification();
923
- await this.ctx.shutdown();
924
- }
925
952
  }
@@ -1,6 +1,5 @@
1
1
  import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
2
2
  import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
3
- import { logger } from "@oh-my-pi/pi-utils";
4
3
  import { KeybindingsManager } from "../../config/keybindings";
5
4
  import type {
6
5
  CompactOptions,
@@ -176,10 +175,10 @@ export class ExtensionUiController {
176
175
  this.ctx.streamingMessage = undefined;
177
176
  this.ctx.pendingTools.clear();
178
177
 
179
- this.ctx.chatContainer.addChild(new Spacer(1));
180
- this.ctx.chatContainer.addChild(
178
+ this.ctx.present([
179
+ new Spacer(1),
181
180
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
182
- );
181
+ ]);
183
182
  await this.ctx.reloadTodos();
184
183
  this.ctx.ui.requestRender(true, { clearScrollback: true });
185
184
 
@@ -326,10 +325,6 @@ export class ExtensionUiController {
326
325
  .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
327
326
  .catch((err: unknown) => {
328
327
  const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
329
- if (this.ctx.isBackgrounded) {
330
- logger.error(errorText);
331
- return;
332
- }
333
328
  this.ctx.showError(errorText);
334
329
  });
335
330
  },
@@ -374,9 +369,6 @@ export class ExtensionUiController {
374
369
  getContextUsage: () => this.ctx.session.getContextUsage(),
375
370
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
376
371
  reload: async () => {
377
- if (this.ctx.isBackgrounded) {
378
- return;
379
- }
380
372
  await this.ctx.session.reload();
381
373
  this.ctx.chatContainer.clear();
382
374
  this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
@@ -384,9 +376,6 @@ export class ExtensionUiController {
384
376
  this.ctx.showStatus("Reloaded session");
385
377
  },
386
378
  newSession: async options => {
387
- if (this.ctx.isBackgrounded) {
388
- return { cancelled: true };
389
- }
390
379
  // Stop any loading animation
391
380
  if (this.ctx.loadingAnimation) {
392
381
  this.ctx.loadingAnimation.stop();
@@ -415,19 +404,16 @@ export class ExtensionUiController {
415
404
  this.ctx.streamingMessage = undefined;
416
405
  this.ctx.pendingTools.clear();
417
406
 
418
- this.ctx.chatContainer.addChild(new Spacer(1));
419
- this.ctx.chatContainer.addChild(
407
+ this.ctx.present([
408
+ new Spacer(1),
420
409
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
421
- );
410
+ ]);
422
411
  await this.ctx.reloadTodos();
423
412
  this.ctx.ui.requestRender(true, { clearScrollback: true });
424
413
 
425
414
  return { cancelled: false };
426
415
  },
427
416
  branch: async entryId => {
428
- if (this.ctx.isBackgrounded) {
429
- return { cancelled: true };
430
- }
431
417
  const result = await this.ctx.session.branch(entryId);
432
418
  if (result.cancelled) {
433
419
  return { cancelled: true };
@@ -443,9 +429,6 @@ export class ExtensionUiController {
443
429
  return { cancelled: false };
444
430
  },
445
431
  navigateTree: async (targetId, options) => {
446
- if (this.ctx.isBackgrounded) {
447
- return { cancelled: true };
448
- }
449
432
  const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
450
433
  if (result.cancelled) {
451
434
  return { cancelled: true };
@@ -464,9 +447,6 @@ export class ExtensionUiController {
464
447
  },
465
448
  compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
466
449
  switchSession: async sessionPath => {
467
- if (this.ctx.isBackgrounded) {
468
- return { cancelled: true };
469
- }
470
450
  this.clearHookWidgets();
471
451
  const result = await this.ctx.session.switchSession(sessionPath);
472
452
  if (!result) {
@@ -482,36 +462,6 @@ export class ExtensionUiController {
482
462
  extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
483
463
  }
484
464
 
485
- createBackgroundUiContext(): ExtensionUIContext {
486
- return {
487
- select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined,
488
- confirm: async (_title: string, _message: string, _dialogOptions) => false,
489
- input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
490
- notify: () => {},
491
- onTerminalInput: () => () => {},
492
- setStatus: () => {},
493
- setWorkingMessage: () => {},
494
- setWidget: () => {},
495
- setTitle: () => {},
496
- custom: async () => undefined as never,
497
- setEditorText: () => {},
498
- pasteToEditor: () => {},
499
- getEditorText: () => "",
500
- editor: async () => undefined,
501
- get theme() {
502
- return theme;
503
- },
504
- getAllThemes: () => Promise.resolve([]),
505
- getTheme: () => Promise.resolve(undefined),
506
- setTheme: () => Promise.resolve({ success: false, error: "Background mode" }),
507
- setFooter: () => {},
508
- setHeader: () => {},
509
- setEditorComponent: () => {},
510
- getToolsExpanded: () => false,
511
- setToolsExpanded: () => {},
512
- };
513
- }
514
-
515
465
  /**
516
466
  * Emit session event to all extension tools.
517
467
  */
@@ -531,7 +481,7 @@ export class ExtensionUiController {
531
481
  ui: uiContext,
532
482
  getContextUsage: () => this.ctx.session.getContextUsage(),
533
483
  compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
534
- hasUI: !this.ctx.isBackgrounded,
484
+ hasUI: true,
535
485
  cwd: this.ctx.sessionManager.getCwd(),
536
486
  sessionManager: this.ctx.session.sessionManager,
537
487
  modelRegistry: this.ctx.session.modelRegistry,
@@ -557,22 +507,14 @@ export class ExtensionUiController {
557
507
  * Show a tool error in the chat.
558
508
  */
559
509
  showToolError(toolName: string, error: string): void {
560
- if (this.ctx.isBackgrounded) {
561
- logger.error(`Tool "${toolName}" error: ${error}`);
562
- return;
563
- }
564
510
  const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
565
- this.ctx.chatContainer.addChild(errorText);
566
- this.ctx.ui.requestRender();
511
+ this.ctx.present(errorText);
567
512
  }
568
513
 
569
514
  /**
570
515
  * Set hook status text in the footer.
571
516
  */
572
517
  setHookStatus(key: string, text: string | undefined): void {
573
- if (this.ctx.isBackgrounded) {
574
- return;
575
- }
576
518
  this.ctx.statusLine.setHookStatus(key, text);
577
519
  this.ctx.ui.requestRender();
578
520
  }
@@ -860,14 +802,9 @@ export class ExtensionUiController {
860
802
 
861
803
  showExtensionError(extensionPath: string, error: string): void {
862
804
  const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
863
- this.ctx.chatContainer.addChild(errorText);
864
- this.ctx.ui.requestRender();
805
+ this.ctx.present(errorText);
865
806
  }
866
807
  async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
867
- if (this.ctx.isBackgrounded) {
868
- await this.#compactSession(instructionsOrOptions);
869
- return;
870
- }
871
808
  await this.ctx.executeCompaction(instructionsOrOptions, false);
872
809
  }
873
810
 
@@ -892,7 +829,7 @@ export class ExtensionUiController {
892
829
  #applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void {
893
830
  // For non-streaming cases with display=true, update UI
894
831
  // (streaming cases update via message_end event)
895
- if (!this.ctx.isBackgrounded && !wasStreaming && shouldDisplay) {
832
+ if (!wasStreaming && shouldDisplay) {
896
833
  this.ctx.rebuildChatFromMessages();
897
834
  }
898
835
  }