@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13

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 (134) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
  3. package/dist/cli.js +3069 -3047
  4. package/dist/types/advisor/delta-split.d.ts +24 -0
  5. package/dist/types/advisor/runtime.d.ts +2 -2
  6. package/dist/types/async/job-manager.d.ts +8 -1
  7. package/dist/types/cli/update-cli.d.ts +53 -1
  8. package/dist/types/config/keybindings.d.ts +10 -0
  9. package/dist/types/config/model-resolver.d.ts +15 -2
  10. package/dist/types/config/settings-schema.d.ts +4 -0
  11. package/dist/types/discovery/agents-md.d.ts +10 -1
  12. package/dist/types/eval/runner-cache.d.ts +12 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +12 -3
  14. package/dist/types/extensibility/extensions/types.d.ts +15 -4
  15. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
  16. package/dist/types/lib/xai-http.d.ts +0 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +8 -5
  18. package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/types.d.ts +4 -0
  20. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
  21. package/dist/types/modes/interactive-mode.d.ts +17 -8
  22. package/dist/types/modes/types.d.ts +7 -10
  23. package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
  24. package/dist/types/session/agent-session-types.d.ts +2 -0
  25. package/dist/types/session/agent-session.d.ts +22 -3
  26. package/dist/types/session/messages.d.ts +20 -0
  27. package/dist/types/session/retry-fallback-chains.d.ts +13 -0
  28. package/dist/types/session/session-advisors.d.ts +1 -1
  29. package/dist/types/session/session-history-format.d.ts +10 -0
  30. package/dist/types/session/session-maintenance.d.ts +1 -1
  31. package/dist/types/session/session-tools.d.ts +24 -5
  32. package/dist/types/session/turn-recovery.d.ts +34 -5
  33. package/dist/types/slash-commands/types.d.ts +5 -1
  34. package/dist/types/task/executor.d.ts +1 -1
  35. package/dist/types/tools/approval.d.ts +7 -0
  36. package/dist/types/tools/todo.d.ts +14 -15
  37. package/dist/types/tools/write.d.ts +2 -2
  38. package/dist/types/utils/local-date.d.ts +2 -0
  39. package/dist/types/vibe/runtime.d.ts +1 -1
  40. package/dist/types/web/parallel.d.ts +1 -0
  41. package/dist/types/web/search/providers/brave.d.ts +8 -3
  42. package/dist/types/web/search/providers/codex.d.ts +6 -0
  43. package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
  44. package/dist/types/web/search/providers/jina.d.ts +3 -3
  45. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  46. package/dist/types/web/search/providers/perplexity.d.ts +4 -0
  47. package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
  48. package/package.json +13 -13
  49. package/src/advisor/delta-split.ts +98 -0
  50. package/src/advisor/runtime.ts +321 -69
  51. package/src/async/job-manager.ts +14 -3
  52. package/src/cli/plugin-cli.ts +30 -2
  53. package/src/cli/update-cli.ts +259 -24
  54. package/src/config/keybindings.ts +52 -9
  55. package/src/config/model-resolver.ts +19 -3
  56. package/src/config/settings-schema.ts +5 -0
  57. package/src/cursor.ts +10 -5
  58. package/src/discovery/agents-md.ts +61 -23
  59. package/src/eval/jl/kernel.ts +2 -20
  60. package/src/eval/py/kernel.ts +2 -20
  61. package/src/eval/rb/kernel.ts +2 -20
  62. package/src/eval/runner-cache.ts +41 -0
  63. package/src/exec/non-interactive-env.ts +14 -3
  64. package/src/extensibility/extensions/loader.ts +5 -2
  65. package/src/extensibility/extensions/runner.ts +184 -66
  66. package/src/extensibility/extensions/types.ts +26 -2
  67. package/src/extensibility/extensions/wrapper.ts +13 -7
  68. package/src/extensibility/plugins/marketplace/manager.ts +6 -2
  69. package/src/hindsight/client.ts +1 -1
  70. package/src/lib/xai-http.ts +0 -4
  71. package/src/lsp/client.ts +2 -0
  72. package/src/lsp/servers.ts +1 -1
  73. package/src/mcp/tool-bridge.ts +15 -6
  74. package/src/modes/components/agent-hub-renderer.ts +9 -3
  75. package/src/modes/components/agent-hub.ts +2 -1
  76. package/src/modes/components/status-line/component.ts +58 -6
  77. package/src/modes/components/status-line/segments.ts +12 -1
  78. package/src/modes/components/status-line/types.ts +1 -0
  79. package/src/modes/components/user-message.ts +20 -5
  80. package/src/modes/controllers/event-controller.ts +48 -0
  81. package/src/modes/controllers/extension-ui-controller.ts +14 -7
  82. package/src/modes/controllers/input-controller.ts +25 -6
  83. package/src/modes/interactive-mode.ts +315 -129
  84. package/src/modes/rpc/rpc-frame.ts +13 -5
  85. package/src/modes/theme/tui-adapters.ts +4 -5
  86. package/src/modes/types.ts +13 -7
  87. package/src/modes/utils/hotkeys-markdown.ts +10 -6
  88. package/src/prompts/system/system-prompt.md +1 -1
  89. package/src/registry/persisted-agents.ts +43 -8
  90. package/src/sdk.ts +139 -8
  91. package/src/session/agent-session-types.ts +2 -0
  92. package/src/session/agent-session.ts +66 -10
  93. package/src/session/messages.ts +98 -28
  94. package/src/session/retry-fallback-chains.ts +14 -0
  95. package/src/session/session-advisors.ts +32 -15
  96. package/src/session/session-history-format.ts +15 -1
  97. package/src/session/session-maintenance.ts +8 -8
  98. package/src/session/session-manager.ts +6 -2
  99. package/src/session/session-tools.ts +321 -184
  100. package/src/session/turn-recovery.ts +225 -47
  101. package/src/slash-commands/builtin-modes.ts +41 -12
  102. package/src/slash-commands/types.ts +5 -1
  103. package/src/task/executor.ts +92 -45
  104. package/src/task/structured-subagent.ts +5 -5
  105. package/src/tools/approval.ts +44 -10
  106. package/src/tools/fetch.ts +21 -2
  107. package/src/tools/image-gen.ts +6 -8
  108. package/src/tools/todo.ts +70 -26
  109. package/src/tools/tts.ts +3 -2
  110. package/src/tools/write.ts +7 -3
  111. package/src/utils/local-date.ts +13 -0
  112. package/src/utils/tools-manager.ts +2 -2
  113. package/src/vibe/runtime.ts +22 -14
  114. package/src/web/kagi.ts +91 -34
  115. package/src/web/parallel.ts +11 -2
  116. package/src/web/scrapers/crates-io.ts +2 -2
  117. package/src/web/scrapers/discogs.ts +2 -2
  118. package/src/web/scrapers/docs-rs.ts +2 -2
  119. package/src/web/scrapers/github.ts +2 -2
  120. package/src/web/scrapers/musicbrainz.ts +1 -2
  121. package/src/web/scrapers/pubmed.ts +2 -2
  122. package/src/web/scrapers/sec-edgar.ts +2 -2
  123. package/src/web/search/providers/brave.ts +121 -46
  124. package/src/web/search/providers/codex.ts +88 -12
  125. package/src/web/search/providers/exa.ts +45 -10
  126. package/src/web/search/providers/firecrawl.ts +53 -11
  127. package/src/web/search/providers/gemini.ts +139 -27
  128. package/src/web/search/providers/jina.ts +48 -25
  129. package/src/web/search/providers/parallel.ts +23 -9
  130. package/src/web/search/providers/perplexity.ts +24 -7
  131. package/src/web/search/providers/searxng.ts +77 -1
  132. package/src/web/search/providers/tavily.ts +23 -22
  133. package/src/web/search/providers/tinyfish.ts +44 -10
  134. package/src/web/search/providers/xai.ts +85 -14
