@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2

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 (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -43,7 +43,7 @@ import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-stora
43
43
  import type { CompactMode } from "../../session/compact-modes";
44
44
  import type { NewSessionOptions } from "../../session/session-entries";
45
45
  import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
46
- import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
46
+ import { formatActiveAccountLabel, limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
47
47
  import { outputMeta } from "../../tools/output-meta";
48
48
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
49
49
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -64,7 +64,7 @@ function showMarkdownPanel(ctx: InteractiveModeContext, title: string, markdown:
64
64
  block.addChild(new Spacer(1));
65
65
  block.addChild(new Markdown(markdown.trim(), 1, 1, getMarkdownTheme()));
66
66
  block.addChild(new DynamicBorder());
67
- ctx.present(block);
67
+ ctx.presentCommandOutput(block);
68
68
  }
69
69
 
70
70
  export class CommandController {
@@ -349,46 +349,119 @@ export class CommandController {
349
349
  this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
350
350
  }
351
351
 
352
+ static readonly #advisorStatusGlyph: Record<string, string> = {
353
+ running: "●",
354
+ paused: "○",
355
+ no_model: "○",
356
+ quota_exhausted: "✕",
357
+ error: "✕",
358
+ };
359
+
360
+ static readonly #advisorStatusLabel: Record<string, string> = {
361
+ running: "running",
362
+ paused: "off",
363
+ no_model: "no model",
364
+ quota_exhausted: "quota exhausted",
365
+ error: "error",
366
+ };
367
+
352
368
  async handleAdvisorStatusCommand(): Promise<void> {
353
369
  const stats = this.ctx.session.getAdvisorStats();
354
- if (!stats.active) {
355
- this.ctx.present([
356
- new Spacer(1),
357
- new Text(
358
- stats.configured
359
- ? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
360
- : "Advisor is disabled.",
361
- 1,
362
- 0,
363
- ),
364
- ]);
370
+ if (!stats.configured) {
371
+ this.ctx.present([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
365
372
  return;
366
373
  }
367
- if (stats.advisors.length > 1) {
374
+ // Fetch live quota data (cached 5 min by the auth-gateway) so we can show
375
+ // real usage windows/reset timers per advisor provider. Non-fatal when absent.
376
+ const usageProvider = this.ctx.session as { fetchUsageReports?: () => Promise<UsageReport[] | null> };
377
+ let usageReports: UsageReport[] | null = null;
378
+ if (usageProvider.fetchUsageReports) {
379
+ try {
380
+ usageReports = await usageProvider.fetchUsageReports();
381
+ } catch {
382
+ // Network/auth failure is non-fatal — just skip the quota line.
383
+ }
384
+ }
385
+ // Resolve the active OAuth identity for each advisor's provider so quota
386
+ // filtering matches the credential actually in use (not sibling accounts).
387
+ const resolveActiveAdvisorAccount = (provider: string, sessionId?: string): OAuthAccountIdentity | undefined =>
388
+ this.ctx.session.modelRegistry.authStorage.getOAuthAccountIdentity(
389
+ provider,
390
+ sessionId ?? this.ctx.session.sessionId,
391
+ );
392
+ const nowMs = Date.now();
393
+ // Roster view: show every configured advisor with its status, even when
394
+ // none are live (all paused/no-model). The old code returned a generic
395
+ // message that hid the per-advisor state the user needs to act on.
396
+ if (stats.advisors.length > 1 || (stats.configured && !stats.active)) {
368
397
  let info = `${theme.bold("Advisor Status")} (${stats.advisors.length} advisors)\n`;
369
398
  for (const a of stats.advisors) {
370
- const ctx =
371
- a.contextWindow > 0
372
- ? `${a.contextTokens.toLocaleString()} / ${a.contextWindow.toLocaleString()} (${Math.round((a.contextTokens / a.contextWindow) * 100)}%)`
373
- : `${a.contextTokens.toLocaleString()}`;
374
- info += `\n${theme.bold(a.name)}\n`;
375
- info += `${theme.fg("dim", "Model:")} ${a.model.provider}/${a.model.id}\n`;
376
- info += `${theme.fg("dim", "Context:")} ${ctx}\n`;
377
- info += `${theme.fg("dim", "Messages:")} ${a.messages.total.toLocaleString()}\n`;
378
- info += `${theme.fg("dim", "Spend:")} ${a.tokens.input.toLocaleString()} in / ${a.tokens.output.toLocaleString()} out`;
379
- if (a.cost > 0) info += `, $${a.cost.toFixed(4)}`;
380
- info += "\n";
399
+ const glyph = CommandController.#advisorStatusGlyph[a.status] ?? "?";
400
+ const label = CommandController.#advisorStatusLabel[a.status] ?? a.status;
401
+ const color =
402
+ a.status === "running"
403
+ ? "success"
404
+ : a.status === "quota_exhausted" || a.status === "error"
405
+ ? "error"
406
+ : "dim";
407
+ info += `\n${theme.fg(color, glyph)} ${theme.bold(a.name)} ${theme.fg("dim", `[${label}]`)}\n`;
408
+ if (a.model) {
409
+ info += `${theme.fg("dim", "Model:")} ${a.model.provider}/${a.model.id}\n`;
410
+ }
411
+ if (a.model && usageReports) {
412
+ const quota = formatCompactQuota(
413
+ a.model.provider,
414
+ usageReports,
415
+ nowMs,
416
+ resolveActiveAdvisorAccount(a.model.provider, a.sessionId),
417
+ );
418
+ if (quota) info += `${theme.fg("dim", quota)}\n`;
419
+ }
420
+ if (a.status === "running" || a.status === "quota_exhausted") {
421
+ const ctx =
422
+ a.contextWindow > 0
423
+ ? `${a.contextTokens.toLocaleString()} / ${a.contextWindow.toLocaleString()} (${Math.round((a.contextTokens / a.contextWindow) * 100)}%)`
424
+ : `${a.contextTokens.toLocaleString()}`;
425
+ info += `${theme.fg("dim", "Context:")} ${ctx}\n`;
426
+ info += `${theme.fg("dim", "Messages:")} ${a.messages.total.toLocaleString()}\n`;
427
+ info += `${theme.fg("dim", "Spend:")} ${a.tokens.input.toLocaleString()} in / ${a.tokens.output.toLocaleString()} out`;
428
+ if (a.cost > 0) info += `, $${a.cost.toFixed(4)}`;
429
+ info += "\n";
430
+ }
431
+ }
432
+ if (stats.active) {
433
+ info += `\n${theme.bold("Totals")}\n`;
434
+ info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
435
+ if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
381
436
  }
382
- info += `\n${theme.bold("Totals")}\n`;
383
- info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
384
- if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
385
437
  this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
386
438
  return;
387
439
  }
388
- const model = stats.model!;
440
+ // Single active advisor — detailed view.
441
+ const model = stats.model;
389
442
  let info = `${theme.bold("Advisor Status")}\n\n`;
390
- info += `${theme.bold("Provider")}\n`;
391
- info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
443
+ if (stats.advisors.length === 1) {
444
+ const a = stats.advisors[0];
445
+ const glyph = CommandController.#advisorStatusGlyph[a.status] ?? "?";
446
+ const label = CommandController.#advisorStatusLabel[a.status] ?? a.status;
447
+ info += `${theme.fg(a.status === "running" ? "success" : "error", glyph)} ${a.name} ${theme.fg("dim", `[${label}]`)}\n\n`;
448
+ }
449
+ if (model) {
450
+ info += `${theme.bold("Provider")}\n`;
451
+ info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
452
+ }
453
+ if (model && usageReports) {
454
+ const quota = formatCompactQuota(
455
+ model.provider,
456
+ usageReports,
457
+ nowMs,
458
+ resolveActiveAdvisorAccount(model.provider, stats.advisors[0]?.sessionId),
459
+ );
460
+ if (quota) {
461
+ info += `\n${theme.bold("Quota")}\n`;
462
+ info += `${theme.fg("dim", quota)}\n`;
463
+ }
464
+ }
392
465
  info += `\n${theme.bold("Messages")}\n`;
393
466
  info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
394
467
  info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
@@ -406,14 +479,7 @@ export class CommandController {
406
479
  if (stats.tokens.cacheRead > 0) {
407
480
  info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
408
481
  }
409
- if (stats.tokens.cacheWrite > 0) {
410
- info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
411
- }
412
- info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
413
- if (stats.cost > 0) {
414
- info += `\n${theme.bold("Cost")}\n`;
415
- info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
416
- }
482
+ if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
417
483
  this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
418
484
  }
419
485
 
@@ -993,6 +1059,12 @@ export class CommandController {
993
1059
  return;
994
1060
  }
995
1061
  }
1062
+ try {
1063
+ await this.ctx.settings.flush();
1064
+ } catch (err) {
1065
+ this.ctx.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
1066
+ return;
1067
+ }
996
1068
 
997
1069
  try {
998
1070
  await this.ctx.sessionManager.moveTo(resolvedPath);
@@ -1000,7 +1072,6 @@ export class CommandController {
1000
1072
  this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
1001
1073
  return;
1002
1074
  }
1003
-
1004
1075
  await this.ctx.applyCwdChange(resolvedPath);
1005
1076
 
1006
1077
  this.ctx.updateEditorBorderColor();
@@ -1303,7 +1374,7 @@ export class CommandController {
1303
1374
  }
1304
1375
 
1305
1376
  const BAR_WIDTH_MAX = 24;
1306
- const BAR_WIDTH_MIN = 4;
1377
+ const COLUMN_WIDTH_MIN = 4;
1307
1378
 
1308
1379
  function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
1309
1380
  const duration = formatDuration(Math.max(0, now - job.startTime));
@@ -1537,6 +1608,57 @@ function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
1537
1608
  }
1538
1609
  return `resets in ${formatDuration(minReset)}`;
1539
1610
  }
1611
+ /**
1612
+ * Compact one-line quota summary for a single advisor's provider.
1613
+ * Returns `null` when the provider has no usage data.
1614
+ * When `activeAccount` is provided, only limits matching that credential
1615
+ * are shown (mirrors `renderUsageReports`'s account-stickiness filtering).
1616
+ * Example output: `Quota: 7d window · 67% used · resets in 3.2d`
1617
+ */
1618
+ export function formatCompactQuota(
1619
+ provider: string,
1620
+ reports: UsageReport[],
1621
+ nowMs: number,
1622
+ activeAccount?: OAuthAccountIdentity,
1623
+ ): string | null {
1624
+ const providerReports = reports.filter(r => r.provider === provider);
1625
+ if (providerReports.length === 0) return null;
1626
+ // Group limits by window id so we show BOTH the 5-hour and 7-day windows
1627
+ // (or any other distinct windows the provider exposes). Within each window,
1628
+ // pick the highest used fraction across accounts — that's the most pressing.
1629
+ const byWindow = new Map<string, { limit: UsageLimit; fraction: number }>();
1630
+ for (const report of providerReports) {
1631
+ for (const limit of report.limits) {
1632
+ // Skip limits that belong to a different credential than the one
1633
+ // the advisor is actually using, so we don't alarm the user with
1634
+ // an exhausted account that isn't theirs.
1635
+ if (activeAccount && !limitMatchesActiveAccount(report, limit, activeAccount)) continue;
1636
+ const fraction = resolveUsedFraction(limit);
1637
+ if (fraction === undefined) continue;
1638
+ const key = limit.window?.id ?? limit.scope.windowId ?? "—";
1639
+ const existing = byWindow.get(key);
1640
+ if (!existing || fraction > existing.fraction) byWindow.set(key, { limit, fraction });
1641
+ }
1642
+ }
1643
+ if (byWindow.size === 0) return null;
1644
+ // Sort windows by urgency (highest fraction first) so the most pressing
1645
+ // quota is always the first thing the user sees.
1646
+ const entries = [...byWindow.values()].sort((a, b) => b.fraction - a.fraction);
1647
+ const lines: string[] = [];
1648
+ for (const { limit, fraction } of entries) {
1649
+ const pct = Math.round(fraction * 100);
1650
+ const windowLabel = limit.window?.label ?? limit.scope.windowId ?? "—";
1651
+ // Include the limit label (account/tier) when it carries identity beyond
1652
+ // the window name, so the user can tell which credential's quota is shown.
1653
+ const identity = limit.label.trim();
1654
+ const header = identity && identity !== windowLabel ? `${windowLabel} (${identity})` : windowLabel;
1655
+ const parts = [`${header}: ${pct}% used`];
1656
+ const reset = resolveResetRange([limit], nowMs);
1657
+ if (reset) parts.push(reset);
1658
+ lines.push(parts.join(" · "));
1659
+ }
1660
+ return `Quota: ${lines.join(" │ ")}`;
1661
+ }
1540
1662
 
1541
1663
  function resolveStatusIcon(status: UsageLimit["status"], uiTheme: typeof theme): string {
1542
1664
  if (status === "exhausted") return uiTheme.fg("error", uiTheme.status.error);
@@ -1571,7 +1693,7 @@ function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: numb
1571
1693
  }
1572
1694
 
1573
1695
  /**
1574
- * Pick a per-column width so n bars + a trailing amount string fit in `available` columns.
1696
+ * Pick a per-account column width so the columns and trailing amount fit in `available`.
1575
1697
  * Falls back to the minimum when the terminal is too narrow rather than wrapping.
1576
1698
  */
1577
1699
  function resolveColumnWidth(count: number, available: number, trailing: number): number {
@@ -1580,10 +1702,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
1580
1702
  const gaps = count - 1;
1581
1703
  const spaceForBars = available - indent - gaps - (trailing > 0 ? trailing + 1 : 0);
1582
1704
  const ideal = Math.floor(spaceForBars / count);
1583
- const min = BAR_WIDTH_MIN;
1584
- const max = BAR_WIDTH_MAX;
1585
- if (ideal < min) return min;
1586
- if (ideal > max) return max;
1705
+ if (ideal < COLUMN_WIDTH_MIN) return COLUMN_WIDTH_MIN;
1587
1706
  return ideal;
1588
1707
  }
1589
1708
 
@@ -1642,7 +1761,7 @@ export function renderUsageReports(
1642
1761
  }
1643
1762
 
1644
1763
  lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
1645
- const activeAccountLabel = activeAccount?.email ?? activeAccount?.accountId ?? activeAccount?.projectId;
1764
+ const activeAccountLabel = formatActiveAccountLabel(activeAccount);
1646
1765
  if (activeAccountLabel) {
1647
1766
  lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
1648
1767
  }
@@ -1719,6 +1838,7 @@ export function renderUsageReports(
1719
1838
  const sectionCount = renderableGroups.reduce((max, g) => Math.max(max, g.sortedLimits.length), 0);
1720
1839
  const sectionTrailing = renderableGroups.reduce((max, g) => Math.max(max, visibleWidth(g.amountText)), 0);
1721
1840
  const sectionColumnWidth = resolveColumnWidth(sectionCount, availableWidth, sectionTrailing);
1841
+ const sectionBarWidth = Math.min(sectionColumnWidth, BAR_WIDTH_MAX);
1722
1842
 
1723
1843
  for (const { group, sortedLimits, sortedReports, amountText } of renderableGroups) {
1724
1844
  const status = resolveAggregateStatus(sortedLimits);
@@ -1736,7 +1856,7 @@ export function renderUsageReports(
1736
1856
  );
1737
1857
  lines.push(` ${accountLabels.join(" ")}`.trimEnd());
1738
1858
  const bars = sortedLimits.map(limit =>
1739
- padColumn(renderUsageBar(limit, uiTheme, sectionColumnWidth), sectionColumnWidth),
1859
+ padColumn(renderUsageBar(limit, uiTheme, sectionBarWidth), sectionColumnWidth),
1740
1860
  );
1741
1861
  lines.push(` ${bars.join(" ")} ${amountText}`.trimEnd());
1742
1862
  const resetText = sortedLimits.length <= 1 ? resolveResetRange(sortedLimits, nowMs) : null;
@@ -36,6 +36,7 @@ import {
36
36
  assistantUsageIsBilled,
37
37
  splitAssistantMessageToolTimeline,
38
38
  } from "../utils/transcript-render-helpers";
39
+ import { isWarpCliAgentProtocolActive } from "../warp-events";
39
40
  import { StreamingRevealController } from "./streaming-reveal";
40
41
  import { streamingStringKeysForTool, ToolArgsRevealController } from "./tool-args-reveal";
41
42
 
@@ -1187,6 +1188,7 @@ export class EventController {
1187
1188
  // final history — seal it instead of letting its spinner tick while idle.
1188
1189
  this.#resolveDisplaceablePoll();
1189
1190
  this.#resolveDisplaceableTodo();
1191
+ this.ctx.flushPendingCommandOutput();
1190
1192
  this.#lastAssistantComponent = undefined;
1191
1193
  this.ctx.ui.requestRender();
1192
1194
  this.#scheduleIdleCompaction();
@@ -1563,6 +1565,10 @@ export class EventController {
1563
1565
  const notify = settings.get("completion.notify");
1564
1566
  if (notify === "off") return;
1565
1567
 
1568
+ // Warp structured OSC 777 already drives native completion UX when the
1569
+ // protocol is negotiated — avoid a second legacy desktop/OSC-9 toast.
1570
+ if (isWarpCliAgentProtocolActive()) return;
1571
+
1566
1572
  // Skip when the turn was aborted (e.g. ask cancelled with Ctrl+C) or
1567
1573
  // errored — those are not "Task complete" events. Mirrors the gate
1568
1574
  // already used by #currentContextTokens, #handleMessageEnd, and the
@@ -21,7 +21,6 @@ import type {
21
21
  TerminalInputHandler,
22
22
  } from "../../extensibility/extensions";
23
23
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
24
- import { createExtensionModelQuery } from "../../extensibility/extensions/model-api";
25
24
  import { AskDialogComponent, boundPromptTitle } from "../../modes/components/ask-dialog";
26
25
  import { HookEditorComponent } from "../../modes/components/hook-editor";
27
26
  import { HookInputComponent } from "../../modes/components/hook-input";
@@ -494,32 +493,15 @@ export class ExtensionUiController {
494
493
  if (!uiContext) {
495
494
  return;
496
495
  }
497
- for (const registeredTool of this.ctx.session.extensionRunner?.getAllRegisteredTools() ?? []) {
496
+ const runner = this.ctx.session.extensionRunner;
497
+ for (const registeredTool of runner?.getAllRegisteredTools() ?? []) {
498
498
  if (registeredTool.definition.onSession) {
499
499
  try {
500
500
  await registeredTool.definition.onSession(event, {
501
+ ...runner!.createContext(),
501
502
  ui: uiContext,
502
- getContextUsage: () => this.ctx.session.getContextUsage(),
503
- compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
504
503
  hasUI: true,
505
- cwd: this.ctx.sessionManager.getCwd(),
506
- sessionManager: this.ctx.session.sessionManager,
507
- modelRegistry: this.ctx.session.modelRegistry,
508
- model: this.ctx.session.model,
509
- models: createExtensionModelQuery(
510
- this.ctx.session.modelRegistry,
511
- this.ctx.session.settings,
512
- () => this.ctx.session.model,
513
- ),
514
- isIdle: () => !this.ctx.session.isStreaming,
515
- hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
516
- abort: () => {
517
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
518
- },
519
- shutdown: () => {
520
- // Signal shutdown request
521
- },
522
- getSystemPrompt: () => this.ctx.session.systemPrompt,
504
+ compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
523
505
  });
524
506
  } catch (err) {
525
507
  this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
@@ -35,7 +35,6 @@ import { EnhancedPasteController } from "../../utils/enhanced-paste";
35
35
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
36
36
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
37
37
  import { resizeImage } from "../../utils/image-resize";
38
- import { generateSessionTitle } from "../../utils/title-generator";
39
38
 
40
39
  /**
41
40
  * Slash commands that may carry secrets in their arguments should never be
@@ -392,7 +391,16 @@ export class InputController {
392
391
  this.ctx.editor.onClear = () => this.handleCtrlC();
393
392
  this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
394
393
  this.ctx.editor.setActionKeys("app.display.reset", this.ctx.keybindings.getKeys("app.display.reset"));
395
- this.ctx.editor.onDisplayReset = () => this.ctx.ui.resetDisplay();
394
+ this.ctx.editor.onDisplayReset = () => {
395
+ // Explicit user gesture (Ctrl+L): re-query the terminal background once
396
+ // so a mid-session light/dark switch is picked up even on terminals
397
+ // without an end-to-end Mode 2031 notification path (#5352). The
398
+ // appearance callback re-evaluates the auto theme; the repaint below
399
+ // then renders the resolved palette. Bounded to one OSC 11 probe per
400
+ // gesture — no timers, no periodic polling.
401
+ this.ctx.ui.terminal.refreshAppearance?.();
402
+ this.ctx.ui.resetDisplay();
403
+ };
396
404
  this.ctx.editor.onExit = () => this.handleCtrlD();
397
405
  this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
398
406
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
@@ -821,16 +829,8 @@ export class InputController {
821
829
  // chance, so titling defers past "hi" instead of latching onto it.
822
830
  if (!this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE && !isLowSignalTitleInput(text)) {
823
831
  this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
824
- const registry = this.ctx.session.modelRegistry;
825
- generateSessionTitle(
826
- text,
827
- registry,
828
- this.ctx.settings,
829
- this.ctx.session.sessionId,
830
- this.ctx.session.model,
831
- provider => this.ctx.session.agent.metadataForProvider(provider),
832
- this.ctx.session.titleSystemPrompt,
833
- )
832
+ this.ctx.session
833
+ .generateTitle(text)
834
834
  .then(async title => {
835
835
  // Re-check: a concurrent attempt for an earlier message may have
836
836
  // already named the session. Don't clobber it. Terminal title and