@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (260) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli.js +3785 -3695
  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 +13 -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/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  35. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  37. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  38. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  39. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  40. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  41. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  42. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  43. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  44. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  45. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  46. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  47. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  48. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  49. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  50. package/dist/types/modes/interactive-mode.d.ts +10 -1
  51. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  52. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  53. package/dist/types/modes/theme/theme.d.ts +1 -1
  54. package/dist/types/modes/types.d.ts +9 -1
  55. package/dist/types/sdk.d.ts +2 -2
  56. package/dist/types/session/agent-session.d.ts +8 -1
  57. package/dist/types/session/blob-store.d.ts +4 -0
  58. package/dist/types/session/session-manager.d.ts +13 -0
  59. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  60. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  61. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  62. package/dist/types/ssh/connection-manager.d.ts +2 -0
  63. package/dist/types/ssh/file-transfer.d.ts +79 -0
  64. package/dist/types/ssh/utils.d.ts +6 -0
  65. package/dist/types/system-prompt.d.ts +3 -0
  66. package/dist/types/task/executor.d.ts +16 -0
  67. package/dist/types/tiny/text.d.ts +1 -1
  68. package/dist/types/tools/builtin-names.d.ts +5 -1
  69. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  70. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  71. package/dist/types/tools/index.d.ts +3 -3
  72. package/dist/types/tools/path-utils.d.ts +29 -0
  73. package/dist/types/tools/read.d.ts +2 -2
  74. package/dist/types/tools/render-utils.d.ts +8 -0
  75. package/dist/types/tools/renderers.d.ts +11 -0
  76. package/dist/types/tools/ssh.d.ts +2 -0
  77. package/dist/types/tools/todo.d.ts +0 -16
  78. package/dist/types/tools/write.d.ts +2 -2
  79. package/dist/types/utils/active-repo-context.d.ts +8 -0
  80. package/dist/types/utils/image-resize.d.ts +1 -0
  81. package/dist/types/utils/markit-cache.d.ts +23 -0
  82. package/dist/types/utils/markit.d.ts +5 -1
  83. package/dist/types/utils/prompt-path.d.ts +1 -0
  84. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  85. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  86. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  87. package/dist/types/web/search/providers/xai.d.ts +13 -0
  88. package/dist/types/web/search/types.d.ts +18 -2
  89. package/package.json +30 -15
  90. package/scripts/build-binary.ts +9 -9
  91. package/scripts/bundle-dist.ts +2 -2
  92. package/src/advisor/__tests__/advisor.test.ts +61 -4
  93. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  94. package/src/advisor/advise-tool.ts +1 -1
  95. package/src/advisor/emission-guard.ts +172 -0
  96. package/src/advisor/index.ts +1 -0
  97. package/src/advisor/runtime.ts +11 -0
  98. package/src/advisor/watchdog.ts +27 -1
  99. package/src/cli/args.ts +5 -2
  100. package/src/cli/auth-broker-cli.ts +17 -0
  101. package/src/cli/config-cli.ts +4 -0
  102. package/src/cli/flag-tables.ts +7 -4
  103. package/src/cli/gallery-cli.ts +14 -2
  104. package/src/cli/gallery-fixtures/edit.ts +60 -0
  105. package/src/cli/gallery-fixtures/fs.ts +17 -17
  106. package/src/cli/gallery-fixtures/search.ts +4 -4
  107. package/src/cli/usage-cli.ts +20 -1
  108. package/src/cli/web-search-cli.ts +1 -1
  109. package/src/collab/display-name.ts +13 -0
  110. package/src/collab/guest.ts +5 -1
  111. package/src/collab/host.ts +2 -13
  112. package/src/commands/worktree.ts +6 -0
  113. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  114. package/src/config/model-discovery.ts +77 -8
  115. package/src/config/model-registry.ts +89 -11
  116. package/src/config/model-resolver.ts +39 -1
  117. package/src/config/models-config-schema.ts +41 -5
  118. package/src/config/models-config.ts +4 -1
  119. package/src/config/settings-schema.ts +85 -27
  120. package/src/config/settings.ts +172 -1
  121. package/src/cursor.ts +1 -1
  122. package/src/dap/config.ts +152 -8
  123. package/src/dap/session.ts +1 -1
  124. package/src/discovery/claude-plugins.ts +3 -2
  125. package/src/discovery/helpers.ts +3 -1
  126. package/src/edit/hashline/diff.ts +14 -3
  127. package/src/edit/hashline/execute.ts +42 -6
  128. package/src/edit/hashline/filesystem.ts +38 -2
  129. package/src/edit/index.ts +1 -0
  130. package/src/edit/modes/patch.ts +11 -1
  131. package/src/edit/renderer.ts +140 -13
  132. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  133. package/src/eval/agent-bridge.ts +26 -3
  134. package/src/eval/js/tool-bridge.ts +2 -2
  135. package/src/export/html/index.ts +1 -1
  136. package/src/export/html/template.js +3 -1
  137. package/src/export/html/tool-views.generated.js +20 -20
  138. package/src/extensibility/extensions/types.ts +22 -22
  139. package/src/extensibility/hooks/types.ts +11 -11
  140. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  141. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  142. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  143. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  144. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  145. package/src/internal-urls/docs-index.generated.txt +2 -2
  146. package/src/internal-urls/docs-index.ts +2 -3
  147. package/src/internal-urls/index.ts +1 -0
  148. package/src/internal-urls/registry-helpers.ts +19 -4
  149. package/src/internal-urls/router.ts +5 -3
  150. package/src/internal-urls/ssh-protocol.ts +367 -0
  151. package/src/internal-urls/types.ts +19 -2
  152. package/src/irc/bus.ts +11 -3
  153. package/src/mcp/tool-bridge.ts +32 -2
  154. package/src/memories/index.ts +1 -1
  155. package/src/modes/acp/acp-agent.ts +14 -1
  156. package/src/modes/acp/acp-event-mapper.ts +91 -27
  157. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  158. package/src/modes/components/agent-hub.ts +3 -0
  159. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  160. package/src/modes/components/custom-editor.ts +90 -5
  161. package/src/modes/components/move-overlay.ts +282 -0
  162. package/src/modes/components/plan-review-overlay.ts +35 -35
  163. package/src/modes/components/plugin-selector.ts +6 -1
  164. package/src/modes/components/queue-mode-selector.ts +6 -1
  165. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  166. package/src/modes/components/session-selector.ts +11 -10
  167. package/src/modes/components/settings-defs.ts +14 -1
  168. package/src/modes/components/settings-selector.ts +196 -29
  169. package/src/modes/components/show-images-selector.ts +6 -1
  170. package/src/modes/components/status-line/component.ts +108 -28
  171. package/src/modes/components/status-line/segments.ts +5 -1
  172. package/src/modes/components/status-line/types.ts +2 -0
  173. package/src/modes/components/theme-selector.ts +6 -1
  174. package/src/modes/components/thinking-selector.ts +6 -1
  175. package/src/modes/components/tool-execution.ts +25 -9
  176. package/src/modes/components/tree-selector.ts +5 -5
  177. package/src/modes/controllers/command-controller.ts +140 -47
  178. package/src/modes/controllers/event-controller.ts +59 -3
  179. package/src/modes/controllers/input-controller.ts +68 -2
  180. package/src/modes/controllers/selector-controller.ts +5 -2
  181. package/src/modes/controllers/streaming-reveal.ts +17 -0
  182. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  183. package/src/modes/interactive-mode.ts +116 -50
  184. package/src/modes/internal-url-autocomplete.ts +17 -2
  185. package/src/modes/prompt-action-autocomplete.ts +3 -1
  186. package/src/modes/running-subagent-badge.ts +13 -0
  187. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  188. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  189. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  190. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  191. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  192. package/src/modes/theme/theme.ts +9 -1
  193. package/src/modes/types.ts +9 -1
  194. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  195. package/src/priority.json +15 -0
  196. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  197. package/src/prompts/advisor/context-files.md +8 -0
  198. package/src/prompts/advisor/system.md +21 -7
  199. package/src/prompts/agents/designer.md +1 -1
  200. package/src/prompts/agents/explore.md +1 -1
  201. package/src/prompts/agents/librarian.md +2 -2
  202. package/src/prompts/agents/plan.md +2 -2
  203. package/src/prompts/agents/reviewer.md +1 -1
  204. package/src/prompts/agents/task.md +2 -2
  205. package/src/prompts/system/active-repo-context.md +4 -0
  206. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  207. package/src/prompts/system/plan-mode-active.md +3 -3
  208. package/src/prompts/system/project-prompt.md +2 -2
  209. package/src/prompts/system/system-prompt.md +6 -6
  210. package/src/prompts/tools/bash.md +2 -2
  211. package/src/prompts/tools/checkpoint.md +1 -1
  212. package/src/prompts/tools/{find.md → glob.md} +1 -1
  213. package/src/prompts/tools/{search.md → grep.md} +3 -3
  214. package/src/prompts/tools/read.md +4 -2
  215. package/src/sdk.ts +65 -25
  216. package/src/session/agent-session.ts +519 -132
  217. package/src/session/blob-store.ts +24 -0
  218. package/src/session/session-history-format.ts +2 -2
  219. package/src/session/session-manager.ts +46 -0
  220. package/src/slash-commands/builtin-registry.ts +138 -20
  221. package/src/slash-commands/helpers/usage-report.ts +23 -2
  222. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  223. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  224. package/src/ssh/connection-manager.ts +12 -15
  225. package/src/ssh/file-transfer.ts +209 -0
  226. package/src/ssh/utils.ts +24 -0
  227. package/src/system-prompt.ts +56 -28
  228. package/src/task/executor.ts +96 -36
  229. package/src/task/index.ts +3 -3
  230. package/src/task/render.ts +14 -13
  231. package/src/task/worktree.ts +38 -1
  232. package/src/tiny/text.ts +49 -4
  233. package/src/tiny/worker.ts +3 -3
  234. package/src/tools/bash.ts +2 -2
  235. package/src/tools/builtin-names.ts +26 -2
  236. package/src/tools/{find.ts → glob.ts} +48 -42
  237. package/src/tools/{search.ts → grep.ts} +311 -129
  238. package/src/tools/index.ts +13 -14
  239. package/src/tools/irc.ts +6 -2
  240. package/src/tools/path-utils.ts +64 -1
  241. package/src/tools/read.ts +35 -6
  242. package/src/tools/render-utils.ts +14 -0
  243. package/src/tools/renderers.ts +15 -4
  244. package/src/tools/ssh.ts +17 -2
  245. package/src/tools/todo.ts +4 -26
  246. package/src/tools/write.ts +25 -9
  247. package/src/utils/active-repo-context.ts +143 -0
  248. package/src/utils/image-resize.ts +88 -7
  249. package/src/utils/lang-from-path.ts +3 -3
  250. package/src/utils/markit-cache.ts +166 -0
  251. package/src/utils/markit.ts +86 -18
  252. package/src/utils/prompt-path.ts +3 -0
  253. package/src/utils/title-generator.ts +1 -1
  254. package/src/web/search/provider.ts +54 -34
  255. package/src/web/search/providers/duckduckgo.ts +140 -0
  256. package/src/web/search/providers/firecrawl.ts +144 -0
  257. package/src/web/search/providers/tinyfish.ts +159 -0
  258. package/src/web/search/providers/xai.ts +292 -0
  259. package/src/web/search/providers/zai.ts +142 -56
  260. 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");
