@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
@@ -9,7 +9,7 @@ import type {
9
9
  ToolLoadMode,
10
10
  } from "@oh-my-pi/pi-agent-core";
11
11
  import type { ComputerSafetyCheck, ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
12
- import { sanitizeText } from "@oh-my-pi/pi-utils";
12
+ import { sanitizeText, untilAborted } from "@oh-my-pi/pi-utils";
13
13
  import type { Settings } from "../../config/settings";
14
14
  import type { Theme } from "../../modes/theme/theme";
15
15
  import { type ApprovalMode, formatApprovalPrompt, resolveApproval, truncateForPrompt } from "../../tools/approval";
@@ -190,10 +190,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
190
190
  const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
191
191
  const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
192
192
  const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
193
- if (resolveApproval(this.tool, approvalArgs(params, context), approvalMode, userPolicies).policy === "deny") {
193
+ const preResolved = resolveApproval(this.tool, approvalArgs(params, context), approvalMode, userPolicies);
194
+ if (preResolved.policy === "deny") {
194
195
  throw new Error(
195
- `Tool "${this.tool.name}" is blocked by user policy.\n` +
196
- `To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
196
+ `Tool "${preResolved.policyKey ?? this.tool.name}" is blocked by user policy.\n` +
197
+ `To allow: remove "tools.approval.${preResolved.policyKey ?? this.tool.name}: deny" from config.`,
197
198
  );
198
199
  }
199
200
 
@@ -242,8 +243,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
242
243
  context?.xdevTierResolved?.(resolved.tier);
243
244
  if (resolved.policy === "deny") {
244
245
  throw new Error(
245
- `Tool "${this.tool.name}" is blocked by user policy.\n` +
246
- `To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
246
+ `Tool "${resolved.policyKey ?? this.tool.name}" is blocked by user policy.\n` +
247
+ `To allow: remove "tools.approval.${resolved.policyKey ?? this.tool.name}: deny" from config.`,
247
248
  );
248
249
  }
249
250
  const pendingSafetyChecks = computerSafetyChecks(context);
@@ -255,7 +256,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
255
256
  // and tool-demanded overrides still prompt. Provider safety checks are
256
257
  // stronger: yolo, per-tool allow, and xdev approval never acknowledge
257
258
  // them on the user's behalf.
258
- const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, this.tool.name);
259
+ const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, resolved.policyKey ?? this.tool.name);
259
260
  const xdevBypass = context?.xdevApproved === true && effectiveParams === params;
260
261
  const approvalCheck = {
261
262
  required: pendingSafetyChecks.length > 0 || (resolved.policy === "prompt" && (explicitPrompt || !xdevBypass)),
@@ -263,6 +264,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
263
264
  };
264
265
 