@@ -70,6 +70,7 @@ import { clearClaudePluginRootsCache } from "../discovery/helpers";
70
70
  import type {
71
71
  AutocompleteProviderFactory,
72
72
  ContextUsage,
73
+ ExtensionCustomOptions,
73
74
  ExtensionUIContext,
74
75
  ExtensionUIDialogOptions,
75
76
  ExtensionUISelectItem,
@@ -123,6 +124,7 @@ import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-
123
124
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
124
125
  import {
125
126
  formatPhaseDisplayName,
127
+ isClosedTodo,
126
128
  selectCollapsedTodos,
127
129
  setActiveTodoDescriptionsProvider,
128
130
  todoMatchesAnyDescription,
@@ -344,6 +346,17 @@ function formatContextTokenCount(value: number): string {
344
346
  return formatNumber(Math.max(0, Math.round(value))).toLowerCase();
345
347
  }
346
348
 
349
+ /**
350
+ * Reads a tool-name snapshot out of a persisted `mode_change` payload. Session
351
+ * files are user-editable and survive across versions, so anything that is not
352
+ * a plain array of strings is treated as absent rather than trusted.
353
+ */
354
+ function readPersistedToolNames(value: unknown): string[] | undefined {
355
+ if (!Array.isArray(value)) return undefined;
356
+ if (!value.every(name => typeof name === "string")) return undefined;
357
+ return value as string[];
358
+ }
359
+
347
360
  /** Options for creating an InteractiveMode instance (for future API use) */
348
361
  export interface InteractiveModeOptions {
349
362
  /** Providers that were migrated during startup */
@@ -373,6 +386,42 @@ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveReg
373
386
  }
374
387
  }
375
388
 
389
+ /**
390
+ * Preview of the command panels queued while the agent streams, rendered above
391
+ * the editor so `/usage` and friends answer immediately mid-turn.
392
+ *
393
+ * Capped in height: the panels are shown in full in the transcript at the next
394
+ * settle, so the preview only has to answer the question, not reproduce the
395
+ * whole report. Rendering is delegated to the real panels at the real width, so
396
+ * the preview cannot drift from what eventually lands in the transcript.
397
+ */
398
+ class DeferredCommandPreview implements Component {
399
+ constructor(
400
+ private readonly items: readonly Component[],
401
+ private readonly maxRows: number,
402
+ private readonly commandCount: number,
403
+ ) {}
404
+
405
+ render(width: number): readonly string[] {
406
+ const rows: string[] = [];
407
+ for (const item of this.items) rows.push(...item.render(width));
408
+ const queued = this.commandCount === 1 ? "1 command output" : `${this.commandCount} command outputs`;
409
+ if (rows.length <= this.maxRows) {
410
+ rows.push(theme.fg("dim", `${queued} — repeated in the transcript when the agent pauses`));
411
+ return rows;
412
+ }
413
+ const shown = rows.slice(0, Math.max(1, this.maxRows - 1));
414
+ const hidden = rows.length - shown.length;
415
+ shown.push(theme.fg("dim", `… ${hidden} more rows — ${queued} shown in full when the agent pauses`));
416
+ return shown;
417
+ }
418
+ }
419
+
420
+ /** Never shrink the queued-output preview below this, even on a short terminal. */
421
+ const DEFERRED_PREVIEW_MIN_ROWS = 6;
422
+ /** Ceiling for the preview as a share of the viewport, so the prompt stays visible. */
423
+ const DEFERRED_PREVIEW_VIEWPORT_FRACTION = 0.4;
424
+
376
425
  /** How long the ctrl+p model-role cycle chip track lingers above the editor
377
426
  * before it auto-clears, mirroring the todo HUD's auto-clear timer. */
378
427
  const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
@@ -450,6 +499,7 @@ export class InteractiveMode implements InteractiveModeContext {
450
499
  omfgContainer: Container;
451
500
  errorBannerContainer: Container;
452
501
  modelCycleContainer: Container;
502
+ deferredCommandContainer: Container;
453
503
  editor: CustomEditor;
454
504
  editorContainer: Container;
455
505
  hookWidgetContainerAbove: Container;
@@ -531,6 +581,7 @@ export class InteractiveMode implements InteractiveModeContext {
531
581
  locallySubmittedUserSignatures: Set<string> = new Set();
532
582
  #pendingSubmittedInput: SubmittedUserInput | undefined;
533
583
  #pendingSubmissionDispose: (() => void) | undefined;
584
+ #pendingSubmissionPreservesDraft = false;
534
585
  #optimisticUserMessageComponents: Component[] = [];
535
586
  lastSigintTime = 0;
536
587
  lastEscapeTime = 0;
@@ -556,6 +607,8 @@ export class InteractiveMode implements InteractiveModeContext {
556
607
 
557
608
  #pendingCommandOutput: Component[] = [];
558
609
  #pendingCommandOutputSessionId: string | undefined;
610
+ /** Commands (not components) queued while streaming, for the deferral hint. */
611
+ #pendingCommandOutputCommands = 0;
559
612
  #pendingSlashCommands: SlashCommand[] = [];
560
613
  /** Built-in editor autocomplete provider, before extension wrapping. */
561
614
  #baseAutocompleteProvider: AutocompleteProvider | undefined;
@@ -643,6 +696,10 @@ export class InteractiveMode implements InteractiveModeContext {
643
696
  this.pendingMessagesContainer.disposeChildren();
644
697
  this.#cancelModelCycleClearTimer();
645
698
  this.modelCycleContainer.disposeChildren();
699
+ this.deferredCommandContainer.disposeChildren();
700
+ this.#pendingCommandOutput = [];
701
+ this.#pendingCommandOutputSessionId = undefined;
702
+ this.#pendingCommandOutputCommands = 0;
646
703
  this.compactionQueuedMessages = [];
647
704
  this.streamingComponent = undefined;
648
705
  this.streamingMessage = undefined;
@@ -729,6 +786,7 @@ export class InteractiveMode implements InteractiveModeContext {
729
786
  this.omfgContainer = new AnchoredLiveContainer();
730
787
  this.errorBannerContainer = new AnchoredLiveContainer();
731
788
  this.modelCycleContainer = new AnchoredLiveContainer();
789
+ this.deferredCommandContainer = new AnchoredLiveContainer();
732
790
  this.editor = new CustomEditor(getEditorTheme());
733
791
  this.ui.enableScopedInputRender(this.editor);
734
792
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
@@ -983,6 +1041,7 @@ export class InteractiveMode implements InteractiveModeContext {
983
1041
  this.ui.addChild(this.omfgContainer);
984
1042
  this.ui.addChild(this.errorBannerContainer);
985
1043
  this.ui.addChild(this.modelCycleContainer);
1044
+ this.ui.addChild(this.deferredCommandContainer);
986
1045
  // Working loader / transient status sits below the sticky todo + subagent
987
1046
  // HUDs, just above the editor's hook-widget top margin — so it reads next to
988
1047
  // the prompt while keeping the one-line gap above the editor.
@@ -1584,14 +1643,17 @@ export class InteractiveMode implements InteractiveModeContext {
1584
1643
  this.addMessageToChat(message, options);
1585
1644
  }
1586
1645
 
1587
- startPendingSubmission(input: {
1588
- text: string;
1589
- images?: ImageContent[];
1590
- imageLinks?: (string | undefined)[];
1591
- customType?: string;
1592
- display?: boolean;
1593
- streamingBehavior?: "steer" | "followUp";
1594
- }): SubmittedUserInput {
1646
+ startPendingSubmission(
1647
+ input: {
1648
+ text: string;
1649
+ images?: ImageContent[];
1650
+ imageLinks?: (string | undefined)[];
1651
+ customType?: string;
1652
+ display?: boolean;
1653
+ streamingBehavior?: "steer" | "followUp";
1654
+ },
1655
+ options?: { preserveDraft?: boolean },
1656
+ ): SubmittedUserInput {
1595
1657
  const submission: SubmittedUserInput = {
1596
1658
  text: input.text,
1597
1659
  images: input.images,
@@ -1603,6 +1665,7 @@ export class InteractiveMode implements InteractiveModeContext {
1603
1665
  started: false,
1604
1666
  };
1605
1667
  this.#pendingSubmittedInput = submission;
1668
+ this.#pendingSubmissionPreservesDraft = options?.preserveDraft === true;
1606
1669
  if (!submission.customType) {
1607
1670
  this.#resetGoalContinuationSuppression();
1608
1671
  const imageCount = submission.images?.length ?? 0;
@@ -1622,8 +1685,10 @@ export class InteractiveMode implements InteractiveModeContext {
1622
1685
  } else {
1623
1686
  this.clearOptimisticUserMessage();
1624
1687
  }
1625
- this.editor.setText("");
1626
- this.editor.imageLinks = undefined;
1688
+ if (!options?.preserveDraft) {
1689
+ this.editor.setText("");
1690
+ this.editor.imageLinks = undefined;
1691
+ }
1627
1692
  this.ensureLoadingAnimation();
1628
1693
  this.ui.requestRender();
1629
1694
  return submission;
@@ -1634,9 +1699,11 @@ export class InteractiveMode implements InteractiveModeContext {
1634
1699
  if (!submission || submission.started) {
1635
1700
  return false;
1636
1701
  }
1702
+ const preserveDraft = this.#pendingSubmissionPreservesDraft;
1637
1703
 
1638
1704
  submission.cancelled = true;
1639
1705
  this.#pendingSubmittedInput = undefined;
1706
+ this.#pendingSubmissionPreservesDraft = false;
1640
1707
  this.clearOptimisticUserMessage();
1641
1708
  this.#pendingWorkingMessage = undefined;
1642
1709
  if (submission.customType === "goal-continuation") {
@@ -1645,7 +1712,7 @@ export class InteractiveMode implements InteractiveModeContext {
1645
1712
  if (this.loadingAnimation) {
1646
1713
  this.#stopLoadingAnimation(true);
1647
1714
  }
1648
- if (!submission.customType) {
1715
+ if (!submission.customType && !preserveDraft) {
1649
1716
  this.editor.pendingImages = submission.images ? [...submission.images] : [];
1650
1717
  this.editor.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
1651
1718
  this.editor.imageLinks = this.editor.pendingImageLinks;
@@ -1662,6 +1729,7 @@ export class InteractiveMode implements InteractiveModeContext {
1662
1729
  return false;
1663
1730
  }
1664
1731
  input.started = true;
1732
+ this.#pendingSubmissionPreservesDraft = false;
1665
1733
  const annotationStateKey = this.#planReviewAnnotationStateBySubmission.get(input);
1666
1734
  if (annotationStateKey) {
1667
1735
  this.#planReviewAnnotationStateBySubmission.delete(input);
@@ -1676,6 +1744,7 @@ export class InteractiveMode implements InteractiveModeContext {
1676
1744
  if (wasPendingSubmission) {
1677
1745
  this.#pendingSubmittedInput = undefined;
1678
1746
  this.#pendingSubmissionDispose = undefined;
1747
+ this.#pendingSubmissionPreservesDraft = false;
1679
1748
  }
1680
1749
  if (input.customType === "goal-continuation") {
1681
1750
  this.#goalContinuationTurnInFlight = false;
@@ -1983,35 +2052,40 @@ export class InteractiveMode implements InteractiveModeContext {
1983
2052
  this.#todoAutoClearTimer = undefined;
1984
2053
  }
1985
2054
 
1986
- #isClosedTodo(task: TodoItem): boolean {
1987
- return task.status === "completed" || task.status === "abandoned";
1988
- }
1989
-
1990
- #hasClosedTodos(phases: TodoPhase[]): boolean {
1991
- return phases.some(phase => phase.tasks.some(task => this.#isClosedTodo(task)));
1992
- }
1993
-
1994
- #removeClosedTodos(phases: TodoPhase[]): TodoPhase[] {
1995
- const next: TodoPhase[] = [];
2055
+ /**
2056
+ * Whether every todo is closed, so the HUD has nothing left to track.
2057
+ *
2058
+ * The auto-clear only fires on a settled list. Scrubbing closed tasks while
2059
+ * open work remains is destructive: the walking viewport already hides all but
2060
+ * the newest closed row, and those tasks are what the phase progress counters
2061
+ * and the stage roman numerals are computed from — dropping them mid-run reset
2062
+ * an in-flight phase to `0/n` and renumbered the stages, so a plan the agent
2063
+ * was four tasks into rendered as untouched until the next `todo` call
2064
+ * restored the real snapshot.
2065
+ */
2066
+ #isTodoListSettled(phases: TodoPhase[]): boolean {
2067
+ let seenTask = false;
1996
2068
  for (const phase of phases) {
1997
- const tasks = phase.tasks.filter(task => !this.#isClosedTodo(task));
1998
- if (tasks.length > 0) next.push({ name: phase.name, tasks });
2069
+ for (const task of phase.tasks) {
2070
+ if (!isClosedTodo(task)) return false;
2071
+ seenTask = true;
2072
+ }
1999
2073
  }
2000
- return next;
2074
+ return seenTask;
2001
2075
  }
2002
2076
 
2003
2077
  #syncTodoAutoClearTimer(): void {
2004
2078
  this.#cancelTodoAutoClearTimer();
2005
2079
  const delaySeconds = this.settings.get("tasks.todoClearDelay");
2006
- if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || !this.#hasClosedTodos(this.todoPhases)) return;
2080
+ if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || !this.#isTodoListSettled(this.todoPhases)) return;
2007
2081
  if (delaySeconds === 0) {
2008
- this.todoPhases = this.#removeClosedTodos(this.todoPhases);
2082
+ this.todoPhases = [];
2009
2083
  return;
2010
2084
  }
2011
2085
 
2012
2086
  this.#todoAutoClearTimer = setTimeout(() => {
2013
2087
  this.#todoAutoClearTimer = undefined;
2014
- this.todoPhases = this.#removeClosedTodos(this.todoPhases);
2088
+ this.todoPhases = [];
2015
2089
  this.#renderTodoList();
2016
2090
  this.ui.requestRender();
2017
2091
  }, delaySeconds * 1000);
@@ -2142,7 +2216,9 @@ export class InteractiveMode implements InteractiveModeContext {
2142
2216
  // brighter muted gray. The root header carries overall stage progression.
2143
2217
  const renderPhase = (phase: TodoPhase, oneBased: number, isActive: boolean): string | string[] => {
2144
2218
  const label = multiPhase ? formatPhaseDisplayName(phase.name, oneBased) : phase.name;
2145
- const done = phase.tasks.filter(t => t.status === "completed").length;
2219
+ // Closed, not just completed: the collapsed task window hides abandoned
2220
+ // tasks too, so counting only completions leaves the phase reading stuck.
2221
+ const done = phase.tasks.filter(isClosedTodo).length;
2146
2222
  const progress = ` · ${done}/${phase.tasks.length}`;
2147
2223
  if (!isActive) {
2148
2224
  const header = theme.fg("muted", label) + theme.fg("dim", progress);
@@ -2502,6 +2578,14 @@ export class InteractiveMode implements InteractiveModeContext {
2502
2578
  this.#vibeModeOwnerScope?.ownerId === targetVibeScope.ownerId &&
2503
2579
  this.#vibeModeOwnerScope.parentSessionId === targetVibeScope.parentSessionId &&
2504
2580
  this.#vibeModeOwnerScope.parentSessionFile === targetVibeScope.parentSessionFile;
2581
+ // #clearTransientModeState below keeps the live active set instead of
2582
+ // applying a snapshot, so for a vibe -> vibe switch the live toolset is
2583
+ // already the reduced vibe set and cannot serve as the pre-vibe snapshot.
2584
+ // That is the only case the persisted snapshot is for: a cold resume or a
2585
+ // switch in from a non-vibe session built its toolset from the current CLI
2586
+ // flags and settings, and that set — not a historical one — is what exiting
2587
+ // vibe must restore.
2588
+ const vibeToolsetLostToTeardown = this.vibeModeEnabled && !preserveVibe;
2505
2589
  await this.#clearTransientModeState({ preserveVibe, vibeScopeAlreadySuspended });
2506
2590
  await VibeSessionRegistry.global().rehydrate(vibeSession);
2507
2591
  const goalEnabled = this.session.settings.get("goal.enabled");
@@ -2538,7 +2622,14 @@ export class InteractiveMode implements InteractiveModeContext {
2538
2622
  }
2539
2623
  this.session.goalRuntime.clearAccounting();
2540
2624
  if (sessionContext.mode === "vibe") {
2541
- if (!preserveVibe) await this.#enterVibeMode({ persistModeChange: false });
2625
+ if (!preserveVibe) {
2626
+ await this.#enterVibeMode({
2627
+ persistModeChange: false,
2628
+ previousTools: vibeToolsetLostToTeardown
2629
+ ? readPersistedToolNames(sessionContext.modeData?.previousTools)
2630
+ : undefined,
2631
+ });
2632
+ }
2542
2633
  return;
2543
2634
  }
2544
2635
  if (!this.session.settings.get("plan.enabled")) {
@@ -3281,14 +3372,17 @@ export class InteractiveMode implements InteractiveModeContext {
3281
3372
  }
3282
3373
  }
3283
3374
 
3284
- async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
3375
+ async handlePlanModeCommand(
3376
+ initialPrompt?: string,
3377
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3378
+ ): Promise<boolean> {
3285
3379
  if (this.goalModeEnabled || this.goalModePaused) {
3286
3380
  this.showWarning("Exit goal mode first.");
3287
- return;
3381
+ return false;
3288
3382
  }
3289
3383
  if (this.vibeModeEnabled) {
3290
3384
  this.showWarning("Exit vibe mode first.");
3291
- return;
3385
+ return false;
3292
3386
  }
3293
3387
  if (this.planModeEnabled) {
3294
3388
  const planFilePath = this.planModePlanFilePath ?? (await this.#getPlanFilePath());
@@ -3297,10 +3391,10 @@ export class InteractiveMode implements InteractiveModeContext {
3297
3391
  "Exit plan mode?",
3298
3392
  "This exits plan mode without approving a plan.",
3299
3393
  );
3300
- if (!confirmed) return;
3394
+ if (!confirmed) return false;
3301
3395
  }
3302
3396
  await this.#exitPlanMode({ paused: true });
3303
- return;
3397
+ return false;
3304
3398
  }
3305
3399
  if (this.planModePaused && !initialPrompt) {
3306
3400
  // No-arg third toggle: paused → off. Tools, model, and plan state were
@@ -3313,16 +3407,28 @@ export class InteractiveMode implements InteractiveModeContext {
3313
3407
  this.#updatePlanModeStatus();
3314
3408
  this.sessionManager.appendModeChange("none");
3315
3409
  this.showStatus("Plan mode disabled.");
3316
- return;
3410
+ return false;
3317
3411
  }
3318
3412
  if (!this.session.settings.get("plan.enabled")) {
3319
3413
  this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
3320
- return;
3414
+ return false;
3321
3415
  }
3322
3416
  await this.#enterPlanMode();
3323
- if (initialPrompt && this.onInputCallback) {
3324
- this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
3417
+ if (!initialPrompt) return false;
3418
+ if (this.session.isStreaming) {
3419
+ const images = input?.images?.length ? input.images : undefined;
3420
+ await this.withLocalSubmission(
3421
+ initialPrompt,
3422
+ () => this.session.prompt(initialPrompt, { streamingBehavior: "steer", images }),
3423
+ { imageCount: images?.length ?? 0 },
3424
+ );
3425
+ return true;
3325
3426
  }
3427
+ if (this.onInputCallback) {
3428
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt, ...input }, { preserveDraft: true }));
3429
+ return true;
3430
+ }
3431
+ return false;
3326
3432
  }
3327
3433
 
3328
3434
  /**
@@ -3332,26 +3438,41 @@ export class InteractiveMode implements InteractiveModeContext {
3332
3438
  * the previous toolset, and kills every worker session so workers cannot
3333
3439
  * outlive the mode that directs them.
3334
3440
  */
3335
- async handleVibeModeCommand(initialPrompt?: string): Promise<void> {
3441
+ async handleVibeModeCommand(
3442
+ initialPrompt?: string,
3443
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3444
+ ): Promise<boolean> {
3336
3445
  if (this.vibeModeEnabled) {
3337
3446
  await this.#exitVibeMode();
3338
- return;
3447
+ return false;
3339
3448
  }
3340
3449
  if (this.planModeEnabled || this.planModePaused) {
3341
3450
  this.showWarning("Exit plan mode first.");
3342
- return;
3451
+ return false;
3343
3452
  }
3344
3453
  if (this.goalModeEnabled || this.goalModePaused) {
3345
3454
  this.showWarning("Exit goal mode first.");
3346
- return;
3455
+ return false;
3347
3456
  }
3348
3457
  await this.#enterVibeMode();
3349
- if (initialPrompt && this.onInputCallback) {
3350
- this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
3458
+ if (!initialPrompt) return false;
3459
+ if (this.session.isStreaming) {
3460
+ const images = input?.images?.length ? input.images : undefined;
3461
+ await this.withLocalSubmission(
3462
+ initialPrompt,
3463
+ () => this.session.prompt(initialPrompt, { streamingBehavior: "steer", images }),
3464
+ { imageCount: images?.length ?? 0 },
3465
+ );
3466
+ return true;
3351
3467
  }
3468
+ if (this.onInputCallback) {
3469
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt, ...input }, { preserveDraft: true }));
3470
+ return true;
3471
+ }
3472
+ return false;
3352
3473
  }
3353
3474
 
3354
- async #enterVibeMode(options?: { persistModeChange?: boolean }): Promise<void> {
3475
+ async #enterVibeMode(options?: { persistModeChange?: boolean; previousTools?: string[] }): Promise<void> {
3355
3476
  if (this.vibeModeEnabled) {
3356
3477
  return;
3357
3478
  }
@@ -3367,7 +3488,12 @@ export class InteractiveMode implements InteractiveModeContext {
3367
3488
  const vibeRegistry = VibeSessionRegistry.global();
3368
3489
  const ownerScope = vibeRegistry.ownerScope(this.#vibeParentSession());
3369
3490
  vibeRegistry.activateScope(ownerScope);
3370
- const previousTools = this.session.getEnabledToolNames();
3491
+ // When a vibe session switches into another session that is also in vibe
3492
+ // mode, the teardown keeps the live active set, which is by then the reduced
3493
+ // vibe set, so re-snapshotting it here would make the snapshot useless. That
3494
+ // path passes the pre-vibe toolset recorded on the target's own mode_change
3495
+ // entry instead.
3496
+ const previousTools = options?.previousTools ?? this.session.getEnabledToolNames();
3371
3497
  const vibeBaseTools = ["read"];
3372
3498
  if (this.session.hasBuiltInTool("todo")) vibeBaseTools.push("todo");
3373
3499
  await this.session.activateVibeTools(vibeBaseTools);
@@ -3382,7 +3508,7 @@ export class InteractiveMode implements InteractiveModeContext {
3382
3508
  await this.session.sendVibeModeContext({ deliverAs: "steer" });
3383
3509
  }
3384
3510
  this.#updateVibeModeStatus();
3385
- if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe");
3511
+ if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe", { previousTools });
3386
3512
  this.showStatus(
3387
3513
  "Vibe mode enabled. You direct fast/good worker sessions; toolset is read + optional parent Todo + vibe tools.",
3388
3514
  );
@@ -3434,76 +3560,72 @@ export class InteractiveMode implements InteractiveModeContext {
3434
3560
  this.showStatus(nextBudget === undefined ? "Goal budget cleared." : `Goal budget set to ${nextBudget}.`);
3435
3561
  }
3436
3562
 
3437
- async handleGoalModeCommand(rest?: string): Promise<void> {
3438
- try {
3439
- if (this.planModeEnabled || this.planModePaused) {
3440
- this.showWarning("Exit plan mode first.");
3441
- return;
3442
- }
3443
- if (this.vibeModeEnabled) {
3444
- this.showWarning("Exit vibe mode first.");
3445
- return;
3446
- }
3447
- if (!this.session.settings.get("goal.enabled")) {
3448
- this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
3449
- return;
3450
- }
3451
- const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
3452
- if (sub) {
3453
- await this.#dispatchGoalSubcommand(sub, subRest);
3454
- return;
3455
- }
3456
- if (this.goalModeEnabled) {
3457
- if (subRest) {
3458
- this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
3459
- return;
3460
- }
3461
- await this.#openGoalMenu("active");
3462
- return;
3463
- }
3464
- const pausedState = this.#getPausedGoalState();
3465
- if (pausedState) {
3466
- if (subRest) {
3467
- this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3468
- return;
3469
- }
3470
- await this.#openGoalMenu("paused");
3471
- return;
3563
+ async handleGoalModeCommand(
3564
+ rest?: string,
3565
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3566
+ ): Promise<boolean> {
3567
+ if (this.planModeEnabled || this.planModePaused) {
3568
+ this.showWarning("Exit plan mode first.");
3569
+ return false;
3570
+ }
3571
+ if (this.vibeModeEnabled) {
3572
+ this.showWarning("Exit vibe mode first.");
3573
+ return false;
3574
+ }
3575
+ if (!this.session.settings.get("goal.enabled")) {
3576
+ this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
3577
+ return false;
3578
+ }
3579
+ const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
3580
+ if (sub) return await this.#dispatchGoalSubcommand(sub, subRest, input);
3581
+ if (this.goalModeEnabled) {
3582
+ if (subRest) {
3583
+ this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
3584
+ return false;
3472
3585
  }
3586
+ await this.#openGoalMenu("active");
3587
+ return false;
3588
+ }
3589
+ const pausedState = this.#getPausedGoalState();
3590
+ if (pausedState) {
3473
3591
  if (subRest) {
3474
- await this.#startGoalFromObjective(subRest);
3475
- return;
3592
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3593
+ return false;
3476
3594
  }
3477
- const objective = (
3478
- await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
3479
- )?.trim();
3480
- if (!objective) return;
3481
- await this.#startGoalFromObjective(objective);
3482
- } catch (error) {
3483
- this.showError(error instanceof Error ? error.message : String(error));
3595
+ await this.#openGoalMenu("paused");
3596
+ return false;
3484
3597
  }
3598
+ if (subRest) return await this.#startGoalFromObjective(subRest, input);
3599
+ const objective = (
3600
+ await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
3601
+ )?.trim();
3602
+ if (!objective) return false;
3603
+ return await this.#startGoalFromObjective(objective, input);
3485
3604
  }
3486
- async handleGuidedGoalCommand(rest?: string): Promise<void> {
3605
+ async handleGuidedGoalCommand(
3606
+ rest?: string,
3607
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3608
+ ): Promise<boolean> {
3487
3609
  try {
3488
3610
  if (this.planModeEnabled || this.planModePaused) {
3489
3611
  this.showWarning("Exit plan mode first.");
3490
- return;
3612
+ return false;
3491
3613
  }
3492
3614
  if (this.vibeModeEnabled) {
3493
3615
  this.showWarning("Exit vibe mode first.");
3494
- return;
3616
+ return false;
3495
3617
  }
3496
3618
  if (!this.session.settings.get("goal.enabled")) {
3497
3619
  this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
3498
- return;
3620
+ return false;
3499
3621
  }
3500
3622
  if (this.goalModeEnabled) {
3501
3623
  this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
3502
- return;
3624
+ return false;
3503
3625
  }
3504
3626
  if (this.#getPausedGoalState()) {
3505
3627
  this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3506
- return;
3628
+ return false;
3507
3629
  }
3508
3630
 
3509
3631
  // Expose the goal tool for the interview so the agent can finish by
@@ -3521,51 +3643,57 @@ export class InteractiveMode implements InteractiveModeContext {
3521
3643
  // assistant turns, and the user answers in the ordinary editor. Queue
3522
3644
  // behind an in-flight run instead of aborting it.
3523
3645
  const kickoff = prompt.render(guidedGoalInterviewPrompt, { initial: rest?.trim() || undefined });
3646
+ const images = input?.images?.length ? input.images : undefined;
3524
3647
  if (this.session.isStreaming) {
3525
- await this.session.followUp(kickoff, undefined, { synthetic: true });
3648
+ await this.session.followUp(kickoff, images, { synthetic: true });
3526
3649
  } else {
3527
3650
  try {
3528
- await this.session.prompt(kickoff, { synthetic: true });
3651
+ await this.session.prompt(kickoff, images ? { synthetic: true, images } : { synthetic: true });
3529
3652
  } catch (error) {
3530
3653
  if (!(error instanceof AgentBusyError)) throw error;
3531
- await this.session.followUp(kickoff, undefined, { synthetic: true });
3654
+ await this.session.followUp(kickoff, images, { synthetic: true });
3532
3655
  }
3533
3656
  }
3657
+ return true;
3534
3658
  } catch (error) {
3535
3659
  this.showError(error instanceof Error ? error.message : String(error));
3660
+ return false;
3536
3661
  }
3537
3662
  }
3538
3663
 
3539
- async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
3664
+ async #dispatchGoalSubcommand(
3665
+ sub: GoalSubcommand,
3666
+ rest: string,
3667
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3668
+ ): Promise<boolean> {
3540
3669
  switch (sub) {
3541
3670
  case "set":
3542
- await this.#handleGoalSetSubcommand(rest);
3543
- return;
3671
+ return await this.#handleGoalSetSubcommand(rest, input);
3544
3672
  case "show":
3545
3673
  this.#showGoalDetails();
3546
- return;
3674
+ return false;
3547
3675
  case "pause":
3548
3676
  await this.#pauseGoalAction();
3549
- return;
3677
+ return false;
3550
3678
  case "resume":
3551
3679
  await this.#resumeGoalAction();
3552
- return;
3680
+ return false;
3553
3681
  case "drop":
3554
3682
  await this.#confirmAndDropGoal();
3555
- return;
3683
+ return false;
3556
3684
  case "budget":
3557
3685
  if (!this.goalModeEnabled) {
3558
3686
  this.showWarning(
3559
3687
  this.#getPausedGoalState() ? "Resume the goal before adjusting the budget." : "No active goal.",
3560
3688
  );
3561
- return;
3689
+ return false;
3562
3690
  }
3563
3691
  if (!rest) {
3564
3692
  await this.#promptGoalBudgetEdit();
3565
- return;
3693
+ return false;
3566
3694
  }
3567
3695
  await this.#handleGoalBudgetCommand(rest);
3568
- return;
3696
+ return false;
3569
3697
  }
3570
3698
  }
3571
3699
 
@@ -3665,15 +3793,32 @@ export class InteractiveMode implements InteractiveModeContext {
3665
3793
  await this.#exitGoalMode({ reason: "dropped" });
3666
3794
  }
3667
3795
 
3668
- async #startGoalFromObjective(objective: string): Promise<void> {
3796
+ async #startGoalFromObjective(
3797
+ objective: string,
3798
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3799
+ ): Promise<boolean> {
3669
3800
  await this.#enterGoalMode({ objective, silent: true });
3670
3801
  this.#resetGoalContinuationSuppression();
3671
- if (!this.session.isStreaming && this.onInputCallback) {
3672
- this.onInputCallback(this.startPendingSubmission({ text: objective }));
3802
+ if (this.session.isStreaming) {
3803
+ const images = input?.images?.length ? input.images : undefined;
3804
+ await this.withLocalSubmission(
3805
+ objective,
3806
+ () => this.session.prompt(objective, { streamingBehavior: "steer", images }),
3807
+ { imageCount: images?.length ?? 0 },
3808
+ );
3809
+ return true;
3673
3810
  }
3811
+ if (this.onInputCallback) {
3812
+ this.onInputCallback(this.startPendingSubmission({ text: objective, ...input }, { preserveDraft: true }));
3813
+ return true;
3814
+ }
3815
+ return false;
3674
3816
  }
3675
3817
 
3676
- async #replaceGoalFromObjective(objective: string): Promise<void> {
3818
+ async #replaceGoalFromObjective(
3819
+ objective: string,
3820
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3821
+ ): Promise<boolean> {
3677
3822
  const state = await this.session.goalRuntime.replaceGoal({ objective });
3678
3823
  this.session.setGoalModeState(state);
3679
3824
  this.goalModeEnabled = true;
@@ -3682,26 +3827,35 @@ export class InteractiveMode implements InteractiveModeContext {
3682
3827
  this.#updateGoalModeStatus();
3683
3828
  if (this.session.isStreaming) {
3684
3829
  await this.session.sendGoalModeContext({ deliverAs: "steer" });
3830
+ const images = input?.images?.length ? input.images : undefined;
3831
+ await this.withLocalSubmission(
3832
+ objective,
3833
+ () => this.session.prompt(objective, { streamingBehavior: "steer", images }),
3834
+ { imageCount: images?.length ?? 0 },
3835
+ );
3836
+ return true;
3685
3837
  }
3686
- if (!this.session.isStreaming && this.onInputCallback) {
3687
- this.onInputCallback(this.startPendingSubmission({ text: objective }));
3838
+ if (this.onInputCallback) {
3839
+ this.onInputCallback(this.startPendingSubmission({ text: objective, ...input }, { preserveDraft: true }));
3840
+ return true;
3688
3841
  }
3842
+ return false;
3689
3843
  }
3690
3844
 
3691
- async #handleGoalSetSubcommand(rest: string): Promise<void> {
3845
+ async #handleGoalSetSubcommand(
3846
+ rest: string,
3847
+ input?: Pick<SubmittedUserInput, "images" | "imageLinks">,
3848
+ ): Promise<boolean> {
3692
3849
  if (!this.goalModeEnabled && this.#getPausedGoalState()) {
3693
3850
  this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
3694
- return;
3851
+ return false;
3695
3852
  }
3696
3853
  const objective = rest.trim()
3697
3854
  ? rest.trim()
3698
3855
  : (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
3699
- if (!objective) return;
3700
- if (this.goalModeEnabled) {
3701
- await this.#replaceGoalFromObjective(objective);
3702
- return;
3703
- }
3704
- await this.#startGoalFromObjective(objective);
3856
+ if (!objective) return false;
3857
+ if (this.goalModeEnabled) return await this.#replaceGoalFromObjective(objective, input);
3858
+ return await this.#startGoalFromObjective(objective, input);
3705
3859
  }
3706
3860
 
3707
3861
  /** Manually (re-)open the plan-review overlay — bound to `/plan-review`. Lets
@@ -4148,6 +4302,14 @@ export class InteractiveMode implements InteractiveModeContext {
4148
4302
  * flushed there. That is preferred over leaving it queued behind a command
4149
4303
  * the user runs during the pause, which mounts immediately and would put the
4150
4304
  * older panel out of order.
4305
+ *
4306
+ * The deferral is acknowledged in {@link deferredCommandContainer}, an
4307
+ * anchored container above the editor. Nothing is mounted into the
4308
+ * transcript: a mid-turn transcript mount re-renders rows below the growing
4309
+ * live block and duplicates them in native scrollback (issues #4806/#6767),
4310
+ * which is why the earlier `showStatus` acknowledgment was reverted. An
4311
+ * anchored container is cleared and rebuilt in place, so it costs no
4312
+ * scrollback rows — the same reason the ctrl+p role-cycle track lives there.
4151
4313
  */
4152
4314
  presentCommandOutput(content: Component | readonly Component[]): void {
4153
4315
  if (!this.session.isStreaming) {
@@ -4157,14 +4319,35 @@ export class InteractiveMode implements InteractiveModeContext {
4157
4319
  const sessionId = this.sessionManager.getSessionId();
4158
4320
  if (this.#pendingCommandOutput.length > 0 && this.#pendingCommandOutputSessionId !== sessionId) {
4159
4321
  this.#pendingCommandOutput = [];
4322
+ this.#pendingCommandOutputCommands = 0;
4160
4323
  }
4161
4324
  this.#pendingCommandOutputSessionId = sessionId;
4162
4325
  const items = Array.isArray(content) ? content : [content as Component];
4163
4326
  this.#pendingCommandOutput.push(...items);
4164
- // No feedback here on purpose: mounting anything into the transcript
4165
- // mid-turn (even a status line) re-renders rows below the growing live
4166
- // block and duplicates them in native scrollback — the exact regression
4167
- // issues #4806/#6767 pin. The queue flushes at the next settle.
4327
+ this.#pendingCommandOutputCommands += 1;
4328
+ this.#renderDeferredCommandNotice();
4329
+ this.ui.requestRender();
4330
+ }
4331
+
4332
+ /**
4333
+ * Preview the queued panels above the editor so a command answers straight
4334
+ * away, then clear at settle when the real panels enter the transcript.
4335
+ *
4336
+ * Height is capped against the viewport: a `/usage` report with several
4337
+ * providers is tall enough to push the prompt off screen, and the full text
4338
+ * is a moment away in the transcript either way.
4339
+ */
4340
+ #renderDeferredCommandNotice(): void {
4341
+ this.deferredCommandContainer.clear();
4342
+ if (this.#pendingCommandOutput.length === 0) return;
4343
+ const maxRows = Math.max(
4344
+ DEFERRED_PREVIEW_MIN_ROWS,
4345
+ Math.floor(this.ui.terminal.rows * DEFERRED_PREVIEW_VIEWPORT_FRACTION),
4346
+ );
4347
+ this.deferredCommandContainer.addChild(new Spacer(1));
4348
+ this.deferredCommandContainer.addChild(
4349
+ new DeferredCommandPreview([...this.#pendingCommandOutput], maxRows, this.#pendingCommandOutputCommands),
4350
+ );
4168
4351
  }
4169
4352
 
4170
4353
  /** Mount every command panel queued for the current session while the agent was streaming. */
@@ -4174,6 +4357,8 @@ export class InteractiveMode implements InteractiveModeContext {
4174
4357
  const pendingSessionId = this.#pendingCommandOutputSessionId;
4175
4358
  this.#pendingCommandOutput = [];
4176
4359
  this.#pendingCommandOutputSessionId = undefined;
4360
+ this.#pendingCommandOutputCommands = 0;
4361
+ this.#renderDeferredCommandNotice();
4177
4362
  if (pendingSessionId !== this.sessionManager.getSessionId()) return;
4178
4363
  this.present(pending);
4179
4364
  }
@@ -4195,6 +4380,7 @@ export class InteractiveMode implements InteractiveModeContext {
4195
4380
 
4196
4381
  showError(message: string): void {
4197
4382
  this.#pendingSubmittedInput = undefined;
4383
+ this.#pendingSubmissionPreservesDraft = false;
4198
4384
  this.clearOptimisticUserMessage();
4199
4385
  this.#pendingWorkingMessage = undefined;
4200
4386
  if (this.loadingAnimation) {
@@ -5036,7 +5222,7 @@ export class InteractiveMode implements InteractiveModeContext {
5036
5222
  keybindings: KeybindingsManager,
5037
5223
  done: (result: T) => void,
5038
5224
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
5039
- options?: { overlay?: boolean },
5225
+ options?: ExtensionCustomOptions,
5040
5226
  ): Promise<T> {
5041
5227
  return this.#extensionUiController.showHookCustom(factory, options);
5042
5228
  }