@@ -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
 
@@ -134,6 +134,9 @@ export class SelectorController {
134
134
  availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
135
135
  thinkingLevel: this.ctx.session.thinkingLevel,
136
136
  availableThemes,
137
+ providers: [...new Set(this.ctx.session.getAvailableModels().map(model => model.provider))].sort(
138
+ (a, b) => a.localeCompare(b),
139
+ ),
137
140
  cwd: getProjectDir(),
138
141
  model: this.ctx.session.model,
139
142
  imageBudget: this.ctx.ui.imageBudget,
@@ -341,7 +344,7 @@ export class SelectorController {
341
344
  this.ctx.hideThinkingBlock = value as boolean;
342
345
  for (const child of this.ctx.chatContainer.children) {
343
346
  if (child instanceof AssistantMessageComponent) {
344
- child.setHideThinkingBlock(value as boolean);
347
+ child.setHideThinkingBlock(this.ctx.effectiveHideThinkingBlock);
345
348
  }
346
349
  }
347
350
  // Full clear + replay so blocks frozen in committed scrollback on
@@ -1302,7 +1305,7 @@ export class SelectorController {
1302
1305
  getTool: name => this.ctx.session.getToolByName(name),
1303
1306
  getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
1304
1307
  cwd: this.ctx.sessionManager.getCwd(),
1305
- hideThinkingBlock: () => this.ctx.hideThinkingBlock,
1308
+ hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
1306
1309
  proseOnlyThinking: () => this.ctx.proseOnlyThinking,
1307
1310
  focusAgent: id => this.ctx.focusAgentSession(id),
1308
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";
@@ -309,13 +312,15 @@ export interface InteractiveModeOptions {
309
312
  }
310
313
 
311
314
  /**
312
- * Hosts the working loader and transient status rows. While anything is
313
- * mounted, every row is live: report a seam at 0 so the engine never commits
314
- * a still-animating loader to native scrollback (stale `Working…` rows would
315
- * otherwise pile up above the live one). The transcript's own seam, when
316
- * 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).
317
322
  */
318
- class StatusContainer extends Container implements NativeScrollbackLiveRegion {
323
+ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion {
319
324
  getNativeScrollbackLiveRegionStart(): number | undefined {
320
325
  return this.children.length > 0 ? 0 : undefined;
321
326
  }
@@ -408,6 +413,15 @@ export class InteractiveMode implements InteractiveModeContext {
408
413
  #modelCycleClearTimer: NodeJS.Timeout | undefined;
409
414
  todoPhases: TodoPhase[] = [];
410
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
+ }
411
425
  proseOnlyThinking = true;
412
426
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
413
427
  pendingTools = new Map<string, ToolExecutionHandle>();
@@ -541,6 +555,8 @@ export class InteractiveMode implements InteractiveModeContext {
541
555
  #observerRegistry: SessionObserverRegistry;
542
556
  #eventBus?: EventBus;
543
557
  #eventBusUnsubscribers: Array<() => void> = [];
558
+ #agentRegistryUnsubscribe?: () => void;
559
+ #agentRegistrySubscriptionTarget?: AgentRegistry;
544
560
  #mcpStatusOrder: string[] = [];
545
561
  #mcpPendingServers = new Set<string>();
546
562
  #mcpConnectedServers = new Set<string>();
@@ -598,13 +614,13 @@ export class InteractiveMode implements InteractiveModeContext {
598
614
  setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
599
615
  this.chatContainer = new TranscriptContainer();
600
616
  this.pendingMessagesContainer = new Container();
601
- this.statusContainer = new StatusContainer();
602
- this.todoContainer = new Container();
603
- this.subagentContainer = new Container();
604
- this.btwContainer = new Container();
605
- this.omfgContainer = new Container();
606
- this.errorBannerContainer = new Container();
607
- 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();
608
624
  this.editor = new CustomEditor(getEditorTheme());
609
625
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
610
626
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
@@ -818,13 +834,16 @@ export class InteractiveMode implements InteractiveModeContext {
818
834
 
819
835
  this.ui.addChild(this.chatContainer);
820
836
  this.ui.addChild(this.pendingMessagesContainer);
821
- this.ui.addChild(this.statusContainer);
822
837
  this.ui.addChild(this.todoContainer);
823
838
  this.ui.addChild(this.subagentContainer);
824
839
  this.ui.addChild(this.btwContainer);
825
840
  this.ui.addChild(this.omfgContainer);
826
841
  this.ui.addChild(this.errorBannerContainer);
827
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);
828
847
  this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
829
848
  this.ui.addChild(this.hookWidgetContainerAbove);
830
849
  this.ui.addChild(this.editorContainer);
@@ -839,8 +858,14 @@ export class InteractiveMode implements InteractiveModeContext {
839
858
  this.#observerRegistry.subscribeToEventBus(this.#eventBus);
840
859
  }
841
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();
842
867
  this.#observerRegistry.onChange(() => {
843
- this.statusLine.setSubagentCount(this.#observerRegistry.getActiveSubagentCount());
868
+ this.syncRunningSubagentBadge();
844
869
  // Auto-checkmark todos whose matching subagent just succeeded, then
845
870
  // re-render so the running override (the static "live" glyph when a
846
871
  // subagent is doing the work for a still-pending todo) updates as
@@ -1435,6 +1460,22 @@ export class InteractiveMode implements InteractiveModeContext {
1435
1460
  this.ui.requestRender();
1436
1461
  }
1437
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
+
1438
1479
  updateEditorTopBorder(): void {
1439
1480
  const availableWidth = this.editor.getTopBorderAvailableWidth(this.ui.terminal.columns);
1440
1481
  const topBorder = this.statusLine.getTopBorder(availableWidth);
@@ -1486,9 +1527,7 @@ export class InteractiveMode implements InteractiveModeContext {
1486
1527
  case "abandoned":
1487
1528
  return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1488
1529
  default:
1489
- if (matched) {
1490
- return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1491
- }
1530
+ if (matched) return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1492
1531
  return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1493
1532
  }
1494
1533
  }
@@ -1629,44 +1668,66 @@ export class InteractiveMode implements InteractiveModeContext {
1629
1668
  this.todoContainer.clear();
1630
1669
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
1631
1670
  if (phases.length === 0) return;
1632
- const indent = " ";
1633
- const hook = theme.tree.hook;
1634
- 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
1635
1677
 
1636
1678
  const activeDescs = this.#getActiveSubagentDescriptions();
1637
- // A pending todo "lights up" (accent + running glyph) when an in-flight
1638
- // 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.
1639
1681
  const isMatched = (todo: TodoItem): boolean =>
1640
1682
  activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
1641
1683
 
1642
- if (!this.todoExpanded) {
1643
- const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
1644
- const activePhase = phases[activeIdx];
1645
- if (!activePhase) return;
1646
- const { visible, hiddenOpenCount } = selectStickyTodoWindow(activePhase.tasks, 5);
1647
-
1648
- lines.push(
1649
- `${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,
1650
1694
  );
1651
- visible.forEach((todo, index) => {
1652
- const prefix = `${indent}${index === 0 ? hook : " "} `;
1653
- lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1654
- });
1655
- if (hiddenOpenCount > 0) {
1656
- 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;
1657
1707
  }
1658
- this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1659
- return;
1660
- }
1708
+ const header = theme.bold(theme.fg("accent", label)) + theme.fg("dim", progress);
1709
+ return [header, ...renderTasks(phase)];
1710
+ };
1661
1711
 
1662
- phases.forEach((phase, phaseIndex) => {
1663
- lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
1664
- phase.tasks.forEach((todo, index) => {
1665
- const prefix = `${indent}${index === 0 ? hook : " "} `;
1666
- lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1667
- });
1668
- });
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
+ );
1669
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}`)];
1670
1731
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1671
1732
  }
1672
1733
 
@@ -3156,6 +3217,9 @@ export class InteractiveMode implements InteractiveModeContext {
3156
3217
  }
3157
3218
  this.#eventBusUnsubscribers = [];
3158
3219
  this.#observerRegistry.dispose();
3220
+ this.#agentRegistryUnsubscribe?.();
3221
+ this.#agentRegistryUnsubscribe = undefined;
3222
+ this.#agentRegistrySubscriptionTarget = undefined;
3159
3223
  this.#eventController.dispose();
3160
3224
  this.statusLine.dispose();
3161
3225
  if (this.#resizeHandler) {
@@ -3441,8 +3505,10 @@ export class InteractiveMode implements InteractiveModeContext {
3441
3505
  getSymbolTheme().spinnerFrames,
3442
3506
  );
3443
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);
3444
3511
  }
3445
-
3446
3512
  this.applyPendingWorkingMessage();
3447
3513
  }
3448
3514
 
@@ -3626,7 +3692,7 @@ export class InteractiveMode implements InteractiveModeContext {
3626
3692
  return this.#commandController.handleForkCommand();
3627
3693
  }
3628
3694
 
3629
- handleMoveCommand(targetPath: string): Promise<void> {
3695
+ handleMoveCommand(targetPath?: string): Promise<void> {
3630
3696
  return this.#commandController.handleMoveCommand(targetPath);
3631
3697
  }
3632
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
+ }
@@ -1,4 +1,4 @@
1
- import { type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
1
+ import { routeSelectListMouse, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
2
  import { getSelectListTheme, type SymbolPreset, setSymbolPreset, theme } from "../../theme/theme";
3
3
  import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
4
4
 
@@ -65,18 +65,7 @@ class GlyphSceneController implements SetupSceneController {
65
65
  /** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
66
66
  routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
67
67
  if (this.#committing) return;
68
- if (event.wheel !== null) {
69
- this.#selectList.handleWheel(event.wheel);
70
- return;
71
- }
72
- const index = this.#selectList.hitTest(line - this.#listRowStart);
73
- if (event.motion) {
74
- this.#selectList.setHoverIndex(index ?? null);
75
- return;
76
- }
77
- if (event.leftClick && index !== undefined) {
78
- this.#selectList.clickItem(index);
79
- }
68
+ routeSelectListMouse(this.#selectList, event, line - this.#listRowStart);
80
69
  }
81
70
 
82
71
  render(width: number): readonly string[] {
@@ -74,7 +74,8 @@ class ProvidersSceneController implements SetupSceneController {
74
74
  return;
75
75
  }
76
76
  if (event.motion) this.#tabBar.setHoverTab(null);
77
- const bodyLine = line - this.#tabRowCount - 1;
77
+ const spacerRowsAfterTabs = 1;
78
+ const bodyLine = line - this.#tabRowCount - spacerRowsAfterTabs;
78
79
  if (tab.routeMouse) {
79
80
  tab.routeMouse(event, bodyLine, col);
80
81
  return;
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  padding,
3
+ routeSelectListMouse,
3
4
  type SelectItem,
4
5
  SelectList,
5
6
  type SgrMouseEvent,
@@ -128,18 +129,11 @@ class ThemeSceneController implements SetupSceneController {
128
129
 
129
130
  /** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
130
131
  routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
131
- if (event.wheel !== null) {
132
- this.#selectList.handleWheel(event.wheel);
133
- return;
134
- }
135
- const index = this.#listRowStart >= 0 ? this.#selectList.hitTest(line - this.#listRowStart) : undefined;
136
- if (event.motion) {
137
- this.#selectList.setHoverIndex(index ?? null);
138
- return;
139
- }
140
- if (event.leftClick && index !== undefined) {
141
- this.#selectList.clickItem(index);
142
- }
132
+ // Mirror the pre-helper flow: wheel/motion are always processed, but a
133
+ // hidden list (#listRowStart < 0, e.g. while loading all themes) must
134
+ // never hit-test a row — route through a line that resolves to undefined.
135
+ const listLine = this.#listRowStart >= 0 ? line - this.#listRowStart : Number.NEGATIVE_INFINITY;
136
+ routeSelectListMouse(this.#selectList, event, listLine);
143
137
  }
144
138
 
145
139
  render(width: number): readonly string[] {
@@ -1,4 +1,10 @@
1
- import { type SelectItem, SelectList, type SgrMouseEvent, truncateToWidth } from "@oh-my-pi/pi-tui";
1
+ import {
2
+ routeSelectListMouse,
3
+ type SelectItem,
4
+ SelectList,
5
+ type SgrMouseEvent,
6
+ truncateToWidth,
7
+ } from "@oh-my-pi/pi-tui";
2
8
  import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
3
9
  import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
4
10
  import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
@@ -59,18 +65,7 @@ export class WebSearchTab implements SetupTab {
59
65
 
60
66
  /** Wheel moves the highlight; hover lights the row under the pointer; click confirms it. */
61
67
  routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
62
- if (event.wheel !== null) {
63
- this.#list.handleWheel(event.wheel);
64
- return;
65
- }
66
- const index = this.#list.hitTest(line - this.#listRowStart);
67
- if (event.motion) {
68
- this.#list.setHoverIndex(index ?? null);
69
- return;
70
- }
71
- if (event.leftClick && index !== undefined) {
72
- this.#list.clickItem(index);
73
- }
68
+ routeSelectListMouse(this.#list, event, line - this.#listRowStart);
74
69
  }
75
70
 
76
71
  invalidate(): void {