@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.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 (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
3
  import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
3
4
  import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
4
5
  import { getRoleInfo } from "../../config/model-registry";
@@ -9,16 +10,16 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
9
10
  import { materializeImageReferenceLinks } from "../../modes/image-references";
10
11
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
11
12
  import type { InteractiveModeContext } from "../../modes/types";
12
- import type { AgentSessionEvent } from "../../session/agent-session";
13
- import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
13
+ import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails, USER_INTERRUPT_LABEL } from "../../session/messages";
14
14
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
15
15
  import { isTinyTitleLocalModelKey } from "../../tiny/models";
16
16
  import { isLowSignalTitleInput } from "../../tiny/text";
17
17
  import { tinyTitleClient } from "../../tiny/title-client";
18
18
  import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
19
19
  import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
20
+ import { EnhancedPasteController } from "../../utils/enhanced-paste";
20
21
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
21
- import { ensureSupportedImageInput } from "../../utils/image-loading";
22
+ import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
22
23
  import { resizeImage } from "../../utils/image-resize";
23
24
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
24
25
 
@@ -40,8 +41,10 @@ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
40
41
  export class InputController {
41
42
  constructor(private ctx: InteractiveModeContext) {}
42
43
 
44
+ #enhancedPaste?: EnhancedPasteController;
45
+
43
46
  #showTinyTitleDownloadProgress(modelKey: string): void {
44
- if (!isTinyTitleLocalModelKey(modelKey) || this.ctx.isBackgrounded) return;
47
+ if (!isTinyTitleLocalModelKey(modelKey)) return;
45
48
  const component = new TinyTitleDownloadProgressComponent(modelKey);
46
49
  let added = false;
47
50
  let disposed = false;
@@ -91,7 +94,7 @@ export class InputController {
91
94
  if (this.ctx.loopModeEnabled) {
92
95
  this.ctx.pauseLoop();
93
96
  if (this.ctx.session.isStreaming) {
94
- void this.ctx.session.abort();
97
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
95
98
  } else {
96
99
  this.ctx.cancelPendingSubmission();
97
100
  }
@@ -121,7 +124,7 @@ export class InputController {
121
124
  this.ctx.isPythonMode = false;
122
125
  this.ctx.updateEditorBorderColor();
123
126
  } else if (this.ctx.session.isStreaming) {
124
- void this.ctx.session.abort();
127
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
125
128
  } else if (!this.ctx.editor.getText().trim()) {
126
129
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
127
130
  const action = settings.get("doubleEscapeAction");
@@ -144,6 +147,8 @@ export class InputController {
144
147
  this.ctx.editor.setActionKeys("app.clear", this.ctx.keybindings.getKeys("app.clear"));
145
148
  this.ctx.editor.onClear = () => this.handleCtrlC();
146
149
  this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
150
+ this.ctx.editor.setActionKeys("app.display.reset", this.ctx.keybindings.getKeys("app.display.reset"));
151
+ this.ctx.editor.onDisplayReset = () => this.ctx.ui.resetDisplay();
147
152
  this.ctx.editor.onExit = () => this.handleCtrlD();
148
153
  this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
149
154
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
@@ -174,6 +179,7 @@ export class InputController {
174
179
  this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
175
180
  );
176
181
  this.ctx.editor.onPasteImage = () => this.handleImagePaste();
182
+ this.ctx.editor.onPasteImagePath = path => void this.handleImagePathPaste(path);
177
183
  this.ctx.editor.setActionKeys(
178
184
  "app.clipboard.pasteTextRaw",
179
185
  this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
@@ -188,11 +194,9 @@ export class InputController {
188
194
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
189
195
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
190
196
  this.ctx.editor.onDequeue = () => this.handleDequeue();
191
-
192
197
  this.ctx.editor.clearCustomKeyHandlers();
193
198
  // Wire up extension shortcuts
194
199
  this.registerExtensionShortcuts();
195
-
196
200
  const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
197
201
  for (const key of planModeKeys) {
198
202
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
@@ -223,6 +227,8 @@ export class InputController {
223
227
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
224
228
  }
225
229
 
230
+ this.#setupEnhancedPaste();
231
+
226
232
  this.ctx.editor.onChange = (text: string) => {
227
233
  const wasBashMode = this.ctx.isBashMode;
228
234
  const wasPythonMode = this.ctx.isPythonMode;
@@ -235,16 +241,45 @@ export class InputController {
235
241
  };
236
242
  }
237
243
 
244
+ #setupEnhancedPaste(): void {
245
+ if (this.#enhancedPaste) return;
246
+
247
+ this.#enhancedPaste = new EnhancedPasteController({
248
+ write: data => this.ctx.ui.terminal.write(data),
249
+ pasteText: text => {
250
+ this.ctx.editor.pasteText(text);
251
+ this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
252
+ },
253
+ pasteImage: async image => {
254
+ await this.#normalizeAndInsertPastedImage(image, `Unsupported pasted image format: ${image.mimeType}`);
255
+ },
256
+ showStatus: message => this.ctx.showStatus(message),
257
+ });
258
+ this.ctx.ui.addInputListener(data => (this.#enhancedPaste?.handleInput(data) ? { consume: true } : undefined));
259
+ this.ctx.ui.addStartListener(() => this.#enhancedPaste?.enable());
260
+ }
261
+
238
262
  setupEditorSubmitHandler(): void {
239
263
  this.ctx.editor.onSubmit = async (text: string) => {
240
264
  text = text.trim();
241
265
  if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
242
266
 
243
- // Empty submit while streaming with queued messages: flush queues immediately
244
- if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
245
- // Abort current stream and let queued messages be processed
246
- await this.ctx.session.abort();
247
- return;
267
+ // Empty submit while streaming with queued steering: interrupt now and
268
+ // immediately resume so the visible `Steer:` entry is sent without
269
+ // waiting for the current tool/model boundary.
270
+ if (!text && this.ctx.session.isStreaming) {
271
+ const queuedMessages = this.ctx.session.getQueuedMessages();
272
+ if (queuedMessages.steering.length > 0) {
273
+ await this.ctx.session.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
274
+ this.ctx.updatePendingMessagesDisplay();
275
+ this.ctx.ui.requestRender();
276
+ return;
277
+ }
278
+ if (this.ctx.session.queuedMessageCount > 0) {
279
+ // Preserve the existing empty-submit flush for non-steer queues.
280
+ await this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
281
+ return;
282
+ }
248
283
  }
249
284
 
250
285
  if (!text) return;
@@ -291,7 +326,6 @@ export class InputController {
291
326
  // Handle built-in slash commands
292
327
  const slashResult = await executeBuiltinSlashCommand(text, {
293
328
  ctx: this.ctx,
294
- handleBackgroundCommand: () => this.handleBackgroundCommand(),
295
329
  });
296
330
  if (slashResult === true) {
297
331
  return;
@@ -600,7 +634,7 @@ export class InputController {
600
634
  if (allQueued.length === 0) {
601
635
  this.ctx.updatePendingMessagesDisplay();
602
636
  if (options?.abort) {
603
- this.ctx.session.abort();
637
+ this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
604
638
  }
605
639
  return 0;
606
640
  }
@@ -610,128 +644,99 @@ export class InputController {
610
644
  this.ctx.editor.setText(combinedText);
611
645
  this.ctx.updatePendingMessagesDisplay();
612
646
  if (options?.abort) {
613
- this.ctx.session.abort();
647
+ this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
614
648
  }
615
649
  return allQueued.length;
616
650
  }
617
651
 
618
- handleBackgroundCommand(): void {
619
- if (this.ctx.isBackgrounded) {
620
- this.ctx.showStatus("Background mode already enabled");
621
- return;
622
- }
623
- if (!this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount === 0) {
624
- this.ctx.showWarning("Agent is idle; nothing to background");
625
- return;
626
- }
627
- if (this.ctx.hasActiveBtw()) {
628
- this.ctx.handleBtwEscape();
629
- }
630
- if (this.ctx.hasActiveOmfg()) {
631
- this.ctx.handleOmfgEscape();
632
- }
633
-
634
- this.ctx.isBackgrounded = true;
635
- const backgroundUiContext = this.ctx.createBackgroundUiContext();
636
-
637
- // Background mode disables interactive UI so tools like ask fail fast.
638
- this.ctx.setToolUIContext(backgroundUiContext, false);
639
- this.ctx.initializeHookRunner(backgroundUiContext, false);
640
-
641
- if (this.ctx.loadingAnimation) {
642
- this.ctx.loadingAnimation.stop();
643
- this.ctx.loadingAnimation = undefined;
644
- }
645
- if (this.ctx.autoCompactionLoader) {
646
- this.ctx.autoCompactionLoader.stop();
647
- this.ctx.autoCompactionLoader = undefined;
648
- }
649
- if (this.ctx.retryLoader) {
650
- this.ctx.retryLoader.stop();
651
- this.ctx.retryLoader = undefined;
652
- }
653
- this.ctx.statusContainer.clear();
654
- this.ctx.statusLine.dispose();
655
-
656
- if (this.ctx.unsubscribe) {
657
- this.ctx.unsubscribe();
658
- }
659
- this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
660
- await this.ctx.handleBackgroundEvent(event);
652
+ async #insertPendingImage(imageData: ImageContent): Promise<void> {
653
+ const imageLink = (
654
+ await materializeImageReferenceLinks(
655
+ [
656
+ {
657
+ type: "image",
658
+ data: imageData.data,
659
+ mimeType: imageData.mimeType,
660
+ },
661
+ ],
662
+ this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
663
+ )
664
+ )?.[0];
665
+ this.ctx.pendingImages.push({
666
+ type: "image",
667
+ data: imageData.data,
668
+ mimeType: imageData.mimeType,
661
669
  });
670
+ this.ctx.pendingImageLinks.push(imageLink);
671
+ this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
672
+ const imageNum = this.ctx.pendingImages.length;
673
+ this.ctx.editor.insertText(`[Image #${imageNum}] `);
674
+ this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
675
+ }
662
676
 
663
- // Backgrounding keeps the current process to preserve in-flight agent state.
664
- if (this.ctx.isInitialized) {
665
- this.ctx.ui.stop();
666
- this.ctx.isInitialized = false;
677
+ async #normalizeAndInsertPastedImage(image: ImageContent, unsupportedMessage: string): Promise<boolean> {
678
+ let imageData = await ensureSupportedImageInput(image);
679
+ if (!imageData) {
680
+ this.ctx.showStatus(unsupportedMessage);
681
+ return false;
667
682
  }
668
-
669
- process.stdout.write("Background mode enabled. Run `bg` to continue in background.\n");
670
-
671
- if (process.platform === "win32" || !process.stdout.isTTY) {
672
- process.stdout.write("Backgrounding requires POSIX job control; continuing in foreground.\n");
673
- return;
683
+ if (settings.get("images.autoResize")) {
684
+ try {
685
+ const resized = await resizeImage({
686
+ type: "image",
687
+ data: imageData.data,
688
+ mimeType: imageData.mimeType,
689
+ });
690
+ imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
691
+ } catch {
692
+ // Keep the normalized image when resize fails.
693
+ }
674
694
  }
695
+ await this.#insertPendingImage(imageData);
696
+ return true;
697
+ }
675
698
 
676
- process.kill(0, "SIGTSTP");
699
+ async handleImagePathPaste(path: string): Promise<void> {
700
+ try {
701
+ const image = await loadImageInput({
702
+ path,
703
+ cwd: this.ctx.sessionManager.getCwd(),
704
+ autoResize: false,
705
+ });
706
+ if (!image) {
707
+ this.ctx.editor.pasteText(path);
708
+ this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
709
+ this.ctx.showStatus("Pasted path is not a supported image");
710
+ return;
711
+ }
712
+ await this.#normalizeAndInsertPastedImage(
713
+ { type: "image", data: image.data, mimeType: image.mimeType },
714
+ `Unsupported pasted image format: ${image.mimeType}`,
715
+ );
716
+ } catch (error) {
717
+ this.ctx.editor.pasteText(path);
718
+ this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
719
+ this.ctx.showStatus(
720
+ error instanceof ImageInputTooLargeError ? error.message : "Failed to read pasted image path",
721
+ );
722
+ }
677
723
  }
678
724
 
679
725
  async handleImagePaste(): Promise<boolean> {
680
726
  try {
681
727
  const image = await readImageFromClipboard();
682
- if (image) {
683
- const base64Data = image.data.toBase64();
684
- let imageData = await ensureSupportedImageInput({
728
+ if (!image) {
729
+ this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
730
+ return false;
731
+ }
732
+ return await this.#normalizeAndInsertPastedImage(
733
+ {
685
734
  type: "image",
686
- data: base64Data,
735
+ data: image.data.toBase64(),
687
736
  mimeType: image.mimeType,
688
- });
689
- if (!imageData) {
690
- this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
691
- return false;
692
- }
693
- if (settings.get("images.autoResize")) {
694
- try {
695
- const resized = await resizeImage({
696
- type: "image",
697
- data: imageData.data,
698
- mimeType: imageData.mimeType,
699
- });
700
- imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
701
- } catch {
702
- // Keep the normalized image when resize fails.
703
- }
704
- }
705
-
706
- const imageLink = (
707
- await materializeImageReferenceLinks(
708
- [
709
- {
710
- type: "image",
711
- data: imageData.data,
712
- mimeType: imageData.mimeType,
713
- },
714
- ],
715
- this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
716
- )
717
- )?.[0];
718
- this.ctx.pendingImages.push({
719
- type: "image",
720
- data: imageData.data,
721
- mimeType: imageData.mimeType,
722
- });
723
- this.ctx.pendingImageLinks.push(imageLink);
724
- this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
725
- // Insert placeholder at cursor like Claude does
726
- const imageNum = this.ctx.pendingImages.length;
727
- const placeholder = `[Image #${imageNum}]`;
728
- this.ctx.editor.insertText(`${placeholder} `);
729
- this.ctx.ui.requestRender();
730
- return true;
731
- }
732
- // No image in clipboard - show hint
733
- this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
734
- return false;
737
+ },
738
+ `Unsupported clipboard image format: ${image.mimeType}`,
739
+ );
735
740
  } catch {
736
741
  this.ctx.showStatus("Failed to read clipboard");
737
742
  return false;
@@ -846,7 +851,15 @@ export class InputController {
846
851
  child.setExpanded(expanded);
847
852
  }
848
853
  }
849
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
854
+ // Toggling expansion mutates every block, but on ED3-risk terminals the
855
+ // transcript freezes a snapshot of each block once it scrolls past the live
856
+ // region (committed native scrollback is immutable there). A plain repaint
857
+ // replays those stale snapshots, so the toggle appears to do nothing above
858
+ // the live block. resetDisplay() invalidates the snapshots and forces a
859
+ // full clear + replay — the keyboard-accessible resize-reset equivalent —
860
+ // which is the only path that re-emits the whole transcript at its new
861
+ // heights.
862
+ this.ctx.ui.resetDisplay();
850
863
  }
851
864
 
852
865
  toggleThinkingBlockVisibility(): void {
@@ -37,7 +37,9 @@ import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../
37
37
  import type { OAuthCredential } from "../../session/auth-storage";
38
38
  import { shortenPath } from "../../tools/render-utils";
39
39
  import { openPath } from "../../utils/open";
40
+ import { ChatBlock } from "../components/chat-block";
40
41
  import { MCPAddWizard } from "../components/mcp-add-wizard";
42
+ import { TranscriptBlock } from "../components/transcript-container";
41
43
  import { parseCommandArgs } from "../shared";
42
44
  import { theme } from "../theme/theme";
43
45
  import type { InteractiveModeContext } from "../types";
@@ -49,6 +51,42 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string)
49
51
  return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
50
52
  }
51
53
 
54
+ /**
55
+ * Animated "Connecting to …" transcript block. Owns its spinner interval: it
56
+ * starts on mount and is cleared on {@link ChatBlock.finish}/dispose, so callers
57
+ * never juggle `setInterval`/`clearInterval` or `requestRender` by hand.
58
+ */
59
+ class McpConnectingBlock extends ChatBlock {
60
+ readonly #text: Text;
61
+
62
+ constructor(private readonly serverName: string) {
63
+ super();
64
+ this.addChild(new Spacer(1));
65
+ const frame = theme.spinnerFrames[0] ?? "|";
66
+ this.#text = new Text(theme.fg("muted", `${frame} Connecting to "${serverName}"...`), 1, 0);
67
+ this.addChild(this.#text);
68
+ }
69
+
70
+ protected override onMount(): void {
71
+ const frames = theme.spinnerFrames;
72
+ let frame = 0;
73
+ const interval = setInterval(() => {
74
+ frame++;
75
+ this.#text.setText(
76
+ theme.fg("muted", `${frames[frame % frames.length] ?? "|"} Connecting to "${this.serverName}"...`),
77
+ );
78
+ this.requestRender();
79
+ }, 80);
80
+ this.onCleanup(() => clearInterval(interval));
81
+ }
82
+
83
+ /** Replace the spinner line with a terminal status; pair with {@link finish}. */
84
+ setStatus(text: string): void {
85
+ this.#text.setText(text);
86
+ this.requestRender();
87
+ }
88
+ }
89
+
52
90
  /**
53
91
  * Outcome of {@link MCPCommandController}'s OAuth handler.
54
92
  *
@@ -547,63 +585,45 @@ export class MCPCommandController {
547
585
  },
548
586
  {
549
587
  onAuth: (info: { url: string; instructions?: string }) => {
550
- // Show auth URL prominently in chat
551
- this.ctx.chatContainer.addChild(new Spacer(1));
552
- this.ctx.chatContainer.addChild(
553
- new Text(theme.fg("accent", "━━━ OAuth Authorization Required ━━━"), 1, 0),
554
- );
555
- this.ctx.chatContainer.addChild(new Spacer(1));
556
- this.ctx.chatContainer.addChild(
557
- new Text(theme.fg("muted", "Preparing browser authorization..."), 1, 0),
558
- );
559
- this.ctx.chatContainer.addChild(new Spacer(1));
560
- this.ctx.chatContainer.addChild(
588
+ // Show auth URL prominently in chat as one block
589
+ const block = new TranscriptBlock();
590
+ this.ctx.present(block);
591
+ block.addChild(new Text(theme.fg("accent", "━━━ OAuth Authorization Required ━━━"), 1, 0));
592
+ block.addChild(new Spacer(1));
593
+ block.addChild(new Text(theme.fg("muted", "Preparing browser authorization..."), 1, 0));
594
+ block.addChild(new Spacer(1));
595
+ block.addChild(
561
596
  new Text(
562
597
  theme.fg("muted", "Waiting for authorization... (Press Ctrl+C to cancel, 5 minute timeout)"),
563
598
  1,
564
599
  0,
565
600
  ),
566
601
  );
567
- this.ctx.chatContainer.addChild(new Spacer(1));
568
- this.ctx.chatContainer.addChild(
569
- new Text(theme.fg("accent", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), 1, 0),
570
- );
571
- this.ctx.ui.requestRender();
602
+ block.addChild(new Spacer(1));
603
+ block.addChild(new Text(theme.fg("accent", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), 1, 0));
572
604
  // Try to open browser automatically
573
605
  try {
574
606
  openPath(info.url);
575
607
 
576
608
  // Show confirmation that browser should open
577
- this.ctx.chatContainer.addChild(new Spacer(1));
578
- this.ctx.chatContainer.addChild(
579
- new Text(theme.fg("success", "→ Opening browser automatically..."), 1, 0),
580
- );
581
- this.ctx.chatContainer.addChild(new Spacer(1));
582
- this.ctx.chatContainer.addChild(
583
- new Text(theme.fg("muted", "Alternative if browser did not open:"), 1, 0),
584
- );
585
- this.ctx.chatContainer.addChild(
586
- new Text(theme.fg("success", "Copy this exact URL in your browser:"), 1, 0),
587
- );
588
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", info.url), 1, 0));
609
+ block.addChild(new Spacer(1));
610
+ block.addChild(new Text(theme.fg("success", "→ Opening browser automatically..."), 1, 0));
611
+ block.addChild(new Spacer(1));
612
+ block.addChild(new Text(theme.fg("muted", "Alternative if browser did not open:"), 1, 0));
613
+ block.addChild(new Text(theme.fg("success", "Copy this exact URL in your browser:"), 1, 0));
614
+ block.addChild(new Text(theme.fg("accent", info.url), 1, 0));
589
615
  this.ctx.ui.requestRender();
590
616
  } catch (_error) {
591
617
  // Show error if browser doesn't open
592
- this.ctx.chatContainer.addChild(new Spacer(1));
593
- this.ctx.chatContainer.addChild(
594
- new Text(theme.fg("warning", " Could not open browser automatically"), 1, 0),
595
- );
596
- this.ctx.chatContainer.addChild(
597
- new Text(theme.fg("success", "Copy this exact URL in your browser:"), 1, 0),
598
- );
599
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", info.url), 1, 0));
618
+ block.addChild(new Spacer(1));
619
+ block.addChild(new Text(theme.fg("warning", "→ Could not open browser automatically"), 1, 0));
620
+ block.addChild(new Text(theme.fg("success", "Copy this exact URL in your browser:"), 1, 0));
621
+ block.addChild(new Text(theme.fg("accent", info.url), 1, 0));
600
622
  this.ctx.ui.requestRender();
601
623
  }
602
624
  },
603
625
  onProgress: (message: string) => {
604
- this.ctx.chatContainer.addChild(new Spacer(1));
605
- this.ctx.chatContainer.addChild(new Text(theme.fg("muted", message), 1, 0));
606
- this.ctx.ui.requestRender();
626
+ this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]);
607
627
  },
608
628
  },
609
629
  );
@@ -611,9 +631,10 @@ export class MCPCommandController {
611
631
  // Execute OAuth flow with 5 minute timeout
612
632
  const credentials = await withTimeout(flow.login(), 5 * 60 * 1000, "OAuth flow timed out after 5 minutes");
613
633
 
614
- this.ctx.chatContainer.addChild(new Spacer(1));
615
- this.ctx.chatContainer.addChild(new Text(theme.fg("success", "✓ Authorization completed in browser."), 1, 0));
616
- this.ctx.ui.requestRender();
634
+ this.ctx.present([
635
+ new Spacer(1),
636
+ new Text(theme.fg("success", "✓ Authorization completed in browser."), 1, 0),
637
+ ]);
617
638
 
618
639
  // Generate a unique credential ID
619
640
  const credentialId = `mcp_oauth_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
@@ -766,19 +787,8 @@ export class MCPCommandController {
766
787
  ): Promise<"connected" | "connecting" | "disconnected"> {
767
788
  if (!this.ctx.mcpManager) return "disconnected";
768
789
 
769
- this.ctx.chatContainer.addChild(new Spacer(1));
770
- const frames = theme.spinnerFrames;
771
- const initialFrame = frames[0] ?? "|";
772
- const statusText = new Text(theme.fg("muted", `${initialFrame} Connecting to "${name}"...`), 1, 0);
773
- this.ctx.chatContainer.addChild(statusText);
774
- this.ctx.ui.requestRender();
775
-
776
- let frame = 0;
777
- const interval = setInterval(() => {
778
- statusText.setText(theme.fg("muted", `${frames[frame % frames.length]} Connecting to "${name}"...`));
779
- frame++;
780
- this.ctx.ui.requestRender();
781
- }, 80);
790
+ const block = new McpConnectingBlock(name);
791
+ this.ctx.present(block);
782
792
 
783
793
  try {
784
794
  try {
@@ -792,20 +802,19 @@ export class MCPCommandController {
792
802
  await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
793
803
  }
794
804
  if (state === "connected") {
795
- statusText.setText(theme.fg("success", `✓ Connected to "${name}"`));
805
+ block.setStatus(theme.fg("success", `✓ Connected to "${name}"`));
796
806
  } else if (state === "connecting") {
797
- statusText.setText(theme.fg("muted", `◌ "${name}" is still connecting...`));
807
+ block.setStatus(theme.fg("muted", `◌ "${name}" is still connecting...`));
798
808
  } else {
799
- statusText.setText(
809
+ block.setStatus(
800
810
  options?.suppressDisconnectedWarning
801
811
  ? theme.fg("muted", `◌ Connection check complete for "${name}"`)
802
812
  : theme.fg("warning", `⚠ Could not connect to "${name}" yet`),
803
813
  );
804
814
  }
805
- this.ctx.ui.requestRender();
806
815
  return state;
807
816
  } finally {
808
- clearInterval(interval);
817
+ block.finish();
809
818
  }
810
819
  }
811
820