@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.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 (276) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/dist/cli.js +4387 -4164
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/cli/gc-cli.d.ts +58 -0
  11. package/dist/types/collab/display-name.d.ts +3 -0
  12. package/dist/types/collab/host.d.ts +0 -2
  13. package/dist/types/commands/gc.d.ts +37 -0
  14. package/dist/types/commands/worktree.d.ts +0 -3
  15. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  16. package/dist/types/config/model-discovery.d.ts +6 -1
  17. package/dist/types/config/model-registry.d.ts +6 -2
  18. package/dist/types/config/model-resolver.d.ts +12 -0
  19. package/dist/types/config/models-config-schema.d.ts +29 -2
  20. package/dist/types/config/models-config.d.ts +22 -1
  21. package/dist/types/config/settings-schema.d.ts +145 -21
  22. package/dist/types/config/settings.d.ts +13 -0
  23. package/dist/types/dap/config.d.ts +1 -1
  24. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  25. package/dist/types/edit/renderer.d.ts +4 -0
  26. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  27. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  28. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  29. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  30. package/dist/types/internal-urls/index.d.ts +1 -0
  31. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  32. package/dist/types/internal-urls/router.d.ts +1 -1
  33. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  34. package/dist/types/internal-urls/types.d.ts +19 -2
  35. package/dist/types/irc/bus.d.ts +6 -0
  36. package/dist/types/mcp/transports/stdio.d.ts +25 -1
  37. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  38. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  39. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  40. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  41. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  42. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  43. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  44. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  45. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  46. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  49. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  51. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  52. package/dist/types/modes/interactive-mode.d.ts +10 -1
  53. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  54. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  55. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
  56. package/dist/types/modes/theme/theme.d.ts +2 -1
  57. package/dist/types/modes/types.d.ts +9 -1
  58. package/dist/types/sdk.d.ts +2 -2
  59. package/dist/types/session/agent-session.d.ts +2 -1
  60. package/dist/types/session/session-listing.d.ts +10 -1
  61. package/dist/types/session/session-manager.d.ts +13 -0
  62. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  63. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  64. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  65. package/dist/types/ssh/connection-manager.d.ts +2 -0
  66. package/dist/types/ssh/file-transfer.d.ts +79 -0
  67. package/dist/types/ssh/utils.d.ts +6 -0
  68. package/dist/types/system-prompt.d.ts +5 -0
  69. package/dist/types/task/executor.d.ts +16 -0
  70. package/dist/types/tiny/text.d.ts +1 -1
  71. package/dist/types/tools/builtin-names.d.ts +5 -1
  72. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  73. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  74. package/dist/types/tools/index.d.ts +3 -3
  75. package/dist/types/tools/path-utils.d.ts +29 -0
  76. package/dist/types/tools/plan-mode-guard.d.ts +7 -0
  77. package/dist/types/tools/read.d.ts +2 -2
  78. package/dist/types/tools/render-utils.d.ts +8 -0
  79. package/dist/types/tools/renderers.d.ts +11 -0
  80. package/dist/types/tools/ssh.d.ts +2 -0
  81. package/dist/types/tools/todo.d.ts +0 -16
  82. package/dist/types/tools/write.d.ts +2 -2
  83. package/dist/types/utils/active-repo-context.d.ts +8 -0
  84. package/dist/types/utils/image-resize.d.ts +1 -0
  85. package/dist/types/utils/markit-cache.d.ts +23 -0
  86. package/dist/types/utils/markit.d.ts +5 -1
  87. package/dist/types/utils/prompt-path.d.ts +1 -0
  88. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  89. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  90. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  91. package/dist/types/web/search/providers/xai.d.ts +13 -0
  92. package/dist/types/web/search/types.d.ts +18 -2
  93. package/package.json +30 -15
  94. package/scripts/bench-guard.ts +1 -1
  95. package/scripts/build-binary.ts +9 -9
  96. package/scripts/bundle-dist.ts +2 -2
  97. package/src/advisor/__tests__/advisor.test.ts +40 -4
  98. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  99. package/src/advisor/advise-tool.ts +1 -1
  100. package/src/advisor/emission-guard.ts +172 -0
  101. package/src/advisor/index.ts +1 -0
  102. package/src/advisor/runtime.ts +11 -0
  103. package/src/advisor/watchdog.ts +12 -1
  104. package/src/cli/args.ts +5 -2
  105. package/src/cli/auth-broker-cli.ts +17 -0
  106. package/src/cli/config-cli.ts +4 -0
  107. package/src/cli/flag-tables.ts +7 -4
  108. package/src/cli/gallery-cli.ts +14 -2
  109. package/src/cli/gallery-fixtures/edit.ts +60 -0
  110. package/src/cli/gallery-fixtures/fs.ts +17 -17
  111. package/src/cli/gallery-fixtures/search.ts +4 -4
  112. package/src/cli/gc-cli.ts +939 -0
  113. package/src/cli/usage-cli.ts +20 -1
  114. package/src/cli/web-search-cli.ts +1 -1
  115. package/src/cli-commands.ts +1 -0
  116. package/src/collab/display-name.ts +13 -0
  117. package/src/collab/guest.ts +5 -1
  118. package/src/collab/host.ts +2 -13
  119. package/src/commands/gc.ts +46 -0
  120. package/src/commands/worktree.ts +6 -0
  121. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  122. package/src/config/model-discovery.ts +77 -8
  123. package/src/config/model-registry.ts +89 -11
  124. package/src/config/model-resolver.ts +39 -1
  125. package/src/config/models-config-schema.ts +41 -5
  126. package/src/config/models-config.ts +4 -1
  127. package/src/config/settings-schema.ts +130 -27
  128. package/src/config/settings.ts +216 -7
  129. package/src/cursor.ts +1 -1
  130. package/src/dap/config.ts +152 -8
  131. package/src/dap/session.ts +1 -1
  132. package/src/discovery/helpers.ts +3 -1
  133. package/src/edit/hashline/diff.ts +14 -3
  134. package/src/edit/hashline/execute.ts +42 -6
  135. package/src/edit/hashline/filesystem.ts +49 -8
  136. package/src/edit/index.ts +1 -0
  137. package/src/edit/modes/patch.ts +11 -1
  138. package/src/edit/renderer.ts +140 -13
  139. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  140. package/src/eval/__tests__/julia-prelude.test.ts +18 -0
  141. package/src/eval/agent-bridge.ts +26 -3
  142. package/src/eval/jl/runner.jl +7 -1
  143. package/src/eval/js/tool-bridge.ts +2 -2
  144. package/src/export/html/index.ts +1 -1
  145. package/src/export/html/template.js +3 -1
  146. package/src/export/html/tool-views.generated.js +20 -20
  147. package/src/extensibility/extensions/types.ts +22 -22
  148. package/src/extensibility/hooks/types.ts +11 -11
  149. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
  150. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
  151. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  152. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  153. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  154. package/src/internal-urls/docs-index.generated.txt +2 -2
  155. package/src/internal-urls/docs-index.ts +2 -3
  156. package/src/internal-urls/index.ts +1 -0
  157. package/src/internal-urls/registry-helpers.ts +19 -4
  158. package/src/internal-urls/router.ts +5 -3
  159. package/src/internal-urls/ssh-protocol.ts +367 -0
  160. package/src/internal-urls/types.ts +19 -2
  161. package/src/irc/bus.ts +11 -3
  162. package/src/lsp/index.ts +8 -1
  163. package/src/mcp/oauth-discovery.ts +20 -20
  164. package/src/mcp/tool-bridge.ts +32 -2
  165. package/src/mcp/transports/stdio.test.ts +20 -3
  166. package/src/mcp/transports/stdio.ts +45 -14
  167. package/src/memories/index.ts +1 -1
  168. package/src/modes/acp/acp-event-mapper.ts +2 -2
  169. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  170. package/src/modes/components/agent-hub.ts +3 -0
  171. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  172. package/src/modes/components/custom-editor.ts +90 -5
  173. package/src/modes/components/move-overlay.ts +282 -0
  174. package/src/modes/components/plan-review-overlay.ts +35 -35
  175. package/src/modes/components/plugin-selector.ts +6 -1
  176. package/src/modes/components/queue-mode-selector.ts +6 -1
  177. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  178. package/src/modes/components/session-selector.ts +11 -10
  179. package/src/modes/components/settings-defs.ts +14 -1
  180. package/src/modes/components/settings-selector.ts +196 -29
  181. package/src/modes/components/show-images-selector.ts +6 -1
  182. package/src/modes/components/status-line/component.ts +108 -28
  183. package/src/modes/components/status-line/segments.ts +5 -1
  184. package/src/modes/components/status-line/types.ts +2 -0
  185. package/src/modes/components/theme-selector.ts +6 -1
  186. package/src/modes/components/thinking-selector.ts +6 -1
  187. package/src/modes/components/tool-execution.ts +25 -9
  188. package/src/modes/components/tree-selector.ts +5 -5
  189. package/src/modes/controllers/command-controller.ts +140 -47
  190. package/src/modes/controllers/event-controller.ts +59 -3
  191. package/src/modes/controllers/input-controller.ts +70 -4
  192. package/src/modes/controllers/selector-controller.ts +15 -2
  193. package/src/modes/controllers/streaming-reveal.ts +17 -0
  194. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  195. package/src/modes/interactive-mode.ts +142 -59
  196. package/src/modes/internal-url-autocomplete.ts +17 -2
  197. package/src/modes/prompt-action-autocomplete.ts +3 -1
  198. package/src/modes/running-subagent-badge.ts +13 -0
  199. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  200. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  201. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  202. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  203. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  204. package/src/modes/theme/mermaid-rendering.test.ts +53 -0
  205. package/src/modes/theme/theme.ts +42 -15
  206. package/src/modes/types.ts +9 -1
  207. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  208. package/src/priority.json +15 -0
  209. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  210. package/src/prompts/advisor/system.md +21 -7
  211. package/src/prompts/agents/designer.md +1 -1
  212. package/src/prompts/agents/explore.md +1 -1
  213. package/src/prompts/agents/librarian.md +2 -2
  214. package/src/prompts/agents/plan.md +2 -2
  215. package/src/prompts/agents/reviewer.md +1 -1
  216. package/src/prompts/agents/task.md +2 -2
  217. package/src/prompts/system/active-repo-context.md +4 -0
  218. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  219. package/src/prompts/system/plan-mode-active.md +12 -3
  220. package/src/prompts/system/project-prompt.md +2 -2
  221. package/src/prompts/system/system-prompt.md +8 -6
  222. package/src/prompts/tools/bash.md +2 -2
  223. package/src/prompts/tools/checkpoint.md +1 -1
  224. package/src/prompts/tools/{find.md → glob.md} +1 -1
  225. package/src/prompts/tools/{search.md → grep.md} +3 -3
  226. package/src/prompts/tools/read.md +4 -2
  227. package/src/sdk.ts +98 -29
  228. package/src/session/agent-session.ts +828 -191
  229. package/src/session/session-history-format.ts +2 -2
  230. package/src/session/session-listing.ts +35 -2
  231. package/src/session/session-manager.ts +46 -0
  232. package/src/slash-commands/builtin-registry.ts +158 -22
  233. package/src/slash-commands/helpers/usage-report.ts +23 -2
  234. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  235. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  236. package/src/ssh/connection-manager.ts +12 -15
  237. package/src/ssh/file-transfer.ts +209 -0
  238. package/src/ssh/utils.ts +24 -0
  239. package/src/system-prompt.ts +60 -28
  240. package/src/task/executor.ts +96 -36
  241. package/src/task/index.ts +3 -3
  242. package/src/task/render.ts +14 -13
  243. package/src/task/worktree.ts +38 -1
  244. package/src/tiny/text.ts +49 -4
  245. package/src/tiny/worker.ts +3 -3
  246. package/src/tools/acp-bridge.ts +6 -1
  247. package/src/tools/bash.ts +2 -2
  248. package/src/tools/builtin-names.ts +26 -2
  249. package/src/tools/{find.ts → glob.ts} +48 -42
  250. package/src/tools/{search.ts → grep.ts} +311 -129
  251. package/src/tools/index.ts +13 -14
  252. package/src/tools/irc.ts +6 -2
  253. package/src/tools/path-utils.ts +64 -1
  254. package/src/tools/plan-mode-guard.ts +26 -13
  255. package/src/tools/read.ts +35 -6
  256. package/src/tools/render-utils.ts +14 -0
  257. package/src/tools/renderers.ts +15 -4
  258. package/src/tools/ssh.ts +17 -2
  259. package/src/tools/todo.ts +4 -26
  260. package/src/tools/write.ts +25 -9
  261. package/src/utils/active-repo-context.ts +143 -0
  262. package/src/utils/edit-mode.ts +19 -2
  263. package/src/utils/image-resize.ts +88 -7
  264. package/src/utils/lang-from-path.ts +3 -3
  265. package/src/utils/markit-cache.ts +166 -0
  266. package/src/utils/markit.ts +86 -18
  267. package/src/utils/prompt-path.ts +3 -0
  268. package/src/utils/shell-snapshot.ts +1 -1
  269. package/src/utils/title-generator.ts +1 -1
  270. package/src/web/search/provider.ts +54 -34
  271. package/src/web/search/providers/duckduckgo.ts +140 -0
  272. package/src/web/search/providers/firecrawl.ts +144 -0
  273. package/src/web/search/providers/tinyfish.ts +159 -0
  274. package/src/web/search/providers/xai.ts +292 -0
  275. package/src/web/search/providers/zai.ts +142 -56
  276. package/src/web/search/types.ts +6 -2
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
4
  import type { ImageContent } from "@oh-my-pi/pi-ai";