265
266
  if (approvalCheck.required) {
267
+ const scheduledCall = context?.toolCall?.toolCalls[context.toolCall.index];
268
+ if (scheduledCall?.id === toolCallId && scheduledCall.name === this.tool.name) {
269
+ await untilAborted(signal, () => this.runner.waitForToolApprovalPreview(toolCallId));
270
+ }
271
+
266
272
  const hasApprovalHandlers =
267
273
  this.runner.hasHandlers("tool_approval_requested") || this.runner.hasHandlers("tool_approval_resolved");
268
274
  const sessionId = context?.sessionManager?.getSessionId() ?? "";
@@ -440,14 +440,14 @@ export class MarketplaceManager {
440
440
  return "0.0.0";
441
441
  }
442
442
 
443
- async uninstallPlugin(pluginId: string, scope?: "user" | "project"): Promise<void> {
443
+ /** Validates and removes a marketplace plugin, or only validates when `dryRun` is set. */
444
+ async uninstallPlugin(pluginId: string, scope?: "user" | "project", options?: { dryRun?: boolean }): Promise<void> {
444
445
  const parsed = parsePluginId(pluginId);
445
446
  if (!parsed) {
446
447
  throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
447
448
  }
448
449
 
449
450
  const { userEntries, projectEntries, userReg, projectReg } = await this.#findInBothRegistries(pluginId);
450
-
451
451
  const inUser = userEntries && userEntries.length > 0;
452
452
  const inProject = projectEntries && projectEntries.length > 0;
453
453
 
@@ -481,6 +481,10 @@ export class MarketplaceManager {
481
481
  const registryPath = this.#registryPath(targetScope);
482
482
  const packageNames = await this.#resolveInstalledPackageNames(targetEntries, parsed.name);
483
483
 
484
+ if (options?.dryRun) {
485
+ return;
486
+ }
487
+
484
488
  const updatedReg = removeInstalledPlugin(targetReg, pluginId);
485
489
  await writeInstalledPluginsRegistry(registryPath, updatedReg);
486
490
 
@@ -8,10 +8,10 @@
8
8
  * tests to spy on.
9
9
  */
10
10
 
11
+ import { USER_AGENT } from "@oh-my-pi/pi-utils";
11
12
  import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
12
13
  import type { HindsightConfig } from "./config";
13
14
 
14
- const USER_AGENT = "oh-my-pi-coding-agent";
15
15
  const DEFAULT_USER_AGENT = USER_AGENT;
16
16
  /** Fallback deadlines (ms) applied when the caller supplies no override. */
17
17
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
@@ -12,10 +12,6 @@ interface XAICredentials {
12
12
  baseURL: string;
13
13
  }
14
14
 
15
- export function ohMyPiXAIUserAgent(): string {
16
- return "oh-my-pi/xai";
17
- }
18
-
19
15
  /** xAI provider ids supported by shared HTTP tool transport resolution. */
20
16
  export type XAIHttpProvider = "xai-oauth" | "xai";
21
17
 
package/src/lsp/client.ts CHANGED
@@ -1339,6 +1339,7 @@ export async function sendRequest(
1339
1339
  timeout = setTimeout(() => {
1340
1340
  if (client.pendingRequests.has(id)) {
1341
1341
  client.pendingRequests.delete(id);
1342
+ void sendNotification(client, "$/cancelRequest", { id }).catch(() => {});
1342
1343
  const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
1343
1344
  cleanup();
1344
1345
  reject(err);
@@ -1405,6 +1406,7 @@ export async function sendNotification(
1405
1406
  * Shutdown all LSP clients.
1406
1407
  */
1407
1408
  export async function shutdownAll(): Promise<void> {
1409
+ stopIdleChecker();
1408
1410
  const clientsToShutdown = Array.from(clients.values());
1409
1411
  clients.clear();
1410
1412
  // Mid-initialize clients live only in clientLocks (publication is deferred
@@ -197,9 +197,9 @@ export function getConfig(cwd: string): LspConfig {
197
197
  let config = configCache.get(cwd);
198
198
  if (!config) {
199
199
  config = loadConfig(cwd);
200
- setIdleTimeout(config.idleTimeoutMs);
201
200
  configCache.set(cwd, config);
202
201
  }
202
+ setIdleTimeout(config.idleTimeoutMs);
203
203
  return config;
204
204
  }
205
205
 
@@ -357,6 +357,18 @@ export function createMCPToolName(serverName: string, toolName: string): string
357
357
  return `mcp__${sanitizedServerName}_${normalizedToolName}`;
358
358
  }
359
359
 
360
+ export interface MCPToolOriginSource {
361
+ readonly name: string;
362
+ readonly mcpServerName?: unknown;
363
+ readonly mcpToolName?: unknown;
364
+ }
365
+
366
+ /** Stable identity for a tool's original MCP route, before its public name was normalized. */
367
+ export function getMCPToolOriginKey(tool: MCPToolOriginSource): string | undefined {
368
+ if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") return undefined;
369
+ return `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
370
+ }
371
+
360
372
  /**
361
373
  * Keeps one MCP tool per minted name and logs collisions between distinct MCP
362
374
  * origins. The winner is chosen by a stable origin key (server name + original
@@ -365,19 +377,16 @@ export function createMCPToolName(serverName: string, toolName: string): string
365
377
  * silently flip ownership of the minted name. Non-MCP tools pass through
366
378
  * unchanged.
367
379
  */
368
- export function deduplicateMCPToolsByName<T extends { name: string; mcpServerName?: unknown; mcpToolName?: unknown }>(
369
- tools: readonly T[],
370
- ): T[] {
380
+ export function deduplicateMCPToolsByName<T extends MCPToolOriginSource>(tools: readonly T[]): T[] {
371
381
  const deduplicated: T[] = [];
372
382
  const registered = new Map<string, { tool: T; originKey: string; index: number }>();
373
383
 
374
384
  for (const tool of tools) {
375
- if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") {
385
+ const originKey = getMCPToolOriginKey(tool);
386
+ if (originKey === undefined) {
376
387
  deduplicated.push(tool);
377
388
  continue;
378
389
  }
379
-
380
- const originKey = `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
381
390
  const existing = registered.get(tool.name);
382
391
  if (!existing) {
383
392
  registered.set(tool.name, { tool, originKey, index: deduplicated.length });
@@ -95,19 +95,25 @@ function formatResolvedModelBadge(resolved: string, preserveProvider = false, fa
95
95
  /**
96
96
  * Resolved model + reasoning level for a hub row. Exact executor progress is
97
97
  * authoritative (and survives completion); direct live sessions are the
98
- * fallback for agents without an observer snapshot.
98
+ * fallback for agents without an observer snapshot — the main session has no
99
+ * snapshot at all, so its row is read straight off the live session.
100
+ *
101
+ * Every source reports the model that produced the row's work, never the one
102
+ * the session merely points at: an armed fallback that has not served yet stays
103
+ * attributed to whichever model last actually spoke.
99
104
  */
100
105
  export function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
101
106
  const progress = observed?.progress;
102
107
  const liveThinkingLevel = ref.session?.thinkingLevel;
108
+ const serving = ref.session?.servingModel;
103
109
  const fallbackSelector =
104
- ref.session?.retryFallbackModel ??
110
+ (serving?.isFallback ? serving.selector : undefined) ??
105
111
  (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined) ??
106
112
  (ref.history?.resolvedModelIsFallback ? ref.history.resolvedModel : undefined);
107
113
  if (fallbackSelector) {
108
114
  return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true, liveThinkingLevel)}`;
109
115
  }
110
- const resolvedModel = progress?.resolvedModel ?? ref.history?.resolvedModel;
116
+ const resolvedModel = progress?.resolvedModel ?? ref.history?.resolvedModel ?? serving?.selector;
111
117
  if (resolvedModel) return formatResolvedModelBadge(resolvedModel, false, liveThinkingLevel);
112
118
  const model = ref.session?.model;
113
119
  if (!model) return undefined;
@@ -36,6 +36,7 @@ import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from ".
36
36
  import { registerPersistedSubagents } from "../../registry/persisted-agents";
37
37
  import { USER_INTERRUPT_LABEL } from "../../session/messages";
38
38
  import { shortenPath, truncateToWidth } from "../../tools/render-utils";
39
+ import { formatLocalDateTimeWithOffset } from "../../utils/local-date";
39
40
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
40
41
  import { theme } from "../theme/theme";
41
42
  import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
@@ -829,7 +830,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
829
830
  `Spawned by ${sanitizeDisplayText(ref.parentId ?? MAIN_AGENT_ID)}${children.length > 0 ? ` · ${children.length} children` : ""}`,
830
831
  );
831
832
  if (children.length > 0) add(theme.fg("dim", formatChildIds(children, width)));
832
- add(theme.fg("dim", `Registered ${new Date(ref.createdAt).toISOString().slice(0, 16).replace("T", " ")}Z`));
833
+ add(theme.fg("dim", `Registered ${formatLocalDateTimeWithOffset(new Date(ref.createdAt))}`));
833
834
 
834
835
  section("Changes");
835
836
  add(
@@ -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;