@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
@@ -202,7 +202,7 @@ const pathSegment: StatusLineSegment = {
202
202
  render(ctx) {
203
203
  const opts = ctx.options.path ?? {};
204
204
 
205
- const projectDir = getProjectDir();
205
+ const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
206
206
  const { scratch, relative } = classifyProjectDir(projectDir);
207
207
  let pwd = projectDir;
208
208
 
@@ -213,6 +213,7 @@ const pathSegment: StatusLineSegment = {
213
213
  pwd = stripDisplayRoot(pwd);
214
214
  }
215
215
  }
216
+ const repoSuffix = ctx.activeRepo ? ` ↳ ${ctx.activeRepo.relativeRepoRoot}` : "";
216
217
  if (opts.abbreviate !== false) {
217
218
  pwd = shortenPath(pwd);
218
219
  }
@@ -223,6 +224,9 @@ const pathSegment: StatusLineSegment = {
223
224
  const sliceLen = Math.max(0, maxLen - ellipsis.length);
224
225
  pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
225
226
  }
227
+ if (repoSuffix) {
228
+ pwd = `${pwd}${repoSuffix}`;
229
+ }
226
230
 
227
231
  const showScratchIcon = scratch && opts.stripWorkPrefix !== false;
228
232
  const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
@@ -1,6 +1,7 @@
1
1
  import type { CollabSessionState } from "../../../collab/protocol";
2
2
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
3
3
  import type { AgentSession } from "../../../session/agent-session";
4
+ import type { ActiveRepoContext } from "../../../utils/active-repo-context";
4
5
 
5
6
  export type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle };
6
7
 