4
5
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@oh-my-pi/pi-tui";
5
6
  import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
@@ -124,6 +125,7 @@ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
124
125
  // deliberate human double-tap is always tens of milliseconds apart.
125
126
  const LEFT_DOUBLE_TAP_MIN_GAP_MS = 40;
126
127
  const LEFT_DOUBLE_TAP_MAX_GAP_MS = 500;
128
+ const STREAMING_ESCAPE_CANCEL_WINDOW_MS = 2_000;
127
129
 
128
130
  export class InputController {
129
131
  constructor(
@@ -148,6 +150,16 @@ export class InputController {
148
150
  // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
149
151
  // #detectLeftDoubleTap.
150
152
  #leftTapCount = 0;
153
+ // Streaming turns use a two-step Esc: first press arms this token, second press
154
+ // within the window aborts the same live assistant turn. The token is a per-turn
155
+ // sentinel minted lazily on demand and reset on every `agent_start`/`agent_end`
156
+ // (see setupKeyHandlers), so it survives `message_start`/`message_update`
157
+ // transitions inside a single turn but cannot leak across turn boundaries.
158
+ #streamingEscapeTurnSentinel: object | undefined;
159
+ #streamingEscapeArmedToken: object | undefined;
160
+ #streamingEscapeArmedUntil = 0;
161
+ #streamingEscapeTimer: NodeJS.Timeout | undefined;
162
+ #streamingEscapeSessionSubscribed = false;
151
163
  // Sequential index for `local://attachment-N` references created by large-paste and
152
164
  // pasted-file attachments. Seeded from 0 and bumped past existing attachment files.
153
165
  #attachmentCounter = 0;
@@ -197,8 +209,50 @@ export class InputController {
197
209
  const unsubscribe = tinyTitleClient.onProgress(update);
198
210
  }
199
211
 
212
+ #clearStreamingEscapeArm(): void {
213
+ this.#streamingEscapeArmedToken = undefined;
214
+ this.#streamingEscapeArmedUntil = 0;
215
+ if (this.#streamingEscapeTimer) {
216
+ clearTimeout(this.#streamingEscapeTimer);
217
+ this.#streamingEscapeTimer = undefined;
218
+ }
219
+ }
220
+
221
+ #handleStreamingEscape(): void {
222
+ if (!this.#streamingEscapeTurnSentinel) {
223
+ this.#streamingEscapeTurnSentinel = {};
224
+ }
225
+ const token = this.#streamingEscapeTurnSentinel;
226
+ const now = Date.now();
227
+ if (this.#streamingEscapeArmedToken === token && now <= this.#streamingEscapeArmedUntil) {
228
+ this.#clearStreamingEscapeArm();
229
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
230
+ return;
231
+ }
232
+
233
+ this.#clearStreamingEscapeArm();
234
+ this.#streamingEscapeArmedToken = token;
235
+ this.#streamingEscapeArmedUntil = now + STREAMING_ESCAPE_CANCEL_WINDOW_MS;
236
+ this.#streamingEscapeTimer = setTimeout(() => {
237
+ if (this.#streamingEscapeArmedToken === token && Date.now() >= this.#streamingEscapeArmedUntil) {
238
+ this.#clearStreamingEscapeArm();
239
+ }
240
+ }, STREAMING_ESCAPE_CANCEL_WINDOW_MS);
241
+ this.#streamingEscapeTimer.unref?.();
242
+ this.ctx.showStatus("Press Esc again within 2s to cancel streaming.");
243
+ }
244
+
200
245
  setupKeyHandlers(): void {
201
246
  this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
247
+ if (!this.#streamingEscapeSessionSubscribed && typeof this.ctx.session.subscribe === "function") {
248
+ this.#streamingEscapeSessionSubscribed = true;
249
+ this.ctx.session.subscribe(event => {
250
+ if (event.type === "agent_start" || event.type === "agent_end") {
251
+ this.#streamingEscapeTurnSentinel = undefined;
252
+ this.#clearStreamingEscapeArm();
253
+ }
254
+ });
255
+ }
202
256
  if (!this.#focusedLeftTapListenerInstalled) {
203
257
  this.#focusedLeftTapListenerInstalled = true;
204
258
  this.ctx.ui.addInputListener(data => {
@@ -274,7 +328,7 @@ export class InputController {
274
328
  if (this.ctx.loopModeEnabled) {
275
329
  this.ctx.pauseLoop();
276
330
  if (this.ctx.session.isStreaming) {
277
- void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
331
+ this.#handleStreamingEscape();
278
332
  } else {
279
333
  this.ctx.cancelPendingSubmission();
280
334
  }
@@ -325,12 +379,13 @@ export class InputController {
325
379
  this.ctx.isPythonMode = false;
326
380
  this.ctx.updateEditorBorderColor();
327
381
  } else if (this.ctx.session.isStreaming) {
328
- void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
382
+ this.#handleStreamingEscape();
329
383
  } else if (this.ctx.editor.getText().trim()) {
330
384
  // Esc with typed text clears the draft instead of (or before) any double-Esc action
331
385
  this.ctx.editor.setText("");
332
386
  this.ctx.ui.requestRender();
333
387
  this.ctx.lastEscapeTime = 0;
388
+ this.#clearStreamingEscapeArm();
334
389
  } else {
335
390
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
336
391
  const action = settings.get("doubleEscapeAction");
@@ -969,7 +1024,7 @@ export class InputController {
969
1024
  //
970
1025
  // SIGTSTP: brush-core (the embedded shell behind every bash tool call)
971
1026
  // installs a tokio SIGTSTP listener on `Process::wait` to detect when
972
- // its children have been stopped (`crates/brush-core-vendored/src/sys/
1027
+ // its children have been stopped (`crates/vendor/brush-core/src/sys/
973
1028
  // unix/signal.rs::tstp_signal_listener` → `tokio::signal::unix::
974
1029
  // signal(SIGTSTP)`). Per tokio's documented contract, the first call
975
1030
  // for a given SignalKind permanently replaces the kernel-default
@@ -993,7 +1048,7 @@ export class InputController {
993
1048
  // children that must survive the suspend (MCP stdio servers via
994
1049
  // the `detached: true` spawn in `mcp/transports/stdio.ts`, every
995
1050
  // brush external command via brush's per-child `setsid` in
996
- // `crates/brush-core-vendored/src/commands.rs`) are already in
1051
+ // `crates/vendor/brush-core/src/commands.rs`) are already in
997
1052
  // their own sessions, so pgid=0 does not reach them.
998
1053
  process.kill(0, "SIGSTOP");
999
1054
  } catch (err) {
@@ -1684,6 +1739,17 @@ export class InputController {
1684
1739
  }
1685
1740
 
1686
1741
  toggleThinkingBlockVisibility(): void {
1742
+ // When thinking is "off", thinking blocks are always hidden (some
1743
+ // providers return them regardless). The toggle is meaningless in
1744
+ // that state — inform the user instead of silently flipping the
1745
+ // persisted value. When thinking is on, the toggle works normally
1746
+ // even if blocks are already hidden (user may want to show them).
1747
+ const thinkingOff =
1748
+ ((this.ctx.viewSession ?? this.ctx.session)?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
1749
+ if (thinkingOff) {
1750
+ this.ctx.showStatus("Thinking is off — enable thinking to show blocks");
1751
+ return;
1752
+ }
1687
1753
  this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
1688
1754
  this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
1689
1755
 
@@ -22,6 +22,7 @@ import {
22
22
  getSymbolTheme,
23
23
  previewTheme,
24
24
  setColorBlindMode,
25
+ setMarkdownMermaidRendering,
25
26
  setSymbolPreset,
26
27
  setTheme,
27
28
  theme,
@@ -133,6 +134,9 @@ export class SelectorController {
133
134
  availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
134
135
  thinkingLevel: this.ctx.session.thinkingLevel,
135
136
  availableThemes,
137
+ providers: [...new Set(this.ctx.session.getAvailableModels().map(model => model.provider))].sort(
138
+ (a, b) => a.localeCompare(b),
139
+ ),
136
140
  cwd: getProjectDir(),
137
141
  model: this.ctx.session.model,
138
142
  imageBudget: this.ctx.ui.imageBudget,
@@ -340,7 +344,7 @@ export class SelectorController {
340
344
  this.ctx.hideThinkingBlock = value as boolean;
341
345
  for (const child of this.ctx.chatContainer.children) {
342
346
  if (child instanceof AssistantMessageComponent) {
343
- child.setHideThinkingBlock(value as boolean);
347
+ child.setHideThinkingBlock(this.ctx.effectiveHideThinkingBlock);
344
348
  }
345
349
  }
346
350
  // Full clear + replay so blocks frozen in committed scrollback on
@@ -374,6 +378,15 @@ export class SelectorController {
374
378
  this.ctx.ui.requestRender();
375
379
  break;
376
380
 
381
+ case "tui.renderMermaid":
382
+ setMarkdownMermaidRendering(value as boolean);
383
+ this.ctx.session.refreshBaseSystemPrompt().catch(err => {
384
+ this.ctx.showError(`Failed to apply Mermaid rendering setting: ${err}`);
385
+ });
386
+ this.ctx.rebuildChatFromMessages();
387
+ this.ctx.ui.resetDisplay();
388
+ break;
389
+
377
390
  case "theme": {
378
391
  setTheme(value as string, true).then(result => {
379
392
  this.ctx.statusLine.invalidate();
@@ -1292,7 +1305,7 @@ export class SelectorController {
1292
1305
  getTool: name => this.ctx.session.getToolByName(name),
1293
1306
  getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
1294
1307
  cwd: this.ctx.sessionManager.getCwd(),
1295
- hideThinkingBlock: () => this.ctx.hideThinkingBlock,
1308
+ hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
1296
1309
  proseOnlyThinking: () => this.ctx.proseOnlyThinking,
1297
1310
  focusAgent: id => this.ctx.focusAgentSession(id),
1298
1311
  sessionFile: this.ctx.sessionManager.getSessionFile() ?? null,
@@ -270,6 +270,23 @@ export class StreamingRevealController {
270
270
  this.#unitCounter.reset();
271
271
  }
272
272
 
273
+ /**
274
+ * Re-read cached visibility flags (hideThinkingBlock, proseOnlyThinking)
275
+ * and re-render the current target. Called when the thinking level changes
276
+ * mid-stream so the reveal controller doesn't keep rendering with stale values.
277
+ */
278
+ resyncVisibility(): void {
279
+ if (!this.#target || !this.#component) return;
280
+ this.#hideThinkingBlock = this.#getHideThinkingBlock();
281
+ this.#proseOnlyThinking = this.#getProseOnlyThinking();
282
+ // Recalculate visible units — hiding thinking blocks may reduce the total,
283
+ // and the reveal position may now exceed it.
284
+ const total = this.#visibleUnits(this.#target);
285
+ this.#revealed = Math.min(this.#revealed, total);
286
+ this.#renderCurrent();
287
+ this.#syncTimer(total);
288
+ }
289
+
273
290
  /** Total reveal units of `message`, memoized per block across ticks. */
274
291
  #visibleUnits(message: AssistantMessage): number {
275
292
  let total = 0;
@@ -1,4 +1,4 @@
1
- import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
1
+ import { parseStreamingJson } from "@oh-my-pi/pi-utils";
2
2
  import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
3
3
 
4
4
  /** Minimal component surface the reveal pushes frames into. */
@@ -86,6 +86,7 @@ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" wit
86
86
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
87
87
  type: "text",
88
88
  };
89
+ import type { AgentRegistry } from "../registry/agent-registry";
89
90
  import type { AgentSession, AgentSessionEvent, ResolvedRoleModel } from "../session/agent-session";
90
91
  import type { CompactMode } from "../session/compact-modes";
91
92
  import { HistoryStorage } from "../session/history-storage";
@@ -104,9 +105,10 @@ import { normalizeLocalScheme } from "../tools/path-utils";
104
105
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
105
106
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
106
107
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
107
- import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
108
+ import { formatPhaseDisplayName, todoMatchesAnyDescription } from "../tools/todo";
108
109
  import { ToolError } from "../tools/tool-errors";
109
110
  import { vocalizer } from "../tts/vocalizer";
111
+ import { renderTreeList } from "../tui/tree-list";
110
112
  import type { EventBus } from "../utils/event-bus";
111
113
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
112
114
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -154,6 +156,7 @@ import {
154
156
  parseLoopLimitArgs,
155
157
  } from "./loop-limit";
156
158
  import { OAuthManualInputManager } from "./oauth-manual-input";
159
+ import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
157
160
  import type { ObservableSession } from "./session-observer-registry";
158
161
  import { SessionObserverRegistry } from "./session-observer-registry";
159
162
  import { runProviderSetupWizard } from "./setup-wizard/lazy";
@@ -167,6 +170,7 @@ import {
167
170
  getSymbolTheme,
168
171
  onTerminalAppearanceChange,
169
172
  onThemeChange,
173
+ setMarkdownMermaidRendering,
170
174
  theme,
171
175
  } from "./theme/theme";
172
176
  import type {
@@ -308,13 +312,15 @@ export interface InteractiveModeOptions {
308
312
  }
309
313
 
310
314
  /**
311
- * Hosts the working loader and transient status rows. While anything is
312
- * mounted, every row is live: report a seam at 0 so the engine never commits
313
- * a still-animating loader to native scrollback (stale `Working…` rows would
314
- * otherwise pile up above the live one). The transcript's own seam, when
315
- * present, sits higher and wins (topmost-seam merge in TUI.render).
315
+ * Anchored live-region container for the HUD/status rows between the transcript
316
+ * and the editor (working loader, todo + subagent HUDs, transient notification
317
+ * panels). While it has content every row is live: it reports a seam at 0 so the
318
+ * engine never commits these anchored, rebuilt-in-place rows to native
319
+ * scrollback otherwise stale duplicates pile up above the live copy on short
320
+ * terminals once the loader sits below a tall HUD. The transcript's own seam,
321
+ * when present, sits higher and wins (topmost-seam merge in TUI.render).
316
322
  */
317
- class StatusContainer extends Container implements NativeScrollbackLiveRegion {
323
+ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion {
318
324
  getNativeScrollbackLiveRegionStart(): number | undefined {
319
325
  return this.children.length > 0 ? 0 : undefined;
320
326
  }
@@ -407,6 +413,15 @@ export class InteractiveMode implements InteractiveModeContext {
407
413
  #modelCycleClearTimer: NodeJS.Timeout | undefined;
408
414
  todoPhases: TodoPhase[] = [];
409
415
  hideThinkingBlock = false;
416
+ /**
417
+ * Effective thinking-block visibility: hidden when the user's setting is on
418
+ * OR the session thinking level is "off". Some providers (MiniMax, GLM,
419
+ * DeepSeek) return thinking blocks even with reasoning disabled; this
420
+ * respects the user's intent when they set thinking to "off" (#626).
421
+ */
422
+ get effectiveHideThinkingBlock(): boolean {
423
+ return this.hideThinkingBlock || (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
424
+ }
410
425
  proseOnlyThinking = true;
411
426
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
412
427
  pendingTools = new Map<string, ToolExecutionHandle>();
@@ -540,6 +555,8 @@ export class InteractiveMode implements InteractiveModeContext {
540
555
  #observerRegistry: SessionObserverRegistry;
541
556
  #eventBus?: EventBus;
542
557
  #eventBusUnsubscribers: Array<() => void> = [];
558
+ #agentRegistryUnsubscribe?: () => void;
559
+ #agentRegistrySubscriptionTarget?: AgentRegistry;
543
560
  #mcpStatusOrder: string[] = [];
544
561
  #mcpPendingServers = new Set<string>();
545
562
  #mcpConnectedServers = new Set<string>();
@@ -588,6 +605,7 @@ export class InteractiveMode implements InteractiveModeContext {
588
605
  }
589
606
 
590
607
  setTuiTight(settings.get("tui.tight"));
608
+ setMarkdownMermaidRendering(settings.get("tui.renderMermaid"));
591
609
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
592
610
  this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
593
611
  // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
@@ -596,13 +614,13 @@ export class InteractiveMode implements InteractiveModeContext {
596
614
  setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
597
615
  this.chatContainer = new TranscriptContainer();
598
616
  this.pendingMessagesContainer = new Container();
599
- this.statusContainer = new StatusContainer();
600
- this.todoContainer = new Container();
601
- this.subagentContainer = new Container();
602
- this.btwContainer = new Container();
603
- this.omfgContainer = new Container();
604
- this.errorBannerContainer = new Container();
605
- this.modelCycleContainer = new Container();
617
+ this.statusContainer = new AnchoredLiveContainer();
618
+ this.todoContainer = new AnchoredLiveContainer();
619
+ this.subagentContainer = new AnchoredLiveContainer();
620
+ this.btwContainer = new AnchoredLiveContainer();
621
+ this.omfgContainer = new AnchoredLiveContainer();
622
+ this.errorBannerContainer = new AnchoredLiveContainer();
623
+ this.modelCycleContainer = new AnchoredLiveContainer();
606
624
  this.editor = new CustomEditor(getEditorTheme());
607
625
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
608
626
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
@@ -816,13 +834,16 @@ export class InteractiveMode implements InteractiveModeContext {
816
834
 
817
835
  this.ui.addChild(this.chatContainer);
818
836
  this.ui.addChild(this.pendingMessagesContainer);
819
- this.ui.addChild(this.statusContainer);
820
837
  this.ui.addChild(this.todoContainer);
821
838
  this.ui.addChild(this.subagentContainer);
822
839
  this.ui.addChild(this.btwContainer);
823
840
  this.ui.addChild(this.omfgContainer);
824
841
  this.ui.addChild(this.errorBannerContainer);
825
842
  this.ui.addChild(this.modelCycleContainer);
843
+ // Working loader / transient status sits below the sticky todo + subagent
844
+ // HUDs, just above the editor's hook-widget top margin — so it reads next to
845
+ // the prompt while keeping the one-line gap above the editor.
846
+ this.ui.addChild(this.statusContainer);
826
847
  this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
827
848
  this.ui.addChild(this.hookWidgetContainerAbove);
828
849
  this.ui.addChild(this.editorContainer);
@@ -837,8 +858,14 @@ export class InteractiveMode implements InteractiveModeContext {
837
858
  this.#observerRegistry.subscribeToEventBus(this.#eventBus);
838
859
  }
839
860
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
861
+ this.statusLine.setSubagentHubHint(
862
+ this.keybindings.getDisplayString("app.agents.hub") ||
863
+ this.keybindings.getDisplayString("app.session.observe") ||
864
+ undefined,
865
+ );
866
+ this.syncRunningSubagentBadge();
840
867
  this.#observerRegistry.onChange(() => {
841
- this.statusLine.setSubagentCount(this.#observerRegistry.getActiveSubagentCount());
868
+ this.syncRunningSubagentBadge();
842
869
  // Auto-checkmark todos whose matching subagent just succeeded, then
843
870
  // re-render so the running override (the static "live" glyph when a
844
871
  // subagent is doing the work for a still-pending todo) updates as
@@ -1433,6 +1460,22 @@ export class InteractiveMode implements InteractiveModeContext {
1433
1460
  this.ui.requestRender();
1434
1461
  }
1435
1462
 
1463
+ /** Refresh the running-subagents status badge from the active local or collab registry. */
1464
+ syncRunningSubagentBadge(): void {
1465
+ const registry = getRunningSubagentBadgeRegistry(this.collabGuest);
1466
+ if (this.#agentRegistrySubscriptionTarget !== registry) {
1467
+ this.#agentRegistryUnsubscribe?.();
1468
+ this.#agentRegistrySubscriptionTarget = registry;
1469
+ this.#agentRegistryUnsubscribe = registry.onChange(() => {
1470
+ this.syncRunningSubagentBadge();
1471
+ this.ui.requestRender();
1472
+ });
1473
+ }
1474
+ const count = countRunningSubagentBadgeAgents(registry);
1475
+ this.statusLine.setSubagentCount(count);
1476
+ this.updateEditorTopBorder();
1477
+ }
1478
+
1436
1479
  updateEditorTopBorder(): void {
1437
1480
  const availableWidth = this.editor.getTopBorderAvailableWidth(this.ui.terminal.columns);
1438
1481
  const topBorder = this.statusLine.getTopBorder(availableWidth);
@@ -1484,9 +1527,7 @@ export class InteractiveMode implements InteractiveModeContext {
1484
1527
  case "abandoned":
1485
1528
  return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1486
1529
  default:
1487
- if (matched) {
1488
- return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1489
- }
1530
+ if (matched) return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1490
1531
  return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1491
1532
  }
1492
1533
  }
@@ -1627,44 +1668,66 @@ export class InteractiveMode implements InteractiveModeContext {
1627
1668
  this.todoContainer.clear();
1628
1669
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
1629
1670
  if (phases.length === 0) return;
1630
- const indent = " ";
1631
- const hook = theme.tree.hook;
1632
- const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
1671
+ const expanded = this.todoExpanded;
1672
+ const multiPhase = phases.length > 1;
1673
+ const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
1674
+ // Fixed budgets keep the HUD bounded regardless of plan size / progress.
1675
+ const subsequentStageCap = 4; // stages shown after the active one (header count implies the rest)
1676
+ const activeTaskCap = 5; // open tasks previewed for the active stage
1633
1677
 
1634
1678
  const activeDescs = this.#getActiveSubagentDescriptions();
1635
- // A pending todo "lights up" (accent + running glyph) when an in-flight
1636
- // subagent is doing its work, matched by normalized content overlap.
1679
+ // A pending todo "lights up" (accent) when an in-flight subagent is doing
1680
+ // its work, matched by normalized content overlap.
1637
1681
  const isMatched = (todo: TodoItem): boolean =>
1638
1682
  activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
1639
1683
 
1640
- if (!this.todoExpanded) {
1641
- const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
1642
- const activePhase = phases[activeIdx];
1643
- if (!activePhase) return;
1644
- const { visible, hiddenOpenCount } = selectStickyTodoWindow(activePhase.tasks, 5);
1645
-
1646
- lines.push(
1647
- `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
1684
+ // Task subtree for a phase. Collapsed previews the first open tasks — the
1685
+ // stage's `done/total` makes the hidden count obvious, so there is no
1686
+ // "… more" row; expanded lists every task.
1687
+ const renderTasks = (phase: TodoPhase): string[] => {
1688
+ const open = phase.tasks.filter(t => t.status === "pending" || t.status === "in_progress");
1689
+ const base = expanded ? phase.tasks : open.length > 0 ? open : phase.tasks;
1690
+ const items = expanded ? base : base.slice(0, activeTaskCap);
1691
+ return renderTreeList(
1692
+ { items, expanded: true, renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)) },
1693
+ theme,
1648
1694
  );
1649
- visible.forEach((todo, index) => {
1650
- const prefix = `${indent}${index === 0 ? hook : " "} `;
1651
- lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1652
- });
1653
- if (hiddenOpenCount > 0) {
1654
- lines.push(theme.fg("muted", `${indent} ${hook} +${hiddenOpenCount} more`));
1695
+ };
1696
+
1697
+ // One phase node. The active stage is highlighted with normal-brightness task
1698
+ // progress; other stages render their whole row (name + progress) in the
1699
+ // brighter muted gray. The root header carries overall stage progression.
1700
+ const renderPhase = (phase: TodoPhase, oneBased: number, isActive: boolean): string | string[] => {
1701
+ const label = multiPhase ? formatPhaseDisplayName(phase.name, oneBased) : phase.name;
1702
+ const done = phase.tasks.filter(t => t.status === "completed").length;
1703
+ const progress = ` · ${done}/${phase.tasks.length}`;
1704
+ if (!isActive) {
1705
+ const header = theme.fg("muted", label) + theme.fg("dim", progress);
1706
+ return expanded ? [header, ...renderTasks(phase)] : header;
1655
1707
  }
1656
- this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1657
- return;
1658
- }
1708
+ const header = theme.bold(theme.fg("accent", label)) + theme.fg("dim", progress);
1709
+ return [header, ...renderTasks(phase)];
1710
+ };
1659
1711
 
1660
- phases.forEach((phase, phaseIndex) => {
1661
- lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
1662
- phase.tasks.forEach((todo, index) => {
1663
- const prefix = `${indent}${index === 0 ? hook : " "} `;
1664
- lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1665
- });
1666
- });
1712
+ // Collapsed: active stage + a bounded number of following stages (the
1713
+ // header's "n/total" count implies any not shown). Expanded: every stage
1714
+ // from the top. Roman numerals stay tied to the real phase index.
1715
+ const baseIdx = expanded ? 0 : activeIdx;
1716
+ const phaseSlice = expanded ? phases.slice(baseIdx) : phases.slice(baseIdx, baseIdx + 1 + subsequentStageCap);
1717
+ const phaseTreeLines = renderTreeList(
1718
+ {
1719
+ items: phaseSlice,
1720
+ expanded: true,
1721
+ renderItem: (phase, ctx) => renderPhase(phase, baseIdx + ctx.index + 1, baseIdx + ctx.index === activeIdx),
1722
+ },
1723
+ theme,
1724
+ );
1667
1725
 
1726
+ // Header carries overall stage progression, e.g. "Todos · 1/8".
1727
+ const root =
1728
+ theme.bold(theme.fg("accent", "Todos")) +
1729
+ (multiPhase ? theme.fg("dim", ` · ${activeIdx + 1}/${phases.length}`) : "");
1730
+ const lines = ["", root, ...phaseTreeLines.map(line => ` ${line}`)];
1668
1731
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1669
1732
  }
1670
1733
 
@@ -3021,16 +3084,31 @@ export class InteractiveMode implements InteractiveModeContext {
3021
3084
  // Capture the operator's tier choice and hand it to #approvePlan, which
3022
3085
  // applies it AFTER #exitPlanMode. #exitPlanMode normally restores
3023
3086
  // #planModePreviousModelState (the model from before plan mode), so
3024
- // applying the slider choice any earlier would be silently reverted
3025
- // the bug that made "continue with slow" keep executing on the default
3026
- // model. For compact-context approval, the plan model is kept through
3027
- // compaction, then a successful compaction transitions to the slider model
3028
- // (or restores the pre-plan model when no slider choice was made).
3029
- // `cycle.currentIndex` is exactly that restored model, so any chosen tier
3030
- // differing from it needs an explicit executionModel this also covers
3031
- // leaving the slider on its `default` anchor while planning ran elsewhere.
3087
+ // applying the slider choice any earlier would be silently reverted.
3088
+ // Pass executionModel only when the slider was actually shown a
3089
+ // singleton cycle (e.g. only modelRoles.plan is configured, so
3090
+ // getRoleModelCycle synthesizes a lone `default` entry from the
3091
+ // currently active plan model) hides the slider, the operator made
3092
+ // no selection, and the pre-plan model is not in the cycle. Pinning
3093
+ // that singleton would silently switch the session back to the plan
3094
+ // model after #exitPlanMode restored the pre-plan model.
3095
+ // Treat the choice as implicit only when applying the selected role
3096
+ // would land on the same end state as the restore — same model AND
3097
+ // the same effective thinking level. A role with an explicit thinking
3098
+ // suffix that differs from the restored thinking level must still go
3099
+ // through applyRoleModel, otherwise approving on the same model with a
3100
+ // different configured thinking level silently keeps the pre-plan level.
3101
+ const restoredState = this.#planModePreviousModelState;
3102
+ const restoredIndex =
3103
+ cycle && restoredState
3104
+ ? cycle.models.findIndex(entry => {
3105
+ if (!modelsAreEqual(entry.model, restoredState.model)) return false;
3106
+ if (!entry.explicitThinkingLevel) return true;
3107
+ return entry.thinkingLevel === restoredState.thinkingLevel;
3108
+ })
3109
+ : -1;
3032
3110
  const executionModel =
3033
- cycle && selectedTierIndex !== cycle.currentIndex ? cycle.models[selectedTierIndex] : undefined;
3111
+ slider && cycle && selectedTierIndex !== restoredIndex ? cycle.models[selectedTierIndex] : undefined;
3034
3112
  await this.#approvePlan(latestPlanContent, {
3035
3113
  planFilePath,
3036
3114
  title: details.title,
@@ -3139,6 +3217,9 @@ export class InteractiveMode implements InteractiveModeContext {
3139
3217
  }
3140
3218
  this.#eventBusUnsubscribers = [];
3141
3219
  this.#observerRegistry.dispose();
3220
+ this.#agentRegistryUnsubscribe?.();
3221
+ this.#agentRegistryUnsubscribe = undefined;
3222
+ this.#agentRegistrySubscriptionTarget = undefined;
3142
3223
  this.#eventController.dispose();
3143
3224
  this.statusLine.dispose();
3144
3225
  if (this.#resizeHandler) {
@@ -3424,8 +3505,10 @@ export class InteractiveMode implements InteractiveModeContext {
3424
3505
  getSymbolTheme().spinnerFrames,
3425
3506
  );
3426
3507
  this.statusContainer.addChild(this.loadingAnimation);
3508
+ } else if (!this.statusContainer.children.includes(this.loadingAnimation)) {
3509
+ this.statusContainer.clear();
3510
+ this.statusContainer.addChild(this.loadingAnimation);
3427
3511
  }
3428
-
3429
3512
  this.applyPendingWorkingMessage();
3430
3513
  }
3431
3514
 
@@ -3609,7 +3692,7 @@ export class InteractiveMode implements InteractiveModeContext {
3609
3692
  return this.#commandController.handleForkCommand();
3610
3693
  }
3611
3694
 
3612
- handleMoveCommand(targetPath: string): Promise<void> {
3695
+ handleMoveCommand(targetPath?: string): Promise<void> {
3613
3696
  return this.#commandController.handleMoveCommand(targetPath);
3614
3697
  }
3615
3698
 
@@ -60,6 +60,16 @@ function fuzzyScore(query: string, target: string): number {
60
60
  return Math.max(1, 40 - gaps * 5);
61
61
  }
62
62
 
63
+ /** Decode a completion `value` for fuzzy matching (the inserted value may be
64
+ * percent-encoded, e.g. an ssh host `alice%40prod`); identity for plain values. */
65
+ function decodeUrlCompletionValue(value: string): string {
66
+ try {
67
+ return decodeURIComponent(value);
68
+ } catch {
69
+ return value;
70
+ }
71
+ }
72
+
63
73
  /**
64
74
  * Detect a completable internal-url token immediately before the cursor.
65
75
  * Returns `null` when the text is not a `scheme://` token whose scheme is
@@ -82,17 +92,22 @@ export function extractInternalUrlContext(textBeforeCursor: string): InternalUrl
82
92
  */
83
93
  export async function getInternalUrlSuggestions(
84
94
  textBeforeCursor: string,
95
+ cwd?: string,
85
96
  ): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
86
97
  const ctx = extractInternalUrlContext(textBeforeCursor);
87
98
  if (!ctx) return null;
88
99
 
89
- const candidates = await InternalUrlRouter.instance().complete(ctx.scheme, ctx.query);
100
+ const candidates = await InternalUrlRouter.instance().complete(
101
+ ctx.scheme,
102
+ ctx.query,
103
+ cwd === undefined ? undefined : { cwd },
104
+ );
90
105
  if (!candidates || candidates.length === 0) return null;
91
106
 
92
107
  const query = ctx.query.toLowerCase();
93
108
  const scored: Array<{ item: AutocompleteItem; score: number }> = [];
94
109
  for (const candidate of candidates) {
95
- const target = candidate.value.toLowerCase();
110
+ const target = decodeUrlCompletionValue(candidate.value).toLowerCase();
96
111
  if (!fuzzyMatch(query, target)) continue;
97
112
  scored.push({
98
113
  item: {
@@ -96,9 +96,11 @@ function getPromptActionPrefix(textBeforeCursor: string): string | null {
96
96
  export class PromptActionAutocompleteProvider implements AutocompleteProvider {
97
97
  #baseProvider: CombinedAutocompleteProvider;
98
98
  #actions: PromptActionDefinition[];
99
+ #basePath: string;
99
100
 
100
101
  constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
101
102
  this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
103
+ this.#basePath = basePath;
102
104
  this.#actions = actions;
103
105
  }
104
106
 
@@ -133,7 +135,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
133
135
  }
134
136
  }
135
137
 
136
- const urlSuggestions = await getInternalUrlSuggestions(textBeforeCursor);
138
+ const urlSuggestions = await getInternalUrlSuggestions(textBeforeCursor, this.#basePath);
137
139
  if (urlSuggestions) return urlSuggestions;
138
140
 
139
141
  if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
@@ -0,0 +1,13 @@
1
+ import { AgentRegistry } from "../registry/agent-registry";
2
+
3
+ export interface RunningSubagentRegistrySource {
4
+ agentRegistry: AgentRegistry;
5
+ }
6
+
7
+ export function getRunningSubagentBadgeRegistry(collabGuest: RunningSubagentRegistrySource | undefined): AgentRegistry {
8
+ return collabGuest?.agentRegistry ?? AgentRegistry.global();
9
+ }
10
+
11
+ export function countRunningSubagentBadgeAgents(registry: AgentRegistry): number {
12
+ return registry.list().filter(ref => ref.kind === "sub" && ref.status === "running").length;
13
+ }