@oh-my-pi/pi-coding-agent 15.10.0 → 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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -1,6 +1,5 @@
1
1
  import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
2
2
  import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
3
- import { logger } from "@oh-my-pi/pi-utils";
4
3
  import { KeybindingsManager } from "../../config/keybindings";
5
4
  import type {
6
5
  CompactOptions,
@@ -176,10 +175,10 @@ export class ExtensionUiController {
176
175
  this.ctx.streamingMessage = undefined;
177
176
  this.ctx.pendingTools.clear();
178
177
 
179
- this.ctx.chatContainer.addChild(new Spacer(1));
180
- this.ctx.chatContainer.addChild(
178
+ this.ctx.present([
179
+ new Spacer(1),
181
180
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
182
- );
181
+ ]);
183
182
  await this.ctx.reloadTodos();
184
183
  this.ctx.ui.requestRender(true, { clearScrollback: true });
185
184
 
@@ -326,10 +325,6 @@ export class ExtensionUiController {
326
325
  .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
327
326
  .catch((err: unknown) => {
328
327
  const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
329
- if (this.ctx.isBackgrounded) {
330
- logger.error(errorText);
331
- return;
332
- }
333
328
  this.ctx.showError(errorText);
334
329
  });
335
330
  },
@@ -374,9 +369,6 @@ export class ExtensionUiController {
374
369
  getContextUsage: () => this.ctx.session.getContextUsage(),
375
370
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
376
371
  reload: async () => {
377
- if (this.ctx.isBackgrounded) {
378
- return;
379
- }
380
372
  await this.ctx.session.reload();
381
373
  this.ctx.chatContainer.clear();
382
374
  this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
@@ -384,9 +376,6 @@ export class ExtensionUiController {
384
376
  this.ctx.showStatus("Reloaded session");
385
377
  },
386
378
  newSession: async options => {
387
- if (this.ctx.isBackgrounded) {
388
- return { cancelled: true };
389
- }
390
379
  // Stop any loading animation
391
380
  if (this.ctx.loadingAnimation) {
392
381
  this.ctx.loadingAnimation.stop();
@@ -415,19 +404,16 @@ export class ExtensionUiController {
415
404
  this.ctx.streamingMessage = undefined;
416
405
  this.ctx.pendingTools.clear();
417
406
 
418
- this.ctx.chatContainer.addChild(new Spacer(1));
419
- this.ctx.chatContainer.addChild(
407
+ this.ctx.present([
408
+ new Spacer(1),
420
409
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
421
- );
410
+ ]);
422
411
  await this.ctx.reloadTodos();
423
412
  this.ctx.ui.requestRender(true, { clearScrollback: true });
424
413
 
425
414
  return { cancelled: false };
426
415
  },
427
416
  branch: async entryId => {
428
- if (this.ctx.isBackgrounded) {
429
- return { cancelled: true };
430
- }
431
417
  const result = await this.ctx.session.branch(entryId);
432
418
  if (result.cancelled) {
433
419
  return { cancelled: true };
@@ -443,9 +429,6 @@ export class ExtensionUiController {
443
429
  return { cancelled: false };
444
430
  },
445
431
  navigateTree: async (targetId, options) => {
446
- if (this.ctx.isBackgrounded) {
447
- return { cancelled: true };
448
- }
449
432
  const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
450
433
  if (result.cancelled) {
451
434
  return { cancelled: true };
@@ -464,9 +447,6 @@ export class ExtensionUiController {
464
447
  },
465
448
  compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
466
449
  switchSession: async sessionPath => {
467
- if (this.ctx.isBackgrounded) {
468
- return { cancelled: true };
469
- }
470
450
  this.clearHookWidgets();
471
451
  const result = await this.ctx.session.switchSession(sessionPath);
472
452
  if (!result) {
@@ -482,36 +462,6 @@ export class ExtensionUiController {
482
462
  extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
483
463
  }
484
464
 
485
- createBackgroundUiContext(): ExtensionUIContext {
486
- return {
487
- select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined,
488
- confirm: async (_title: string, _message: string, _dialogOptions) => false,
489
- input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
490
- notify: () => {},
491
- onTerminalInput: () => () => {},
492
- setStatus: () => {},
493
- setWorkingMessage: () => {},
494
- setWidget: () => {},
495
- setTitle: () => {},
496
- custom: async () => undefined as never,
497
- setEditorText: () => {},
498
- pasteToEditor: () => {},
499
- getEditorText: () => "",
500
- editor: async () => undefined,
501
- get theme() {
502
- return theme;
503
- },
504
- getAllThemes: () => Promise.resolve([]),
505
- getTheme: () => Promise.resolve(undefined),
506
- setTheme: () => Promise.resolve({ success: false, error: "Background mode" }),
507
- setFooter: () => {},
508
- setHeader: () => {},
509
- setEditorComponent: () => {},
510
- getToolsExpanded: () => false,
511
- setToolsExpanded: () => {},
512
- };
513
- }
514
-
515
465
  /**
516
466
  * Emit session event to all extension tools.
517
467
  */
@@ -531,7 +481,7 @@ export class ExtensionUiController {
531
481
  ui: uiContext,
532
482
  getContextUsage: () => this.ctx.session.getContextUsage(),
533
483
  compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
534
- hasUI: !this.ctx.isBackgrounded,
484
+ hasUI: true,
535
485
  cwd: this.ctx.sessionManager.getCwd(),
536
486
  sessionManager: this.ctx.session.sessionManager,
537
487
  modelRegistry: this.ctx.session.modelRegistry,
@@ -557,22 +507,14 @@ export class ExtensionUiController {
557
507
  * Show a tool error in the chat.
558
508
  */
559
509
  showToolError(toolName: string, error: string): void {
560
- if (this.ctx.isBackgrounded) {
561
- logger.error(`Tool "${toolName}" error: ${error}`);
562
- return;
563
- }
564
510
  const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
565
- this.ctx.chatContainer.addChild(errorText);
566
- this.ctx.ui.requestRender();
511
+ this.ctx.present(errorText);
567
512
  }
568
513
 
569
514
  /**
570
515
  * Set hook status text in the footer.
571
516
  */
572
517
  setHookStatus(key: string, text: string | undefined): void {
573
- if (this.ctx.isBackgrounded) {
574
- return;
575
- }
576
518
  this.ctx.statusLine.setHookStatus(key, text);
577
519
  this.ctx.ui.requestRender();
578
520
  }
@@ -860,14 +802,9 @@ export class ExtensionUiController {
860
802
 
861
803
  showExtensionError(extensionPath: string, error: string): void {
862
804
  const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
863
- this.ctx.chatContainer.addChild(errorText);
864
- this.ctx.ui.requestRender();
805
+ this.ctx.present(errorText);
865
806
  }
866
807
  async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
867
- if (this.ctx.isBackgrounded) {
868
- await this.#compactSession(instructionsOrOptions);
869
- return;
870
- }
871
808
  await this.ctx.executeCompaction(instructionsOrOptions, false);
872
809
  }
873
810
 
@@ -892,7 +829,7 @@ export class ExtensionUiController {
892
829
  #applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void {
893
830
  // For non-streaming cases with display=true, update UI
894
831
  // (streaming cases update via message_end event)
895
- if (!this.ctx.isBackgrounded && !wasStreaming && shouldDisplay) {
832
+ if (!wasStreaming && shouldDisplay) {
896
833
  this.ctx.rebuildChatFromMessages();
897
834
  }
898
835
  }
@@ -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");
@@ -176,6 +179,7 @@ export class InputController {
176
179
  this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
177
180
  );
178
181
  this.ctx.editor.onPasteImage = () => this.handleImagePaste();
182
+ this.ctx.editor.onPasteImagePath = path => void this.handleImagePathPaste(path);
179
183
  this.ctx.editor.setActionKeys(
180
184
  "app.clipboard.pasteTextRaw",
181
185
  this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
@@ -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;