@@ -47,6 +48,7 @@ export interface SegmentContext {
47
48
  session: AgentSession;
48
49
  /** Focused subagent id while the view is proxied at its session, undefined otherwise. */
49
50
  focusedAgentId?: string | undefined;
51
+ activeRepo: ActiveRepoContext | null;
50
52
  width: number;
51
53
  options: StatusLineSegmentOptions;
52
54
  planMode: {
@@ -1,6 +1,7 @@
1
- import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
1
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
2
  import { getSelectListTheme } from "../../modes/theme/theme";
3
3
  import { DynamicBorder } from "./dynamic-border";
4
+ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
4
5
 
5
6
  /**
6
7
  * Component that renders a theme selector.
@@ -60,4 +61,8 @@ export class ThemeSelectorComponent extends Container {
60
61
  getSelectList(): SelectList {
61
62
  return this.#selectList;
62
63
  }
64
+
65
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
66
+ routeSelectListMouseWithTopBorder(this.#selectList, event, line, col);
67
+ }
63
68
  }
@@ -1,8 +1,9 @@
1
1
  import type { Effort } from "@oh-my-pi/pi-ai";
2
- import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
3
3
  import { getSelectListTheme } from "../../modes/theme/theme";
4
4
  import { getThinkingLevelMetadata } from "../../thinking";
5
5
  import { DynamicBorder } from "./dynamic-border";
6
+ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
6
7
 
7
8
  /**
8
9
  * Component that renders a thinking level selector with borders
@@ -49,4 +50,8 @@ export class ThinkingSelectorComponent extends Container {
49
50
  getSelectList(): SelectList {
50
51
  return this.#selectList;
51
52
  }
53
+
54
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
55
+ routeSelectListMouseWithTopBorder(this.#selectList, event, line, col);
56
+ }
52
57
  }
@@ -655,15 +655,31 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
655
655
  // `provisionalPendingPreview` describes only the PENDING call preview
656
656
  // (`renderCall`, before any result): the result render may re-anchor it
657
657
  // wholesale, so its rows must never commit. Once a (streaming partial)
658
- // result exists the result renderer is the live shape — its body is
659
- // top-anchored and grows append-only, and `deriveLiveCommitState` gates
660
- // per-row durability — so the block is commit-stable like any settled
661
- // stream. Gating the flag on the pending phase is what keeps a collapsed
662
- // streaming eval/bash/ssh whose box outgrows the viewport from stranding
663
- // its head: while commit-unstable its scrolled-off top committed nowhere
664
- // and repainted nowhere, so it read as truncated until ctrl+o (expanded)
665
- // flipped it stable.
666
- if (this.#result !== undefined) return true;
658
+ // result exists the result renderer is usually the live shape — its body
659
+ // is top-anchored and grows append-only, and `deriveLiveCommitState`
660
+ // gates per-row durability — so the block is commit-stable like any
661
+ // settled stream. Gating the flag on the pending phase is what keeps a
662
+ // collapsed streaming eval/bash/ssh whose box outgrows the viewport from
663
+ // stranding its head: while commit-unstable its scrolled-off top
664
+ // committed nowhere and repainted nowhere, so it read as truncated until
665
+ // ctrl+o (expanded) flipped it stable.
666
+ //
667
+ // Renderers whose partial-result chrome (header glyph, frame state)
668
+ // differs from the final result render set `provisionalPartialResult`
669
+ // to opt out of stream-commit while `isPartial` holds: the ratchet
670
+ // would otherwise promote the stable partial chrome to native scrollback
671
+ // after `STABLE_PREFIX_COMMIT_FRAMES` and leave it stranded above the
672
+ // final frame once the chrome flips. Once the result settles
673
+ // (`isPartial === false`) the block is commit-stable again.
674
+ if (this.#result !== undefined) {
675
+ if (this.#isPartial) {
676
+ const tool = this.#tool as { provisionalPartialResult?: boolean } | undefined;
677
+ const provisionalPartialResult =
678
+ tool?.provisionalPartialResult ?? toolRenderers[this.#toolName]?.provisionalPartialResult;
679
+ if (provisionalPartialResult) return false;
680
+ }
681
+ return true;
682
+ }
667
683
  const tool = this.#tool as { provisionalPendingPreview?: boolean | "collapsed" } | undefined;
668
684
  const provisionalPendingPreview =
669
685
  tool?.provisionalPendingPreview ?? toolRenderers[this.#toolName]?.provisionalPendingPreview;
@@ -15,8 +15,8 @@ import type { TreeFilterMode } from "../../config/settings-schema";
15
15
  import { theme } from "../../modes/theme/theme";
16
16
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
17
17
  import type { SessionTreeNode } from "../../session/session-entries";
18
+ import { toPathList } from "../../tools/grep";
18
19
  import { shortenPath } from "../../tools/render-utils";
19
- import { toPathList } from "../../tools/search";
20
20
  import { canonicalizeMessage } from "../../utils/thinking-display";
21
21
  import { DynamicBorder } from "./dynamic-border";
22
22
  import { centeredWindow, contentRowWidth, renderScrollableList } from "./selector-helpers";
@@ -740,7 +740,7 @@ class TreeList implements Component {
740
740
  .slice(0, 50);
741
741
  return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`;
742
742
  }
743
- case "search": {
743
+ case "grep": {
744
744
  const pattern = String(args.pattern || "");
745
745
  const searchPathsInput =
746
746
  typeof args.paths === "string" || Array.isArray(args.paths)
@@ -750,11 +750,11 @@ class TreeList implements Component {
750
750
  : undefined;
751
751
  const paths = toPathList(searchPathsInput);
752
752
  const scope = paths.length > 0 ? paths.join(", ") : ".";
753
- return `[search: /${pattern}/ in ${shortenPath(scope)}]`;
753
+ return `[grep: /${pattern}/ in ${shortenPath(scope)}]`;
754
754
  }
755
- case "find": {
755
+ case "glob": {
756
756
  const paths = Array.isArray(args.paths) ? args.paths.join(", ") : String(args.pattern || ".");
757
- return `[find: ${shortenPath(paths)}]`;
757
+ return `[glob: ${shortenPath(paths)}]`;
758
758
  }
759
759
  case "ls": {
760
760
  const path = shortenPath(String(args.path || "."));
@@ -6,6 +6,7 @@ import {
6
6
  getEnvApiKey,
7
7
  getProviderDetails,
8
8
  type ProviderDetails,
9
+ resolveUsedFraction,
9
10
  type UsageLimit,
10
11
  type UsageReport,
11
12
  } from "@oh-my-pi/pi-ai";
@@ -30,6 +31,7 @@ import { BashExecutionComponent } from "../../modes/components/bash-execution";
30
31
  import { BorderedLoader } from "../../modes/components/bordered-loader";
31
32
  import { DynamicBorder } from "../../modes/components/dynamic-border";
32
33
  import { EvalExecutionComponent } from "../../modes/components/eval-execution";
34
+ import { MoveOverlay, type MoveOverlayResult } from "../../modes/components/move-overlay";
33
35
  import { TranscriptBlock } from "../../modes/components/transcript-container";
34
36
  import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
35
37
  import type { InteractiveModeContext } from "../../modes/types";
@@ -40,6 +42,7 @@ import type { AsyncJobSnapshotItem } from "../../session/agent-session";
40
42
  import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
41
43
  import type { CompactMode } from "../../session/compact-modes";
42
44
  import type { NewSessionOptions } from "../../session/session-entries";
45
+ import { SessionManager } from "../../session/session-manager";
43
46
  import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
44
47
  import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
45
48
  import { outputMeta } from "../../tools/output-meta";
@@ -910,13 +913,34 @@ export class CommandController {
910
913
  ]);
911
914
  }
912
915
 
913
- async handleMoveCommand(targetPath: string): Promise<void> {
916
+ /**
917
+ * `/move` — switch to a fresh empty session in a different directory.
918
+ *
919
+ * With no `targetPath` (TUI only), opens an autocomplete overlay so the user
920
+ * can pick or type a directory. With a `targetPath`, resolves it directly.
921
+ * If the target directory does not exist, the user is asked whether to create
922
+ * it. A brand-new empty session is then started in the target directory and
923
+ * the current session is left behind (resumable via `/resume`).
924
+ */
925
+ async handleMoveCommand(targetPath?: string): Promise<void> {
914
926
  if (this.ctx.session.isStreaming) {
915
927
  this.ctx.showWarning("Wait for the current response to finish or abort it before moving.");
916
928
  return;
917
929
  }
918
930
 
919
- const unquoted = stripOuterDoubleQuotes(targetPath);
931
+ let input: string | undefined = targetPath?.trim() || undefined;
932
+
933
+ // No argument in TUI mode: open the path autocomplete overlay.
934
+ if (!input) {
935
+ const result = await this.ctx.showHookCustom<MoveOverlayResult | undefined>(
936
+ (_tui, _theme, _keybindings, done) => new MoveOverlay(this.ctx.sessionManager.getCwd(), done),
937
+ { overlay: true },
938
+ );
939
+ if (!result) return; // cancelled
940
+ input = result.directory;
941
+ }
942
+
943
+ const unquoted = stripOuterDoubleQuotes(input);
920
944
  if (!unquoted) {
921
945
  this.ctx.showError("Usage: /move <path>");
922
946
  return;
@@ -925,29 +949,85 @@ export class CommandController {
925
949
  const cwd = this.ctx.sessionManager.getCwd();
926
950
  const resolvedPath = resolveToCwd(unquoted, cwd);
927
951
 
952
+ // If the directory doesn't exist, offer to create it.
953
+ let isDirectory: boolean;
928
954
  try {
929
- const stat = await fs.stat(resolvedPath);
930
- if (!stat.isDirectory()) {
931
- this.ctx.showError(`Not a directory: ${resolvedPath}`);
955
+ isDirectory = (await fs.stat(resolvedPath)).isDirectory();
956
+ } catch {
957
+ isDirectory = false;
958
+ }
959
+
960
+ if (!isDirectory) {
961
+ const parentDir = path.dirname(resolvedPath);
962
+ let parentExists = false;
963
+ try {
964
+ parentExists = (await fs.stat(parentDir)).isDirectory();
965
+ } catch {
966
+ parentExists = false;
967
+ }
968
+ if (!parentExists) {
969
+ this.ctx.showError(`Cannot create "${path.basename(resolvedPath)}": parent directory does not exist`);
970
+ return;
971
+ }
972
+ const confirmed = await this.ctx.showHookConfirm(
973
+ "Create directory?",
974
+ `"${path.basename(resolvedPath)}" does not exist. Create it?`,
975
+ );
976
+ if (!confirmed) return;
977
+ try {
978
+ await fs.mkdir(resolvedPath, { recursive: true });
979
+ } catch (err) {
980
+ this.ctx.showError(`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`);
932
981
  return;
933
982
  }
934
- } catch {
935
- this.ctx.showError(`Directory does not exist: ${resolvedPath}`);
936
- return;
937
983
  }
938
984
 
985
+ let newSessionFile: string | undefined;
939
986
  try {
940
- await this.ctx.sessionManager.flush();
941
- await this.ctx.sessionManager.moveTo(resolvedPath);
942
- await this.ctx.applyCwdChange(resolvedPath);
943
-
944
- this.ctx.present([
945
- new Spacer(1),
946
- new Text(`${theme.fg("accent", `${theme.status.success} Session moved to ${resolvedPath}`)}`, 1, 1),
947
- ]);
987
+ // Create a fresh empty session file in the target directory's session
988
+ // folder, then switch to it. The current session is left behind and
989
+ // remains resumable via /resume.
990
+ newSessionFile = SessionManager.createEmptySessionFile(resolvedPath);
991
+ const switched = await this.ctx.session.switchSession(newSessionFile);
992
+ if (!switched) {
993
+ await this.ctx.sessionManager.dropSession(newSessionFile);
994
+ return;
995
+ }
948
996
  } catch (err) {
997
+ if (newSessionFile) {
998
+ try {
999
+ await this.ctx.sessionManager.dropSession(newSessionFile);
1000
+ } catch (dropErr) {
1001
+ this.ctx.showError(
1002
+ `Move failed: ${err instanceof Error ? err.message : String(err)}; failed to remove empty session: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
1003
+ );
1004
+ return;
1005
+ }
1006
+ }
949
1007
  this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
1008
+ return;
950
1009
  }
1010
+
1011
+ this.ctx.session.markMovedFromEmptySessionFile(newSessionFile!);
1012
+ await this.ctx.applyCwdChange(resolvedPath);
1013
+
1014
+ this.ctx.chatContainer.clear();
1015
+ this.ctx.pendingMessagesContainer.clear();
1016
+ this.ctx.compactionQueuedMessages = [];
1017
+ this.ctx.streamingComponent = undefined;
1018
+ this.ctx.streamingMessage = undefined;
1019
+ this.ctx.pendingTools.clear();
1020
+ this.ctx.statusLine.invalidate();
1021
+ this.ctx.statusLine.setSessionStartTime(Date.now());
1022
+ this.ctx.updateEditorTopBorder();
1023
+ this.ctx.updateEditorBorderColor();
1024
+ await this.ctx.reloadTodos();
1025
+ this.ctx.ui.requestRender(true, { clearScrollback: true });
1026
+
1027
+ this.ctx.present([
1028
+ new Spacer(1),
1029
+ new Text(`${theme.fg("accent", `${theme.status.success} Moved to ${resolvedPath}`)}`, 1, 1),
1030
+ ]);
951
1031
  }
952
1032
 
953
1033
  async handleRenameCommand(title: string): Promise<void> {
@@ -1306,22 +1386,10 @@ export function renderProviderSection(details: ProviderDetails, uiTheme: Pick<ty
1306
1386
  return `${lines.join("\n")}\n`;
1307
1387
  }
1308
1388
 
1309
- function resolveFraction(limit: UsageLimit): number | undefined {
1310
- const amount = limit.amount;
1311
- if (amount.usedFraction !== undefined) return amount.usedFraction;
1312
- if (amount.used !== undefined && amount.limit !== undefined && amount.limit > 0) {
1313
- return amount.used / amount.limit;
1314
- }
1315
- if (amount.unit === "percent" && amount.used !== undefined) {
1316
- return amount.used / 100;
1317
- }
1318
- return undefined;
1319
- }
1320
-
1321
1389
  function resolveProviderUsageTotal(reports: UsageReport[]): number {
1322
1390
  return reports
1323
1391
  .flatMap(report => report.limits)
1324
- .map(limit => resolveFraction(limit) ?? 0)
1392
+ .map(limit => resolveUsedFraction(limit) ?? 0)
1325
1393
  .reduce((sum, value) => sum + value, 0);
1326
1394
  }
1327
1395
 
@@ -1342,22 +1410,28 @@ function formatWindowSuffix(label: string, windowLabel: string, uiTheme: typeof
1342
1410
  }
1343
1411
 
1344
1412
  function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: number): string {
1345
- const email = (report.metadata?.email as string | undefined) ?? limit.scope.accountId;
1346
- if (email) return email;
1347
- const accountId = (report.metadata?.accountId as string | undefined) ?? limit.scope.accountId;
1413
+ const email = report.metadata?.email;
1414
+ if (typeof email === "string" && email) return email;
1415
+ const accountId =
1416
+ typeof report.metadata?.accountId === "string" && report.metadata.accountId
1417
+ ? report.metadata.accountId
1418
+ : limit.scope.accountId || undefined;
1348
1419
  if (accountId) return accountId;
1349
- const projectId = (report.metadata?.projectId as string | undefined) ?? limit.scope.projectId;
1420
+ const projectId =
1421
+ typeof report.metadata?.projectId === "string" && report.metadata.projectId
1422
+ ? report.metadata.projectId
1423
+ : limit.scope.projectId || undefined;
1350
1424
  if (projectId) return projectId;
1351
1425
  return `account ${index + 1}`;
1352
1426
  }
1353
1427
 
1354
1428
  function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
1355
- const email = report.metadata?.email as string | undefined;
1356
- if (email) return email;
1357
- const accountId = report.metadata?.accountId as string | undefined;
1358
- if (accountId) return accountId;
1359
- const projectId = report.metadata?.projectId as string | undefined;
1360
- if (projectId) return projectId;
1429
+ const email = report.metadata?.email;
1430
+ if (typeof email === "string" && email) return email;
1431
+ const accountId = report.metadata?.accountId;
1432
+ if (typeof accountId === "string" && accountId) return accountId;
1433
+ const projectId = report.metadata?.projectId;
1434
+ if (typeof projectId === "string" && projectId) return projectId;
1361
1435
  return `account ${index + 1}`;
1362
1436
  }
1363
1437
 
@@ -1432,7 +1506,7 @@ function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
1432
1506
 
1433
1507
  function formatAggregateAmount(limits: UsageLimit[]): string {
1434
1508
  const fractions = limits
1435
- .map(limit => resolveFraction(limit))
1509
+ .map(limit => resolveUsedFraction(limit))
1436
1510
  .filter((value): value is number => value !== undefined);
1437
1511
  if (fractions.length === limits.length && fractions.length > 0) {
1438
1512
  const sum = fractions.reduce((total, value) => total + value, 0);
@@ -1489,7 +1563,7 @@ function resolveStatusColor(status: UsageLimit["status"]): "success" | "warning"
1489
1563
  }
1490
1564
 
1491
1565
  function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: number): string {
1492
- const fraction = resolveFraction(limit);
1566
+ const fraction = resolveUsedFraction(limit);
1493
1567
  if (fraction === undefined) {
1494
1568
  return uiTheme.fg("dim", "·".repeat(barWidth));
1495
1569
  }
@@ -1597,9 +1671,11 @@ export function renderUsageReports(
1597
1671
  const count = report.resetCredits?.availableCount ?? 0;
1598
1672
  if (count <= 0) continue;
1599
1673
  const label =
1600
- (report.metadata?.email as string | undefined) ??
1601
- (report.metadata?.accountId as string | undefined) ??
1602
- "account";
1674
+ typeof report.metadata?.email === "string" && report.metadata.email
1675
+ ? report.metadata.email
1676
+ : typeof report.metadata?.accountId === "string" && report.metadata.accountId
1677
+ ? report.metadata.accountId
1678
+ : "account";
1603
1679
  const isActive =
1604
1680
  !!activeAccount &&
1605
1681
  ((!!activeAccount.accountId && activeAccount.accountId === report.metadata?.accountId) ||
@@ -1607,6 +1683,23 @@ export function renderUsageReports(
1607
1683
  resetAccountLines.push(
1608
1684
  ` • ${label}: ${count} saved reset${count === 1 ? "" : "s"}${isActive ? " (active)" : ""}`,
1609
1685
  );
1686
+ const credits = report.resetCredits?.credits;
1687
+ if (credits) {
1688
+ for (const credit of credits) {
1689
+ if (credit.expiresAt) {
1690
+ const expiryMs = Date.parse(credit.expiresAt);
1691
+ if (!Number.isNaN(expiryMs)) {
1692
+ const remaining = expiryMs - nowMs;
1693
+ const expiryDate = credit.expiresAt.slice(0, 10);
1694
+ if (remaining > 0) {
1695
+ resetAccountLines.push(` expires in ${formatDuration(remaining)} (${expiryDate})`);
1696
+ } else {
1697
+ resetAccountLines.push(` expired (${expiryDate})`);
1698
+ }
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1610
1703
  }
1611
1704
  if (resetAccountLines.length > 0) {
1612
1705
  lines.push(
@@ -1619,7 +1712,7 @@ export function renderUsageReports(
1619
1712
  const entries = group.limits.map((limit, index) => ({
1620
1713
  limit,
1621
1714
  report: group.reports[index],
1622
- fraction: resolveFraction(limit),
1715
+ fraction: resolveUsedFraction(limit),
1623
1716
  index,
1624
1717
  }));
1625
1718
  entries.sort((a, b) => {
@@ -1672,8 +1765,8 @@ export function renderUsageReports(
1672
1765
  const unlimitedReports = providerReports.filter(report => report.limits.length === 0);
1673
1766
  for (const report of unlimitedReports) {
1674
1767
  const label = formatUnlimitedReportLabel(report, 0);
1675
- const tier = report.metadata?.planType as string | undefined;
1676
- const tierSuffix = tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
1768
+ const tier = report.metadata?.planType;
1769
+ const tierSuffix = typeof tier === "string" && tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
1677
1770
  lines.push(
1678
1771
  `${uiTheme.fg("success", uiTheme.status.success)} ${label}${tierSuffix} ${uiTheme.fg("dim", "-- no limits")}`,
1679
1772
  );
@@ -5,7 +5,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
5
5
  import { extractTextContent } from "../../commit/utils";
6
6
  import { settings } from "../../config/settings";
7
7
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
8
- import type { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
+ import { AssistantMessageComponent } from "../../modes/components/assistant-message";
9
9
  import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
10
10
  import {
11
11
  ReadToolGroupComponent,
@@ -86,12 +86,14 @@ export class EventController {
86
86
  #lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
87
87
  #streamingReveal: StreamingRevealController;
88
88
  #toolArgsReveal: ToolArgsRevealController;
89
+ #prevHideThinking = false;
89
90
  #handlers: AgentSessionEventHandlers;
91
+ #terminalProgressActive = false;
90
92
 
91
93
  constructor(private ctx: InteractiveModeContext) {
92
94
  this.#streamingReveal = new StreamingRevealController({
93
95
  getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
94
- getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
96
+ getHideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
95
97
  getProseOnlyThinking: () => this.ctx.proseOnlyThinking,
96
98
  requestRender: () => this.ctx.ui.requestRender(),
97
99
  });
@@ -124,7 +126,27 @@ export class EventController {
124
126
  thinking_level_changed: async () => {
125
127
  this.ctx.statusLine.invalidate();
126
128
  this.ctx.updateEditorBorderColor();
127
- this.ctx.ui.requestRender();
129
+ const hideThinking = this.ctx.effectiveHideThinkingBlock;
130
+ // Only do the expensive full resetDisplay when the effective
131
+ // visibility actually changed. Auto-classification (e.g. high→medium)
132
+ // emits thinking_level_changed without changing visibility — a full
133
+ // terminal replay for those would be disruptive.
134
+ if (hideThinking === this.#prevHideThinking) {
135
+ this.ctx.ui.requestRender();
136
+ return;
137
+ }
138
+ this.#prevHideThinking = hideThinking;
139
+ // Propagate visibility to existing rendered messages.
140
+ for (const child of this.ctx.chatContainer.children) {
141
+ if (child instanceof AssistantMessageComponent) {
142
+ child.setHideThinkingBlock(hideThinking);
143
+ }
144
+ }
145
+ if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
146
+ this.ctx.streamingComponent.setHideThinkingBlock(hideThinking);
147
+ this.#streamingReveal.resyncVisibility();
148
+ }
149
+ this.ctx.ui.resetDisplay();
128
150
  },
129
151
  goal_updated: async () => {},
130
152
  } satisfies AgentSessionEventHandlers;
@@ -134,6 +156,7 @@ export class EventController {
134
156
  this.#streamingReveal.stop();
135
157
  this.#toolArgsReveal.stop();
136
158
  this.#cancelIdleCompaction();
159
+ this.#setTerminalProgress(false);
137
160
  for (const timer of this.#ircExpiryTimers.values()) {
138
161
  clearTimeout(timer);
139
162
  }
@@ -248,6 +271,18 @@ export class EventController {
248
271
  await run(event);
249
272
  }
250
273
 
274
+ #setTerminalProgress(active: boolean): void {
275
+ if (active) {
276
+ if (this.#terminalProgressActive || this.ctx.settings?.get("terminal.showProgress") !== true) return;
277
+ this.ctx.ui.terminal.setProgress(true);
278
+ this.#terminalProgressActive = true;
279
+ return;
280
+ }
281
+ if (!this.#terminalProgressActive) return;
282
+ this.ctx.ui.terminal.setProgress(false);
283
+ this.#terminalProgressActive = false;
284
+ }
285
+
251
286
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
252
287
  this.#lastIntent = undefined;
253
288
  this.#readToolCallArgs.clear();
@@ -266,11 +301,13 @@ export class EventController {
266
301
  this.ctx.statusContainer.clear();
267
302
  }
268
303
  this.#cancelIdleCompaction();
304
+ this.#setTerminalProgress(true);
269
305
  this.ctx.ensureLoadingAnimation();
270
306
  this.ctx.ui.requestRender();
271
307
  }
272
308
 
273
309
  async #handleMessageStart(event: Extract<AgentSessionEvent, { type: "message_start" }>): Promise<void> {
310
+ this.#ensureWorkingLoaderWhileStreaming();
274
311
  if (event.message.role === "hookMessage" || event.message.role === "custom") {
275
312
  const signature = `${event.message.role}:${event.message.customType}:${event.message.timestamp}`;
276
313
  if (this.#renderedCustomMessages.has(signature)) {
@@ -513,6 +550,7 @@ export class EventController {
513
550
  }
514
551
 
515
552
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
553
+ this.#ensureWorkingLoaderWhileStreaming();
516
554
  this.#vocalizeDelta(event);
517
555
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
518
556
  this.ctx.streamingMessage = event.message;
@@ -737,6 +775,7 @@ export class EventController {
737
775
  }
738
776
 
739
777
  async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
778
+ this.#ensureWorkingLoaderWhileStreaming();
740
779
  this.#updateWorkingMessageFromIntent(event.intent);
741
780
  this.#resolveDisplaceablePoll(event.toolName);
742
781
  if (!this.ctx.pendingTools.has(event.toolCallId)) {
@@ -801,6 +840,7 @@ export class EventController {
801
840
  async #handleToolExecutionUpdate(
802
841
  event: Extract<AgentSessionEvent, { type: "tool_execution_update" }>,
803
842
  ): Promise<void> {
843
+ this.#ensureWorkingLoaderWhileStreaming();
804
844
  const component = this.ctx.pendingTools.get(event.toolCallId);
805
845
  if (component) {
806
846
  const asyncState = (event.partialResult.details as { async?: { state?: string } } | undefined)?.async?.state;
@@ -929,6 +969,7 @@ export class EventController {
929
969
  }
930
970
 
931
971
  async #finishAgentEnd(): Promise<void> {
972
+ this.#setTerminalProgress(false);
932
973
  this.#streamingReveal.stop();
933
974
  this.#toolArgsReveal.flushAll();
934
975
  if (this.ctx.loadingAnimation) {
@@ -989,6 +1030,17 @@ export class EventController {
989
1030
  }
990
1031
  }
991
1032
 
1033
+ /**
1034
+ * Restore the live "Working…" loader when a streaming event lands after a
1035
+ * transient status overlay cleared the container. Focus mode dispatches events
1036
+ * for `viewSession`, so key the reconciler on that session, not the main one.
1037
+ */
1038
+ #ensureWorkingLoaderWhileStreaming(): void {
1039
+ if (!this.ctx.viewSession.isStreaming) return;
1040
+ if (this.ctx.autoCompactionLoader || this.ctx.retryLoader) return;
1041
+ this.ctx.ensureLoadingAnimation();
1042
+ }
1043
+
992
1044
  /**
993
1045
  * Trailing Esc hint for live maintenance loaders. While a subagent is
994
1046
  * focused, Esc returns to main instead of cancelling its maintenance
@@ -1004,6 +1056,7 @@ export class EventController {
1004
1056
  event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
1005
1057
  ): Promise<void> {
1006
1058
  this.#cancelIdleCompaction();
1059
+ this.#setTerminalProgress(true);
1007
1060
  this.#stopWorkingLoader();
1008
1061
  this.ctx.statusContainer.clear();
1009
1062
  const reasonText =
@@ -1035,6 +1088,7 @@ export class EventController {
1035
1088
 
1036
1089
  async #handleAutoCompactionEnd(event: Extract<AgentSessionEvent, { type: "auto_compaction_end" }>): Promise<void> {
1037
1090
  this.#cancelIdleCompaction();
1091
+ this.#setTerminalProgress(false);
1038
1092
  if (this.ctx.autoCompactionLoader) {
1039
1093
  this.ctx.autoCompactionLoader.stop();
1040
1094
  this.ctx.autoCompactionLoader = undefined;
@@ -1096,6 +1150,7 @@ export class EventController {
1096
1150
  this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
1097
1151
  }
1098
1152
  await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
1153
+ this.#ensureWorkingLoaderWhileStreaming();
1099
1154
  this.ctx.ui.requestRender();
1100
1155
  }
1101
1156
 
@@ -1130,6 +1185,7 @@ export class EventController {
1130
1185
  if (!event.success) {
1131
1186
  this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
1132
1187
  }
1188
+ this.#ensureWorkingLoaderWhileStreaming();
1133
1189
  this.ctx.ui.requestRender();
1134
1190
  }
1135
1191