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

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 (143) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
  3. package/dist/cli.js +3658 -3627
  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 +14 -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 +4 -0
  25. package/dist/types/session/agent-session.d.ts +24 -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 +35 -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/builtin-names.d.ts +1 -2
  37. package/dist/types/tools/index.d.ts +1 -0
  38. package/dist/types/tools/think.d.ts +38 -0
  39. package/dist/types/tools/todo.d.ts +14 -15
  40. package/dist/types/tools/write.d.ts +2 -2
  41. package/dist/types/utils/local-date.d.ts +2 -0
  42. package/dist/types/vibe/runtime.d.ts +1 -1
  43. package/dist/types/web/parallel.d.ts +1 -0
  44. package/dist/types/web/search/providers/brave.d.ts +8 -3
  45. package/dist/types/web/search/providers/codex.d.ts +6 -0
  46. package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
  47. package/dist/types/web/search/providers/jina.d.ts +3 -3
  48. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  49. package/dist/types/web/search/providers/perplexity.d.ts +4 -0
  50. package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
  51. package/package.json +13 -13
  52. package/src/advisor/delta-split.ts +98 -0
  53. package/src/advisor/runtime.ts +321 -69
  54. package/src/async/job-manager.ts +14 -3
  55. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  56. package/src/cli/plugin-cli.ts +30 -2
  57. package/src/cli/update-cli.ts +259 -24
  58. package/src/config/keybindings.ts +52 -9
  59. package/src/config/model-resolver.ts +19 -3
  60. package/src/config/settings-schema.ts +16 -0
  61. package/src/cursor.ts +10 -5
  62. package/src/discovery/agents-md.ts +61 -23
  63. package/src/eval/jl/kernel.ts +2 -20
  64. package/src/eval/py/kernel.ts +2 -20
  65. package/src/eval/rb/kernel.ts +2 -20
  66. package/src/eval/runner-cache.ts +41 -0
  67. package/src/exec/non-interactive-env.ts +14 -3
  68. package/src/extensibility/extensions/loader.ts +5 -2
  69. package/src/extensibility/extensions/runner.ts +184 -66
  70. package/src/extensibility/extensions/types.ts +26 -2
  71. package/src/extensibility/extensions/wrapper.ts +13 -7
  72. package/src/extensibility/plugins/marketplace/manager.ts +6 -2
  73. package/src/hindsight/client.ts +1 -1
  74. package/src/lib/xai-http.ts +0 -4
  75. package/src/lsp/client.ts +2 -0
  76. package/src/lsp/servers.ts +1 -1
  77. package/src/mcp/tool-bridge.ts +15 -6
  78. package/src/modes/components/agent-hub-renderer.ts +9 -3
  79. package/src/modes/components/agent-hub.ts +2 -1
  80. package/src/modes/components/status-line/component.ts +58 -6
  81. package/src/modes/components/status-line/segments.ts +12 -1
  82. package/src/modes/components/status-line/types.ts +1 -0
  83. package/src/modes/components/user-message.ts +20 -5
  84. package/src/modes/controllers/event-controller.ts +48 -0
  85. package/src/modes/controllers/extension-ui-controller.ts +14 -7
  86. package/src/modes/controllers/input-controller.ts +25 -6
  87. package/src/modes/controllers/selector-controller.ts +5 -0
  88. package/src/modes/interactive-mode.ts +315 -129
  89. package/src/modes/rpc/rpc-frame.ts +13 -5
  90. package/src/modes/theme/tui-adapters.ts +4 -5
  91. package/src/modes/types.ts +13 -7
  92. package/src/modes/utils/hotkeys-markdown.ts +10 -6
  93. package/src/prompts/system/system-prompt.md +10 -1
  94. package/src/registry/persisted-agents.ts +43 -8
  95. package/src/sdk.ts +146 -9
  96. package/src/session/agent-session-types.ts +4 -0
  97. package/src/session/agent-session.ts +91 -10
  98. package/src/session/messages.ts +98 -28
  99. package/src/session/retry-fallback-chains.ts +14 -0
  100. package/src/session/session-advisors.ts +32 -15
  101. package/src/session/session-history-format.ts +15 -1
  102. package/src/session/session-maintenance.ts +8 -8
  103. package/src/session/session-manager.ts +6 -2
  104. package/src/session/session-tools.ts +355 -183
  105. package/src/session/turn-recovery.ts +225 -47
  106. package/src/slash-commands/builtin-modes.ts +41 -12
  107. package/src/slash-commands/types.ts +5 -1
  108. package/src/task/executor.ts +92 -45
  109. package/src/task/structured-subagent.ts +5 -5
  110. package/src/tools/approval.ts +44 -10
  111. package/src/tools/builtin-names.ts +1 -2
  112. package/src/tools/fetch.ts +21 -2
  113. package/src/tools/image-gen.ts +6 -8
  114. package/src/tools/index.ts +8 -0
  115. package/src/tools/renderers.ts +2 -0
  116. package/src/tools/think.ts +62 -0
  117. package/src/tools/todo.ts +70 -26
  118. package/src/tools/tts.ts +3 -2
  119. package/src/tools/write.ts +7 -3
  120. package/src/utils/local-date.ts +13 -0
  121. package/src/utils/tools-manager.ts +2 -2
  122. package/src/vibe/runtime.ts +22 -14
  123. package/src/web/kagi.ts +91 -34
  124. package/src/web/parallel.ts +11 -2
  125. package/src/web/scrapers/crates-io.ts +2 -2
  126. package/src/web/scrapers/discogs.ts +2 -2
  127. package/src/web/scrapers/docs-rs.ts +2 -2
  128. package/src/web/scrapers/github.ts +2 -2
  129. package/src/web/scrapers/musicbrainz.ts +1 -2
  130. package/src/web/scrapers/pubmed.ts +2 -2
  131. package/src/web/scrapers/sec-edgar.ts +2 -2
  132. package/src/web/search/providers/brave.ts +121 -46
  133. package/src/web/search/providers/codex.ts +88 -12
  134. package/src/web/search/providers/exa.ts +45 -10
  135. package/src/web/search/providers/firecrawl.ts +53 -11
  136. package/src/web/search/providers/gemini.ts +139 -27
  137. package/src/web/search/providers/jina.ts +48 -25
  138. package/src/web/search/providers/parallel.ts +23 -9
  139. package/src/web/search/providers/perplexity.ts +24 -7
  140. package/src/web/search/providers/searxng.ts +77 -1
  141. package/src/web/search/providers/tavily.ts +23 -22
  142. package/src/web/search/providers/tinyfish.ts +44 -10
  143. package/src/web/search/providers/xai.ts +85 -14
