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

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 (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -23,7 +23,7 @@ import { formatDefaultToolExecution } from "../../tools/default-renderer";
23
23
  import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
24
24
  import { isWaitingPollDetails } from "../../tools/hub";
25
25
  import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
26
- import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
26
+ import { type FirstResultViewportRepaint, type ToolRenderer, toolRenderers } from "../../tools/renderers";
27
27
  import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
28
28
  import type { XdevState } from "../../tools/xdev";
29
29
  import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
@@ -226,6 +226,8 @@ export interface ToolExecutionOptions {
226
226
  /** Session-persistent edit clipboard register, forked per preview frame. */
227
227
  clipboard?: Clipboard;
228
228
  showImages?: boolean; // default: true (only used if terminal supports images)
229
+ /** Allow the name-keyed renderer registry only when the active tool is the built-in implementation. */
230
+ useBuiltInRenderer?: boolean;
229
231
  editFuzzyThreshold?: number;
230
232
  editAllowFuzzy?: boolean;
231
233
  /** Live-region probe used to settle detached task progress once the block
@@ -312,6 +314,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
312
314
  // forcing the common image-free result to re-shape on every resize tick.
313
315
  #renderedImageCount = 0;
314
316
  #tool?: AgentTool;
317
+ #renderer?: ToolRenderer;
315
318
  #ui: ToolExecutionUi;
316
319
  #cwd: string;
317
320
  #result?: {
@@ -398,6 +401,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
398
401
  super();
399
402
  this.#toolName = toolName;
400
403
  this.#toolLabel = tool?.label ?? toolName;
404
+ this.#renderer = options.useBuiltInRenderer === false ? undefined : toolRenderers[toolName];
401
405
  this.#showImages = options.showImages ?? true;
402
406
  this.#editFuzzyThreshold = options.editFuzzyThreshold;
403
407
  this.#editAllowFuzzy = options.editAllowFuzzy;
@@ -418,10 +422,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
418
422
  this.#contentBox = new Box(0, 1);
419
423
  this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
420
424
 
421
- // Use Box for custom tools or built-in tools that have renderers
422
- const hasRenderer = toolName in toolRenderers;
425
+ // Use Box for custom tools or built-in tools with rich renderers.
423
426
  const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
424
- this.#usesContentBox = hasCustomRenderer || hasRenderer;
427
+ this.#usesContentBox = hasCustomRenderer || this.#renderer !== undefined;
425
428
  if (this.#usesContentBox) {
426
429
  this.addChild(this.#contentBox);
427
430
  } else {
@@ -668,12 +671,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
668
671
  const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
669
672
  const isBackgroundAsyncRunning =
670
673
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
671
- const renderer = toolRenderers[this.#toolName] as
672
- | {
673
- animatedPendingPreview?: boolean | ((args: unknown) => boolean);
674
- animatedPartialResult?: boolean | ((args: unknown) => boolean);
675
- }
676
- | undefined;
674
+ const renderer = this.#renderer;
677
675
  const pendingAnimation = renderer?.animatedPendingPreview;
678
676
  const partialAnimation = renderer?.animatedPartialResult;
679
677
  const pendingCallConsumesSpinner =
@@ -933,7 +931,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
933
931
 
934
932
  #rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
935
933
  const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
936
- const rendererValue = toolRenderers[this.#toolName]?.[name];
934
+ const rendererValue = this.#renderer?.[name];
937
935
  return toolValue === true || (toolValue === undefined && rendererValue === true);
938
936
  }
939
937
 
@@ -948,8 +946,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
948
946
  if (this.#result !== undefined) return false;
949
947
  const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
950
948
  ?.forceFirstResultViewportRepaint;
951
- const value =
952
- toolValue !== undefined ? toolValue : toolRenderers[this.#toolName]?.forceFirstResultViewportRepaint;
949
+ const value = toolValue !== undefined ? toolValue : this.#renderer?.forceFirstResultViewportRepaint;
953
950
  if (typeof value === "function") return value(this.#args, this.#renderState);
954
951
  return value === true;
955
952
  }
@@ -1101,9 +1098,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1101
1098
  const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
1102
1099
  this.#contentBox.setPaddingX(customFramed ? 0 : 1);
1103
1100
  this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
1104
- } else if (this.#toolName in toolRenderers) {
1105
- // Built-in tools with renderers
1106
- const renderer = toolRenderers[this.#toolName];
1101
+ } else if (this.#renderer) {
1102
+ // The active registry entry is a built-in tool with a rich renderer.
1103
+ const renderer = this.#renderer;
1107
1104
 
1108
1105
  // Clean up previous multi-file boxes
1109
1106
  for (const box of this.#multiFileBoxes) {
@@ -286,12 +286,20 @@ class TreeList implements Component {
286
286
 
287
287
  // Apply filter mode
288
288
  let passesFilter = true;
289
- // Entry types hidden in default view (settings/bookkeeping)
289
+ // Entry types hidden in default view (settings/bookkeeping). These carry
290
+ // no conversation content, so the tree only shows them in "all" mode.
290
291
  const isSettingsEntry =
291
292
  entry.type === "label" ||
292
293
  entry.type === "custom" ||
293
294
  entry.type === "model_change" ||
294
- entry.type === "thinking_level_change";
295
+ entry.type === "thinking_level_change" ||
296
+ entry.type === "service_tier_change" ||
297
+ entry.type === "title_change" ||
298
+ entry.type === "credential_pin" ||
299
+ entry.type === "session_init" ||
300
+ entry.type === "ttsr_injection" ||
301
+ entry.type === "mode_change" ||
302
+ entry.type === "reset_boundary";
295
303
 
296
304
  switch (this.#filterMode) {
297
305
  case "user-only":
@@ -390,6 +398,34 @@ class TreeList implements Component {
390
398
  case "label":
391
399
  parts.push("label", entry.label ?? "");
392
400
  break;
401
+ case "service_tier_change":
402
+ parts.push("service tier");
403
+ if (entry.serviceTier) {
404
+ const serviceTier = entry.serviceTier;
405
+ for (const family in serviceTier) {
406
+ const tier = serviceTier[family as keyof typeof serviceTier];
407
+ if (tier) parts.push(family, tier);
408
+ }
409
+ }
410
+ break;
411
+ case "title_change":
412
+ parts.push("title", entry.title);
413
+ break;
414
+ case "mode_change":
415
+ parts.push("mode", entry.mode);
416
+ break;
417
+ case "credential_pin":
418
+ parts.push("credential pin", entry.provider);
419
+ break;
420
+ case "ttsr_injection":
421
+ parts.push("ttsr injection", ...entry.injectedRules);
422
+ break;
423
+ case "reset_boundary":
424
+ parts.push("reset boundary");
425
+ break;
426
+ case "session_init":
427
+ parts.push("session init");
428
+ break;
393
429
  }
394
430
 
395
431
  return parts.join(" ");
@@ -658,8 +694,31 @@ class TreeList implements Component {
658
694
  case "label":
659
695
  result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
660
696
  break;
697
+ case "service_tier_change": {
698
+ // Per-family map, or null when the session went back to the default.
699
+ const tiers = entry.serviceTier
700
+ ? Object.entries(entry.serviceTier)
701
+ .map(([family, tier]) => `${family}:${tier}`)
702
+ .join(" ")
703
+ : "(default)";
704
+ result = theme.fg("dim", `[service tier: ${tiers}]`);
705
+ break;
706
+ }
707
+ case "title_change":
708
+ result = theme.fg("dim", `[title: ${normalize(entry.title)}]`);
709
+ break;
710
+ case "mode_change":
711
+ result = theme.fg("dim", `[mode: ${entry.mode}]`);
712
+ break;
713
+ case "credential_pin":
714
+ result = theme.fg("dim", `[credential pin: ${entry.provider}]`);
715
+ break;
661
716
  default:
662
- result = "";
717
+ // Bookkeeping entries with nothing worth spelling out still get their
718
+ // type. A row that renders to the empty string is worse than a
719
+ // useless one: it draws as a bare bullet with no way to tell what it
720
+ // is or why the tree has a gap in it.
721
+ result = theme.fg("dim", `[${entry.type.replaceAll("_", " ")}]`);
663
722
  }
664
723
 
665
724
  return isSelected ? theme.bold(result) : result;
@@ -11,7 +11,7 @@ import {
11
11
  type UsageReport,
12
12
  } from "@oh-my-pi/pi-ai";
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
14
- import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
14
+ import { formatDuration, logger, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
15
15
  import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
16
16
  import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
17
17
  import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
@@ -1436,9 +1436,15 @@ export class CommandController {
1436
1436
  }
1437
1437
  } catch (error) {
1438
1438
  const message = error instanceof Error ? error.message : String(error);
1439
- if (message === "Handoff cancelled" || (error instanceof Error && error.name === "AbortError")) {
1439
+ // `session.handoff()` normalizes genuine cancellations to this exact message; a
1440
+ // provider error (even one named AbortError) is re-thrown verbatim so it surfaces
1441
+ // as a real failure instead of a false "cancelled".
1442
+ if (message === "Handoff cancelled") {
1440
1443
  this.ctx.showError("Handoff cancelled");
1441
1444
  } else {
1445
+ // Persist the real failure so it is debuggable after the transient
1446
+ // TUI error clears (#7993).
1447
+ logger.error("Handoff failed", { error: message });
1442
1448
  this.ctx.showError(`Handoff failed: ${message}`);
1443
1449
  }
1444
1450
  } finally {
@@ -1073,6 +1073,7 @@ export class EventController {
1073
1073
  content.name,
1074
1074
  renderArgs,
1075
1075
  {
1076
+ useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
1076
1077
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
1077
1078
  clipboard: getEditClipboard(this.ctx.viewSession),
1078
1079
  showImages: settings.get("terminal.showImages"),
@@ -1322,6 +1323,7 @@ export class EventController {
1322
1323
  event.toolName,
1323
1324
  event.args,
1324
1325
  {
1326
+ useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(event.toolName),
1325
1327
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
1326
1328
  clipboard: getEditClipboard(this.ctx.viewSession),
1327
1329
  showImages: settings.get("terminal.showImages"),
@@ -1920,21 +1922,19 @@ export class EventController {
1920
1922
  this.ctx.retryLoader = undefined;
1921
1923
  this.ctx.statusContainer.disposeChildren();
1922
1924
  }
1923
- if (event.success) {
1924
- let appliedRecovered = false;
1925
- for (const recovered of event.recoveredErrors ?? []) {
1926
- const component = this.#takeRetrySupersededAssistantComponent(recovered.persistenceKey);
1927
- if (!component) continue;
1928
- component.applyRetryRecovery(recovered.retryRecovery);
1929
- if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
1930
- appliedRecovered = true;
1931
- }
1932
- if (appliedRecovered || (event.recoveredErrors?.length ?? 0) > 0) {
1933
- this.ctx.clearPinnedError();
1934
- }
1935
- this.#clearRetrySupersededAssistantComponents();
1936
- } else {
1937
- this.#clearRetrySupersededAssistantComponents();
1925
+ let appliedRetryUpdate = false;
1926
+ for (const retryError of event.retryErrors ?? []) {
1927
+ const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
1928
+ if (!component) continue;
1929
+ component.applyRetryRecovery(retryError.retryRecovery);
1930
+ if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
1931
+ appliedRetryUpdate = true;
1932
+ }
1933
+ if (appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0) {
1934
+ this.ctx.clearPinnedError();
1935
+ }
1936
+ this.#clearRetrySupersededAssistantComponents();
1937
+ if (!event.success) {
1938
1938
  this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
1939
1939
  }
1940
1940
  this.#ensureWorkingLoaderWhileStreaming();
@@ -13,6 +13,7 @@ import { renderSegmentTrack } from "../../modes/components/segment-track";
13
13
  import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
14
14
  import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
15
15
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
16
+ import { TreeSelectorComponent } from "../../modes/components/tree-selector";
16
17
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
17
18
  import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
18
19
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
@@ -177,6 +178,7 @@ export class InputController {
177
178
  #focusedPasteListenerInstalled = false;
178
179
  #btwBranchListenerInstalled = false;
179
180
  #btwCopyListenerInstalled = false;
181
+ #expandToolsListenerInstalled = false;
180
182
  // Tap counter for the double-← gesture; reset whenever a quiet gap
181
183
  // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
182
184
  // #detectLeftDoubleTap.
@@ -278,6 +280,24 @@ export class InputController {
278
280
  return { consume: true };
279
281
  });
280
282
  }
283
+ if (!this.#expandToolsListenerInstalled) {
284
+ this.#expandToolsListenerInstalled = true;
285
+ // `app.tools.expand` (Ctrl+O) toggles the transcript's tool-output
286
+ // preview. It must fire regardless of focus so a truncated edit stays
287
+ // expandable while an approval prompt / select dialog holds keyboard
288
+ // focus (#7837). Defers when the main transcript is not the active
289
+ // surface (a fullscreen/anchored overlay — agent hub, transcript
290
+ // viewer, log viewer, model picker) or when the focused component
291
+ // rebinds Ctrl+O for its own use (the tree selector's filter cycle).
292
+ this.ctx.ui.addInputListener(data => {
293
+ if (!this.ctx.keybindings.matches(data, "app.tools.expand")) return undefined;
294
+ if (this.ctx.ui.hasOverlay()) return undefined;
295
+ if (this.ctx.ui.getFocused() instanceof TreeSelectorComponent && matchesKey(data, "ctrl+o"))
296
+ return undefined;
297
+ this.toggleToolOutputExpansion();
298
+ return { consume: true };
299
+ });
300
+ }
281
301
  this.ctx.editor.onEscape = () => {
282
302
  // Side-channel panels are the topmost view. Esc dismisses them before
283
303
  // touching loop mode, maintenance, or the underlying main turn.
@@ -456,8 +476,6 @@ export class InputController {
456
476
  this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
457
477
  );
458
478
  this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
459
- this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
460
- this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
461
479
  this.ctx.editor.setActionKeys(
462
480
  "app.tools.toggleVisibility",
463
481
  this.ctx.keybindings.getKeys("app.tools.toggleVisibility"),
@@ -2026,6 +2026,7 @@ export class SelectorController {
2026
2026
  remote: this.ctx.collabGuest?.hubRemote,
2027
2027
  ui: this.ctx.ui,
2028
2028
  getTool: name => this.ctx.session.getToolByName(name),
2029
+ isBuiltInTool: name => this.ctx.session.hasBuiltInTool(name),
2029
2030
  getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
2030
2031
  cwd: this.ctx.sessionManager.getCwd(),
2031
2032
  hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
@@ -0,0 +1,133 @@
1
+ import type { ColorMode, ColorValue } from "./schema";
2
+
3
+ // ============================================================================
4
+ // Color Utilities
5
+ // ============================================================================
6
+
7
+ export function detectColorMode(): ColorMode {
8
+ const colorterm = Bun.env.COLORTERM;
9
+ if (colorterm === "truecolor" || colorterm === "24bit") {
10
+ return "truecolor";
11
+ }
12
+ // Windows Terminal supports truecolor
13
+ if (Bun.env.WT_SESSION) {
14
+ return "truecolor";
15
+ }
16
+ const term = Bun.env.TERM || "";
17
+ // Only fall back to 256color for truly limited terminals
18
+ if (term === "dumb" || term === "" || term === "linux") {
19
+ return "256color";
20
+ }
21
+ // Assume truecolor for everything else - virtually all modern terminals support it
22
+ return "truecolor";
23
+ }
24
+
25
+ export function colorToAnsi(color: string, mode: ColorMode): string {
26
+ const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
27
+ const ansi = Bun.color(color, format);
28
+ if (ansi === null) {
29
+ throw new Error(`Invalid color value: ${color}`);
30
+ }
31
+ return ansi;
32
+ }
33
+
34
+ export function fgAnsi(color: string | number, mode: ColorMode): string {
35
+ if (color === "") return "\x1b[39m";
36
+ if (typeof color === "number") return `\x1b[38;5;${color}m`;
37
+ if (typeof color === "string") {
38
+ return colorToAnsi(color, mode);
39
+ }
40
+ throw new Error(`Invalid color value: ${color}`);
41
+ }
42
+
43
+ export function bgAnsi(color: string | number, mode: ColorMode): string {
44
+ if (color === "") return "\x1b[49m";
45
+ if (typeof color === "number") return `\x1b[48;5;${color}m`;
46
+ const ansi = colorToAnsi(color, mode);
47
+ return ansi.replace("\x1b[38;", "\x1b[48;");
48
+ }
49
+
50
+ export function resolveVarRefs(
51
+ value: ColorValue,
52
+ vars: Record<string, ColorValue>,
53
+ visited = new Set<string>(),
54
+ ): string | number {
55
+ if (typeof value === "number" || value === "" || value.startsWith("#")) {
56
+ return value;
57
+ }
58
+ if (visited.has(value)) {
59
+ throw new Error(`Circular variable reference detected: ${value}`);
60
+ }
61
+ if (!(value in vars)) {
62
+ throw new Error(`Variable reference not found: ${value}`);
63
+ }
64
+ visited.add(value);
65
+ return resolveVarRefs(vars[value], vars, visited);
66
+ }
67
+
68
+ export function resolveThemeColors<T extends Record<string, ColorValue>>(
69
+ colors: T,
70
+ vars: Record<string, ColorValue> = {},
71
+ ): Record<keyof T, string | number> {
72
+ const resolved: Record<string, string | number> = {};
73
+ for (const [key, value] of Object.entries(colors)) {
74
+ resolved[key] = resolveVarRefs(value, vars);
75
+ }
76
+ return resolved as Record<keyof T, string | number>;
77
+ }
78
+
79
+ /**
80
+ * Resolve a theme color value (hex string or 256-color index) to a CSS hex string.
81
+ * Empty string represents the default terminal color.
82
+ */
83
+ export function resolveToHex(value: string | number, isLight: boolean): string {
84
+ if (typeof value === "number") return ansi256ToHex(value);
85
+ if (value === "") return isLight ? "#000000" : "#e5e5e7";
86
+ return value;
87
+ }
88
+
89
+ /**
90
+ * Convert a 256-color index to hex string.
91
+ * Indices 0-15: basic colors (approximate)
92
+ * Indices 16-231: 6x6x6 color cube
93
+ * Indices 232-255: grayscale ramp
94
+ */
95
+ export function ansi256ToHex(index: number): string {
96
+ // Basic colors (0-15) - approximate common terminal values
97
+ const basicColors = [
98
+ "#000000",
99
+ "#800000",
100
+ "#008000",
101
+ "#808000",
102
+ "#000080",
103
+ "#800080",
104
+ "#008080",
105
+ "#c0c0c0",
106
+ "#808080",
107
+ "#ff0000",
108
+ "#00ff00",
109
+ "#ffff00",
110
+ "#0000ff",
111
+ "#ff00ff",
112
+ "#00ffff",
113
+ "#ffffff",
114
+ ];
115
+ if (index < 16) {
116
+ return basicColors[index];
117
+ }
118
+
119
+ // Color cube (16-231): 6x6x6 = 216 colors
120
+ if (index < 232) {
121
+ const cubeIndex = index - 16;
122
+ const r = Math.floor(cubeIndex / 36);
123
+ const g = Math.floor((cubeIndex % 36) / 6);
124
+ const b = cubeIndex % 6;
125
+ const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0");
126
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
127
+ }
128
+
129
+ // Grayscale (232-255): 24 shades
130
+ const gray = 8 + (index - 232) * 10;
131
+ const grayHex = gray.toString(16).padStart(2, "0");
132
+ return `#${grayHex}${grayHex}${grayHex}`;
133
+ }
@@ -0,0 +1,178 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { type } from "@oh-my-pi/omptype";
4
+ import { adjustHsv, getCustomThemesDir, isEnoent } from "@oh-my-pi/pi-utils";
5
+ import { detectColorMode, resolveThemeColors } from "./color";
6
+ import darkThemeJson from "./dark.json" with { type: "json" };
7
+ import { defaultThemes } from "./defaults";
8
+ import lightThemeJson from "./light.json" with { type: "json" };
9
+ import { type ColorMode, type ThemeBg, type ThemeColor, type ThemeJson, themeJsonSchema } from "./schema";
10
+ import { normalizeSpinnerFramesOverride, type SymbolPreset } from "./symbols";
11
+ import { Theme } from "./theme-class";
12
+
13
+ // ============================================================================
14
+ // Theme Loading
15
+ // ============================================================================
16
+
17
+ const BUILTIN_THEMES: Record<string, ThemeJson> = {
18
+ dark: darkThemeJson as ThemeJson,
19
+ light: lightThemeJson as ThemeJson,
20
+ ...(defaultThemes as Record<string, ThemeJson>),
21
+ };
22
+
23
+ export function getBuiltinThemes(): Record<string, ThemeJson> {
24
+ return BUILTIN_THEMES;
25
+ }
26
+
27
+ export async function getAvailableThemes(): Promise<string[]> {
28
+ const themes = new Set<string>(Object.keys(getBuiltinThemes()));
29
+ const customThemesDir = getCustomThemesDir();
30
+ try {
31
+ const files = await fs.promises.readdir(customThemesDir);
32
+ for (const file of files) {
33
+ if (file.endsWith(".json")) {
34
+ themes.add(file.slice(0, -5));
35
+ }
36
+ }
37
+ } catch {
38
+ // Directory doesn't exist or isn't readable
39
+ }
40
+ return Array.from(themes).sort();
41
+ }
42
+
43
+ export interface ThemeInfo {
44
+ name: string;
45
+ path: string | undefined;
46
+ }
47
+
48
+ export async function getAvailableThemesWithPaths(): Promise<ThemeInfo[]> {
49
+ const result: ThemeInfo[] = [];
50
+
51
+ // Built-in themes (embedded, no file path)
52
+ for (const name of Object.keys(getBuiltinThemes())) {
53
+ result.push({ name, path: undefined });
54
+ }
55
+
56
+ // Custom themes
57
+ const customThemesDir = getCustomThemesDir();
58
+ try {
59
+ const files = await fs.promises.readdir(customThemesDir);
60
+ for (const file of files) {
61
+ if (file.endsWith(".json")) {
62
+ const name = file.slice(0, -5);
63
+ if (!result.some(themeInfo => themeInfo.name === name)) {
64
+ result.push({ name, path: path.join(customThemesDir, file) });
65
+ }
66
+ }
67
+ }
68
+ } catch {
69
+ // Directory doesn't exist or isn't readable
70
+ }
71
+
72
+ return result.sort((a, b) => a.name.localeCompare(b.name));
73
+ }
74
+
75
+ export async function loadThemeJson(name: string): Promise<ThemeJson> {
76
+ const builtinThemes = getBuiltinThemes();
77
+ if (name in builtinThemes) {
78
+ return builtinThemes[name];
79
+ }
80
+ const customThemesDir = getCustomThemesDir();
81
+ const themePath = path.join(customThemesDir, `${name}.json`);
82
+ let content: string;
83
+ try {
84
+ content = await Bun.file(themePath).text();
85
+ } catch (err) {
86
+ if (isEnoent(err)) throw new Error(`Theme not found: ${name}`);
87
+ throw err;
88
+ }
89
+ let json: unknown;
90
+ try {
91
+ json = JSON.parse(content);
92
+ } catch (error) {
93
+ throw new Error(`Failed to parse theme ${name}: ${error}`);
94
+ }
95
+ let parsed: ThemeJson;
96
+ try {
97
+ parsed = themeJsonSchema(json) as ThemeJson;
98
+ if (parsed instanceof type.errors) {
99
+ throw new Error(parsed.summary);
100
+ }
101
+ } catch (error) {
102
+ const errorMessage = error instanceof Error ? error.message : String(error);
103
+ // Extract color key information if available
104
+ const missingColorMatch = errorMessage.match(/missing keys: (.+)/i);
105
+ const missingColors: string[] = missingColorMatch ? missingColorMatch[1].split(",").map(s => s.trim()) : [];
106
+
107
+ let fullErrorMessage = `Invalid theme "${name}":\n`;
108
+ if (missingColors.length > 0) {
109
+ fullErrorMessage += `\nMissing required color tokens:\n`;
110
+ fullErrorMessage += missingColors.map(c => ` - ${c}`).join("\n");
111
+ fullErrorMessage += `\n\nPlease add these colors to your theme's "colors" object.`;
112
+ fullErrorMessage += `\nSee the built-in themes (dark.json, light.json) for reference values.`;
113
+ }
114
+ fullErrorMessage += `\n\nValidation error:\n - ${errorMessage}`;
115
+
116
+ throw new Error(fullErrorMessage);
117
+ }
118
+ return parsed;
119
+ }
120
+
121
+ export interface CreateThemeOptions {
122
+ mode?: ColorMode;
123
+ symbolPresetOverride?: SymbolPreset;
124
+ colorBlindMode?: boolean;
125
+ }
126
+
127
+ /** HSV adjustment to shift green toward blue for colorblind mode (red-green colorblindness) */
128
+ const COLORBLIND_ADJUSTMENT = { h: 60, s: 0.71 };
129
+
130
+ export function createTheme(themeJson: ThemeJson, options: CreateThemeOptions = {}): Theme {
131
+ const { mode, symbolPresetOverride, colorBlindMode } = options;
132
+ const colorMode = mode ?? detectColorMode();
133
+ const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
134
+
135
+ if (colorBlindMode) {
136
+ const added = resolvedColors.toolDiffAdded;
137
+ if (typeof added === "string" && added.startsWith("#")) {
138
+ resolvedColors.toolDiffAdded = adjustHsv(added, COLORBLIND_ADJUSTMENT);
139
+ }
140
+ }
141
+
142
+ const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
143
+ const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
144
+ const bgColorKeys: Set<string> = new Set([
145
+ "selectedBg",
146
+ "userMessageBg",
147
+ "customMessageBg",
148
+ "toolPendingBg",
149
+ "toolSuccessBg",
150
+ "toolErrorBg",
151
+ "statusLineBg",
152
+ ]);
153
+ for (const [key, value] of Object.entries(resolvedColors)) {
154
+ if (bgColorKeys.has(key)) {
155
+ bgColors[key as ThemeBg] = value;
156
+ } else {
157
+ fgColors[key as ThemeColor] = value;
158
+ }
159
+ }
160
+ // Extract symbol configuration - settings override takes precedence over theme
161
+ const symbolPreset: SymbolPreset = symbolPresetOverride ?? themeJson.symbols?.preset ?? "unicode";
162
+ const symbolOverrides = themeJson.symbols?.overrides ?? {};
163
+ const spinnerFramesOverrides = normalizeSpinnerFramesOverride(themeJson.symbols?.spinnerFrames);
164
+ return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides, spinnerFramesOverrides);
165
+ }
166
+
167
+ export async function loadTheme(name: string, options: CreateThemeOptions = {}): Promise<Theme> {
168
+ const themeJson = await loadThemeJson(name);
169
+ return createTheme(themeJson, options);
170
+ }
171
+
172
+ export async function getThemeByName(name: string): Promise<Theme | undefined> {
173
+ try {
174
+ return await loadTheme(name);
175
+ } catch {
176
+ return undefined;
177
+ }
178
+ }