@@ -397,6 +397,7 @@ export class StatusLineComponent implements Component {
397
397
  tier?: string;
398
398
  fiveHour?: { percent: number; resetMinutes?: number };
399
399
  sevenDay?: { percent: number; resetHours?: number };
400
+ monthly?: { percent: number; resetHours?: number };
400
401
  } | null = null;
401
402
  #cachedUsageContextKey: string | null = null;
402
403
  #usageFetchedAt = 0;
@@ -1258,8 +1259,12 @@ export class StatusLineComponent implements Component {
1258
1259
  const normalized = this.#normalizeUsageReports(reports, activeProvider, activeIdentity);
1259
1260
  const resetSnapshot =
1260
1261
  activeProvider === "openai-codex" ? this.#normalizeCodexResetSnapshot(reports, activeIdentity) : null;
1262
+ const usageChanged = this.#cachedUsage !== normalized;
1261
1263
  this.#cachedUsage = normalized;
1262
1264
  this.#usageFetchedAt = Date.now();
1265
+ // Usage fetch is async; without a repaint the top border stays blank until
1266
+ // some unrelated event (git resolve, keystroke, …) rebuilds it.
1267
+ if (usageChanged) this.#onBranchChange?.();
1263
1268
  if (!resetSnapshot) return;
1264
1269
  const contextKey = this.#formatUsageContextKey(activeProvider, activeIdentity);
1265
1270
  const previous = this.#codexResetSnapshots.get(contextKey);
@@ -1368,13 +1373,25 @@ export class StatusLineComponent implements Component {
1368
1373
  tier?: string;
1369
1374
  fiveHour?: { percent: number; resetMinutes?: number };
1370
1375
  sevenDay?: { percent: number; resetHours?: number };
1376
+ monthly?: { percent: number; resetHours?: number };
1371
1377
  } | null {
1372
1378
  if (!Array.isArray(reports)) return null;
1373
1379
  let fiveHour: { percent: number; resetMinutes?: number } | undefined;
1374
1380
  let sevenDay: { percent: number; resetHours?: number } | undefined;
1381
+ let monthly: { percent: number; resetHours?: number } | undefined;
1375
1382
  let fiveHourTier: string | undefined;
1376
1383
  let sevenDayTier: string | undefined;
1384
+ let monthlyTier: string | undefined;
1385
+ let monthlyPriority = Number.POSITIVE_INFINITY;
1377
1386
  const now = Date.now();
1387
+ const cursorMonthlyPriority = (limitId: unknown): number => {
1388
+ // When /auth/usage and /api/usage-summary are merged, prefer the personal
1389
+ // dashboard rails over legacy per-model request fractions.
1390
+ if (limitId === "cursor:usd:individual-auto") return 0;
1391
+ if (limitId === "cursor:usd:individual-plan" || limitId === "cursor:usd:individual-overall") return 1;
1392
+ if (typeof limitId === "string" && limitId.startsWith("cursor:usd:individual-")) return 2;
1393
+ return 3;
1394
+ };
1378
1395
  for (const report of reports) {
1379
1396
  if (!report || typeof report !== "object") continue;
1380
1397
  const provider = (report as { provider?: unknown }).provider;
@@ -1388,8 +1405,9 @@ export class StatusLineComponent implements Component {
1388
1405
  continue;
1389
1406
  }
1390
1407
  const l = limit as {
1408
+ id?: string;
1391
1409
  scope?: { windowId?: string; tier?: string };
1392
- window?: { resetsAt?: number };
1410
+ window?: { resetsAt?: number; durationMs?: number };
1393
1411
  amount?: { usedFraction?: number };
1394
1412
  };
1395
1413
  const fraction = l.amount?.usedFraction;
@@ -1397,9 +1415,22 @@ export class StatusLineComponent implements Component {
1397
1415
  const windowId = l.scope?.windowId;
1398
1416
  const tier = l.scope?.tier;
1399
1417
  const resetsAt = l.window?.resetsAt;
1418
+ // Canonical window ids win. Fall back to the reported span (same
1419
+ // tolerance as the 5h priority-boost check) so providers that emit
1420
+ // non-canonical ids, and cache rows written before a provider was
1421
+ // canonicalized, still map onto the two subscription windows.
1422
+ const durationMs = l.window?.durationMs;
1423
+ const windowClass =
1424
+ windowId === "5h" || windowId === "7d"
1425
+ ? windowId
1426
+ : durationMs !== undefined && Math.abs(durationMs - 5 * 3_600_000) <= 60_000
1427
+ ? "5h"
1428
+ : durationMs !== undefined && Math.abs(durationMs - 7 * 86_400_000) <= 60_000
1429
+ ? "7d"
1430
+ : undefined;
1400
1431
  // Accept tiered limits, but prefer untiered (backward compat with Anthropic).
1401
1432
  // An untiered limit always replaces a tiered one; among same-tieredness, first wins.
1402
- if (windowId === "5h" && (!fiveHour || (fiveHourTier !== undefined && !tier))) {
1433
+ if (windowClass === "5h" && (!fiveHour || (fiveHourTier !== undefined && !tier))) {
1403
1434
  fiveHour = {
1404
1435
  percent: fraction * 100,
1405
1436
  resetMinutes:
@@ -1407,7 +1438,7 @@ export class StatusLineComponent implements Component {
1407
1438
  };
1408
1439
  fiveHourTier = tier || undefined;
1409
1440
  }
1410
- if (windowId === "7d" && (!sevenDay || (sevenDayTier !== undefined && !tier))) {
1441
+ if (windowClass === "7d" && (!sevenDay || (sevenDayTier !== undefined && !tier))) {
1411
1442
  sevenDay = {
1412
1443
  percent: fraction * 100,
1413
1444
  resetHours:
@@ -1415,12 +1446,33 @@ export class StatusLineComponent implements Component {
1415
1446
  };
1416
1447
  sevenDayTier = tier || undefined;
1417
1448
  }
1449
+ // Conservatively gate monthly status-line rendering to Cursor for now —
1450
+ // Copilot/OpenCode also emit monthly windows, but their multi-bucket
1451
+ // shape needs a dedicated selector before we surface `mo N%` for them.
1452
+ if (activeProvider === "cursor" && (windowId === "monthly" || windowId === "30d")) {
1453
+ const priority = cursorMonthlyPriority(l.id);
1454
+ const shouldReplace =
1455
+ !monthly ||
1456
+ priority < monthlyPriority ||
1457
+ (priority === monthlyPriority && monthlyTier !== undefined && !tier);
1458
+ if (shouldReplace) {
1459
+ monthly = {
1460
+ percent: fraction * 100,
1461
+ resetHours:
1462
+ typeof resetsAt === "number"
1463
+ ? Math.max(0, Math.round((resetsAt - now) / 3_600_000))
1464
+ : undefined,
1465
+ };
1466
+ monthlyTier = tier || undefined;
1467
+ monthlyPriority = priority;
1468
+ }
1469
+ }
1418
1470
  }
1419
1471
  }
1420
- if (!fiveHour && !sevenDay) return null;
1472
+ if (!fiveHour && !sevenDay && !monthly) return null;
1421
1473
  // Single compact label; prefer the five-hour tier if displayed windows ever disagree.
1422
- const effectiveTier = fiveHourTier ?? sevenDayTier;
1423
- return { tier: effectiveTier, fiveHour, sevenDay };
1474
+ const effectiveTier = fiveHourTier ?? sevenDayTier ?? monthlyTier;
1475
+ return { tier: effectiveTier, fiveHour, sevenDay, monthly };
1424
1476
  }
1425
1477
 
1426
1478
  /**
@@ -639,7 +639,7 @@ const usageSegment: StatusLineSegment = {
639
639
  id: "usage",
640
640
  render(ctx) {
641
641
  const u = ctx.usage;
642
- if (!u || (!u.fiveHour && !u.sevenDay)) {
642
+ if (!u || (!u.fiveHour && !u.sevenDay && !u.monthly)) {
643
643
  return { content: "", visible: false };
644
644
  }
645
645
  const parts: string[] = [];
@@ -665,6 +665,17 @@ const usageSegment: StatusLineSegment = {
665
665
  : "";
666
666
  parts.push(`7d ${pctText}${reset}`);
667
667
  }
668
+ if (u.monthly) {
669
+ const pct = u.monthly.percent;
670
+ // Cursor-only today (normalize gates monthly to provider === "cursor").
671
+ // Cursor's web dashboard floors included-usage percents (1.88 → "1% used").
672
+ const pctText = theme.fg(pickUsageColor(pct), `${Math.floor(pct)}%`);
673
+ const reset =
674
+ u.monthly.resetHours !== undefined
675
+ ? theme.fg("muted", ` (${formatUsageReset(u.monthly.resetHours, "h")})`)
676
+ : "";
677
+ parts.push(`mo ${pctText}${reset}`);
678
+ }
668
679
  const content = withIcon(theme.icon.time, parts.join(theme.sep.dot));
669
680
  return { content, visible: true };
670
681
  },
@@ -122,6 +122,7 @@ export interface SegmentContext {
122
122
  tier?: string;
123
123
  fiveHour?: { percent: number; resetMinutes?: number };
124
124
  sevenDay?: { percent: number; resetHours?: number };
125
+ monthly?: { percent: number; resetHours?: number };
125
126
  } | null;
126
127
  }
127
128
 
@@ -4,12 +4,27 @@ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
4
4
  import { imageReferenceHyperlink, renderPlaceholders } from "../image-references";
5
5
  import { highlightMagicKeywords } from "../magic-keywords";
6
6
 
7
- // OSC 133 shell integration: marks prompt zones for terminal multiplexers
8
- // Do not emit OSC 133 C ("command start") here: the transcript has no matching
9
- // command-finished marker, so terminals can group later assistant/tool output
10
- // under the first submitted prompt.
7
+ // OSC 133 shell integration: marks prompt zones for terminal multiplexers.
8
+ //
9
+ // The zone must be *closed* within the same render. `133;B` sets a sticky
10
+ // cursor semantic of `.input` in Ghostty (and Ghostty-derived terminals such
11
+ // as cmux) that only a command-start marker clears; leaving it latched makes
12
+ // `cursorIsAtPrompt()` permanently true and tags every subsequently painted
13
+ // cell as `.input`. Combined with `cursor-click-to-move = true` (Ghostty's
14
+ // default) that turns every left-click inside the pane into a burst of
15
+ // synthesized arrow keys on omp's pty, slamming the editor caret to column 0
16
+ // (#8030, #6115).
17
+ //
18
+ // `133;C` is therefore emitted immediately followed by `133;D;0` at the end of
19
+ // the bubble. That clears the input state without reintroducing the grouping
20
+ // problem the marker was originally omitted to avoid: the command zone opens
21
+ // and finishes inside this component, so later assistant/tool output can never
22
+ // be grouped under the first submitted prompt.
11
23
  const OSC133_ZONE_START = "\x1b]133;A\x07";
12
24
  const OSC133_ZONE_END = "\x1b]133;B\x07";
25
+ const OSC133_COMMAND_START = "\x1b]133;C\x07";
26
+ const OSC133_COMMAND_DONE = "\x1b]133;D;0\x07";
27
+ const OSC133_ZONE_CLOSE = OSC133_ZONE_END + OSC133_COMMAND_START + OSC133_COMMAND_DONE;
13
28
 
14
29
  /**
15
30
  * Component that renders a user message
@@ -61,7 +76,7 @@ export class UserMessageComponent extends Container {
61
76
  }
62
77
  const wrapped = lines.slice();
63
78
  wrapped[0] = OSC133_ZONE_START + wrapped[0];
64
- wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END;
79
+ wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_CLOSE;
65
80
  this.#zoneSource = lines;
66
81
  this.#zoneLines = wrapped;
67
82
  return wrapped;
@@ -72,6 +72,12 @@ function exposesRawPartialJson(toolName: string, rawInput: boolean, tool: unknow
72
72
  type AgentSessionEventHandlers = {
73
73
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
74
74
  };
75
+ interface ApprovalPreviewGate {
76
+ promise: Promise<void>;
77
+ resolve(): void;
78
+ reject(reason?: unknown): void;
79
+ started: boolean;
80
+ }
75
81
 
76
82
  export class EventController {
77
83
  #lastReadGroup: ReadToolGroupComponent | undefined = undefined;
@@ -88,6 +94,8 @@ export class EventController {
88
94
  /** Tool calls whose approval prompt drove the title into `attention`; cleared
89
95
  * at their tool_execution_end so the title returns to `working`. */
90
96
  #approvalAttentionToolCallIds = new Set<string>();
97
+ #approvalPreviewGates = new Map<string, ApprovalPreviewGate>();
98
+ #detachToolApprovalPreviewWaiter: (() => void) | undefined;
91
99
  #readToolCallArgs = new Map<string, Record<string, unknown>>();
92
100
  #readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
93
101
  #toolTimelineComponents = new Map<string, Component>();
@@ -197,6 +205,9 @@ export class EventController {
197
205
  // vocalizer falls back to mechanical cleanup when unset. Tolerates
198
206
  // partial contexts (tests, minimal embeddings) by wiring null.
199
207
  const session = ctx.session;
208
+ this.#detachToolApprovalPreviewWaiter = session?.extensionRunner?.setToolApprovalPreviewWaiter(toolCallId =>
209
+ this.#waitForToolApprovalPreview(toolCallId),
210
+ );
200
211
  vocalizer.setEnhancer(
201
212
  session?.modelRegistry && session.agent && session.settings
202
213
  ? new SpeechEnhancer({
@@ -273,6 +284,9 @@ export class EventController {
273
284
  }
274
285
 
275
286
  dispose(): void {
287
+ this.#detachToolApprovalPreviewWaiter?.();
288
+ this.#detachToolApprovalPreviewWaiter = undefined;
289
+ this.#clearApprovalPreviewGates();
276
290
  if (this.#messageUpdateTimer) {
277
291
  clearTimeout(this.#messageUpdateTimer);
278
292
  this.#messageUpdateTimer = undefined;
@@ -294,6 +308,36 @@ export class EventController {
294
308
  this.#lastReadGroup?.finalize();
295
309
  this.#lastReadGroup = undefined;
296
310
  }
311
+ #approvalPreviewGate(toolCallId: string): ApprovalPreviewGate {
312
+ let gate = this.#approvalPreviewGates.get(toolCallId);
313
+ if (!gate) {
314
+ const deferred = Promise.withResolvers<void>();
315
+ gate = { ...deferred, started: false };
316
+ this.#approvalPreviewGates.set(toolCallId, gate);
317
+ }
318
+ return gate;
319
+ }
320
+
321
+ async #waitForToolApprovalPreview(toolCallId: string): Promise<void> {
322
+ await this.#approvalPreviewGate(toolCallId).promise;
323
+ }
324
+
325
+ #startToolApprovalPreview(toolCallId: string): void {
326
+ const gate = this.#approvalPreviewGate(toolCallId);
327
+ if (gate.started) return;
328
+ gate.started = true;
329
+ const component = this.ctx.pendingTools.get(toolCallId);
330
+ const ready = component instanceof ToolExecutionComponent ? component.whenPreviewSettled() : Promise.resolve();
331
+ void ready.then(() => {
332
+ this.ctx.ui.requestRender();
333
+ gate.resolve();
334
+ }, gate.reject);
335
+ }
336
+
337
+ #clearApprovalPreviewGates(): void {
338
+ for (const gate of this.#approvalPreviewGates.values()) gate.resolve();
339
+ this.#approvalPreviewGates.clear();
340
+ }
297
341
 
298
342
  #getReadGroup(): ReadToolGroupComponent {
299
343
  if (!this.#lastReadGroup) {
@@ -687,6 +731,7 @@ export class EventController {
687
731
  }
688
732
 
689
733
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
734
+ this.#clearApprovalPreviewGates();
690
735
  this.#toolTimelineComponents.clear();
691
736
  this.#streamedToolCallIdByIndex.clear();
692
737
  this.#retractedToolCallIds.clear();
@@ -1313,6 +1358,7 @@ export class EventController {
1313
1358
  this.ctx.pendingTools.set(event.toolCallId, group);
1314
1359
  this.#toolTimelineComponents.set(event.toolCallId, group);
1315
1360
  }
1361
+ this.#startToolApprovalPreview(event.toolCallId);
1316
1362
  this.ctx.ui.requestRender();
1317
1363
  return;
1318
1364
  }
@@ -1336,6 +1382,7 @@ export class EventController {
1336
1382
  this.ctx.sessionManager.getCwd(),
1337
1383
  event.toolCallId,
1338
1384
  );
1385
+ component.setArgsComplete(event.toolCallId);
1339
1386
  component.setExpanded(this.ctx.toolOutputExpanded);
1340
1387
  component.setToolActivityVisible(!this.ctx.hideToolActivity);
1341
1388
  this.ctx.chatContainer.addChild(component);
@@ -1363,6 +1410,7 @@ export class EventController {
1363
1410
  this.ctx.ui.requestRender();
1364
1411
  }
1365
1412
  }
1413
+ this.#startToolApprovalPreview(event.toolCallId);
1366
1414
  }
1367
1415
 
1368
1416
  /**
@@ -10,6 +10,7 @@ import type {
10
10
  ExtensionAskDialogResultItem,
11
11
  ExtensionCommandContextActions,
12
12
  ExtensionContextActions,
13
+ ExtensionCustomOptions,
13
14
  ExtensionError,
14
15
  ExtensionUIContext,
15
16
  ExtensionUIDialogOptions,
@@ -1047,7 +1048,7 @@ export class ExtensionUiController {
1047
1048
  keybindings: KeybindingsManager,
1048
1049
  done: (result: T) => void,
1049
1050
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
1050
- options?: { overlay?: boolean },
1051
+ options?: ExtensionCustomOptions,
1051
1052
  ): Promise<T> {
1052
1053
  const savedText = this.ctx.editor.getText();
1053
1054
  const keybindings = KeybindingsManager.inMemory();
@@ -1080,12 +1081,18 @@ export class ExtensionUiController {
1080
1081
  }
1081
1082
  component = c;
1082
1083
  if (options?.overlay) {
1083
- overlayHandle = this.ctx.ui.showOverlay(component, {
1084
- anchor: "bottom-center",
1085
- width: "100%",
1086
- maxHeight: "100%",
1087
- margin: 0,
1088
- });
1084
+ const overlayOptions =
1085
+ typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
1086
+ overlayHandle = this.ctx.ui.showOverlay(
1087
+ component,
1088
+ overlayOptions ?? {
1089
+ anchor: "bottom-center",
1090
+ width: "100%",
1091
+ maxHeight: "100%",
1092
+ margin: 0,
1093
+ },
1094
+ );
1095
+ options.onHandle?.(overlayHandle);
1089
1096
  return;
1090
1097
  }
1091
1098
  this.ctx.editorContainer.clear();
@@ -23,6 +23,7 @@ import type { InteractiveModeContext } from "../../modes/types";
23
23
  import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
24
24
  import { USER_INTERRUPT_LABEL } from "../../session/messages";
25
25
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
26
+ import { parseSlashCommand } from "../../slash-commands/helpers/parse";
26
27
  import { isTinyTitleLocalModelKey } from "../../tiny/models";
27
28
  import { tinyTitleClient } from "../../tiny/title-client";
28
29
  import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
@@ -678,6 +679,7 @@ export class InputController {
678
679
  let inputImageLinks =
679
680
  this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
680
681
  let hasInputImages = (inputImages?.length ?? 0) > 0;
682
+ const submittedImages = inputImages;
681
683
 
682
684
  if (runner?.hasHandlers("input")) {
683
685
  const result = await runner.emitInput(text, inputImages, "interactive");
@@ -697,6 +699,22 @@ export class InputController {
697
699
  }
698
700
  hasInputImages = (inputImages?.length ?? 0) > 0;
699
701
  }
702
+ const submittedMode = parseSlashCommand(text)?.name;
703
+ const draftDetached =
704
+ submittedMode === "plan" ||
705
+ submittedMode === "vibe" ||
706
+ submittedMode === "goal" ||
707
+ submittedMode === "guided-goal";
708
+ if (
709
+ draftDetached &&
710
+ submittedImages?.length &&
711
+ submittedImages.every((image, index) => this.ctx.editor.pendingImages[index] === image)
712
+ ) {
713
+ this.ctx.editor.pendingImages.splice(0, submittedImages.length);
714
+ this.ctx.editor.pendingImageLinks.splice(0, submittedImages.length);
715
+ this.ctx.editor.imageLinks =
716
+ this.ctx.editor.pendingImageLinks.length > 0 ? this.ctx.editor.pendingImageLinks : undefined;
717
+ }
700
718
 
701
719
  if (!text && !hasInputImages) return;
702
720
 
@@ -712,9 +730,11 @@ export class InputController {
712
730
 
713
731
  // Handle built-in slash commands
714
732
  if (text) {
715
- const slashResult = await executeBuiltinSlashCommand(text, {
716
- ctx: this.ctx,
717
- });
733
+ const input =
734
+ (inputImages?.length ?? 0) > 0 || (inputImageLinks?.length ?? 0) > 0
735
+ ? { images: inputImages, imageLinks: inputImageLinks }
736
+ : undefined;
737
+ const slashResult = await executeBuiltinSlashCommand(text, { ctx: this.ctx, input, draftDetached });
718
738
  if (slashResult === true) {
719
739
  if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
720
740
  return;
@@ -1330,9 +1350,8 @@ export class InputController {
1330
1350
  }
1331
1351
 
1332
1352
  if (text) {
1333
- const slashResult = await executeBuiltinSlashCommand(text, {
1334
- ctx: this.ctx,
1335
- });
1353
+ const input = (images?.length ?? 0) > 0 || (imageLinks?.length ?? 0) > 0 ? { images, imageLinks } : undefined;
1354
+ const slashResult = await executeBuiltinSlashCommand(text, { ctx: this.ctx, input });
1336
1355
  if (slashResult === true) {
1337
1356
  if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
1338
1357
  return;
@@ -479,6 +479,11 @@ export class SelectorController {
479
479
  this.ctx.showError(`Failed to apply vision mode: ${err}`);
480
480
  });
481
481
  break;
482
+ case "externalThinking":
483
+ void this.ctx.session.setThinkToolEnabled(value as boolean).catch(err => {
484
+ this.ctx.showError(`Failed to apply external thinking: ${err}`);
485
+ });
486
+ break;
482
487
 
483
488
  case "autocompleteMaxVisible":
484
489
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));