@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -15,8 +15,10 @@ import type {
15
15
  CustomToolResult,
16
16
  RenderResultOptions,
17
17
  } from "../extensibility/custom-tools/types";
18
+ import { resolveLocalUrlToFile } from "../internal-urls/local-protocol";
18
19
  import type { Theme } from "../modes/theme/theme";
19
20
  import type { OutputMeta } from "../tools/output-meta";
21
+ import { normalizeLocalScheme } from "../tools/path-utils";
20
22
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
21
23
  import { callTool } from "./client";
22
24
  import { renderMCPCall, renderMCPResult } from "./render";
@@ -104,13 +106,62 @@ function stripHarnessIntent(args: MCPToolArgs, inputSchema: MCPToolDefinition["i
104
106
  return rest;
105
107
  }
106
108
 
109
+ async function resolveOutboundLocalUrlArgs(
110
+ value: unknown,
111
+ context: CustomToolContext,
112
+ seen: WeakSet<object> = new WeakSet(),
113
+ ): Promise<unknown> {
114
+ if (typeof value === "string") {
115
+ const normalized = normalizeLocalScheme(value);
116
+ if (!normalized.startsWith("local://")) return value;
117
+ const localFile = await resolveLocalUrlToFile(normalized, {
118
+ cwd: context.sessionManager?.getCwd?.(),
119
+ settings: context.settings,
120
+ localProtocolOptions: context.localProtocolOptions,
121
+ });
122
+ return localFile?.path ?? value;
123
+ }
124
+ if (typeof value !== "object" || value === null) return value;
125
+ if (seen.has(value)) return value;
126
+ seen.add(value);
127
+
128
+ if (Array.isArray(value)) {
129
+ let resolved: unknown[] | undefined;
130
+ for (let index = 0; index < value.length; index++) {
131
+ const item = value[index];
132
+ const next = await resolveOutboundLocalUrlArgs(item, context, seen);
133
+ if (next === item && !resolved) continue;
134
+ resolved ??= value.slice();
135
+ resolved[index] = next;
136
+ }
137
+ return resolved ?? value;
138
+ }
139
+
140
+ const input = value as Record<string, unknown>;
141
+ let resolved: Record<string, unknown> | undefined;
142
+ for (const key in input) {
143
+ const item = input[key];
144
+ const next = await resolveOutboundLocalUrlArgs(item, context, seen);
145
+ if (next === item && !resolved) continue;
146
+ resolved ??= { ...input };
147
+ resolved[key] = next;
148
+ }
149
+ return resolved ?? value;
150
+ }
151
+
107
152
  /**
108
153
  * Normalize raw tool params into the outbound `tools/call` arguments: strip
109
- * the harness intent field, then drop optional empty placeholders the server
110
- * declares but doesn't require.
154
+ * the harness intent field, drop optional empty placeholders the server
155
+ * declares but doesn't require, then translate session-local files to paths
156
+ * external MCP servers can read.
111
157
  */
112
- function prepareOutboundArgs(params: unknown, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
113
- return omitUnusedOptionalArgs(stripHarnessIntent(normalizeToolArgs(params), inputSchema), inputSchema);
158
+ async function prepareOutboundArgs(
159
+ params: unknown,
160
+ inputSchema: MCPToolDefinition["inputSchema"],
161
+ context: CustomToolContext,
162
+ ): Promise<MCPToolArgs> {
163
+ const args = omitUnusedOptionalArgs(stripHarnessIntent(normalizeToolArgs(params), inputSchema), inputSchema);
164
+ return (await resolveOutboundLocalUrlArgs(args, context)) as MCPToolArgs;
114
165
  }
115
166
 
116
167
  /** Details included in MCP tool results for rendering */
@@ -316,7 +367,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
316
367
  signal?: AbortSignal,
317
368
  ): Promise<CustomToolResult<MCPToolDetails>> {
318
369
  throwIfAborted(signal);
319
- const args = prepareOutboundArgs(params, this.tool.inputSchema);
370
+ const args = await prepareOutboundArgs(params, this.tool.inputSchema, _ctx);
320
371
  const provider = this.connection._source?.provider;
321
372
  const providerName = this.connection._source?.providerName;
322
373
 
@@ -415,7 +466,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
415
466
  signal?: AbortSignal,
416
467
  ): Promise<CustomToolResult<MCPToolDetails>> {
417
468
  throwIfAborted(signal);
418
- const args = prepareOutboundArgs(params, this.tool.inputSchema);
469
+ const args = await prepareOutboundArgs(params, this.tool.inputSchema, _ctx);
419
470
  const provider = this.#fallbackProvider;
420
471
  const providerName = this.#fallbackProviderName;
421
472
 
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { getThemeByName, setThemeInstance, theme } from "../../theme/theme";
6
+ import { DynamicBorder } from "../dynamic-border";
7
+
8
+ const componentPath = path.resolve(import.meta.dir, "../dynamic-border.ts");
9
+
10
+ describe("DynamicBorder", () => {
11
+ // Regression for #5366: extensions importing legacy pi UI components get a
12
+ // second `src` module graph whose module-level `theme` is never assigned by
13
+ // host startup. `render()` must degrade to plain glyphs instead of throwing
14
+ // "undefined is not an object (evaluating 'theme.boxRound')" and killing the
15
+ // TUI. Other test files in the same process initialize the theme module, so
16
+ // the uninitialized state is only reachable in a fresh module graph — a
17
+ // subprocess reproduces the actual reported scenario deterministically.
18
+ it("renders plain glyphs when the module-level theme is uninitialized", async () => {
19
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-dynamic-border-"));
20
+ try {
21
+ const script = [
22
+ `import { DynamicBorder } from ${JSON.stringify(componentPath)};`,
23
+ 'const custom = new DynamicBorder(str => "<" + str + ">");',
24
+ `if (JSON.stringify(custom.render(4)) !== JSON.stringify(["<────>"])) process.exit(1);`,
25
+ "const plain = new DynamicBorder();",
26
+ `if (JSON.stringify(plain.render(3)) !== JSON.stringify(["───"])) process.exit(2);`,
27
+ "",
28
+ ].join("\n");
29
+ const file = path.join(dir, "uninitialized-theme.ts");
30
+ await Bun.write(file, script);
31
+ const proc = Bun.spawn(["bun", file], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
32
+ const [exitCode, stderr] = await Promise.all([
33
+ proc.exited,
34
+ new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
35
+ ]);
36
+ expect(stderr).toBe("");
37
+ expect(exitCode).toBe(0);
38
+ } finally {
39
+ await fs.rm(dir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ it("paints with theme.boxRound.horizontal once the theme is initialized", async () => {
44
+ const previous = theme;
45
+ const loaded = await getThemeByName("dark");
46
+ if (!loaded) throw new Error("theme unavailable");
47
+ setThemeInstance(loaded);
48
+ try {
49
+ const border = new DynamicBorder(str => `<${str}>`);
50
+ expect(border.render(3)).toEqual([`<${loaded.boxRound.horizontal.repeat(3)}>`]);
51
+ } finally {
52
+ if (previous) setThemeInstance(previous);
53
+ }
54
+ });
55
+ });
@@ -1142,11 +1142,11 @@ export class AgentDashboard extends Container {
1142
1142
  return;
1143
1143
  }
1144
1144
 
1145
- if (matchesSelectUp(data) || data === "k") {
1145
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
1146
1146
  this.#moveSelection(-1);
1147
1147
  return;
1148
1148
  }
1149
- if (matchesSelectDown(data) || data === "j") {
1149
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
1150
1150
  this.#moveSelection(1);
1151
1151
  return;
1152
1152
  }
@@ -340,6 +340,19 @@ export class AgentHubOverlayComponent extends Container {
340
340
  this.#handleTableInput(keyData);
341
341
  }
342
342
 
343
+ /**
344
+ * Seed the table's left-left close detector with the current time so a single
345
+ * subsequent `←` (within {@link LEFT_TAP_WINDOW_MS}) dismisses the hub.
346
+ *
347
+ * The editor's own double-tap detector consumes the `←←` that opens the hub,
348
+ * leaving this detector at its fresh `0` — without this handoff the user would
349
+ * have to press `←←` a second time to escape. Called by the opener when the hub
350
+ * was raised by that gesture.
351
+ */
352
+ armCloseTap(): void {
353
+ this.#lastLeftTap = Date.now();
354
+ }
355
+
343
356
  /**
344
357
  * Open the fullscreen transcript viewer for an agent id (public for table Enter
345
358
  * and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
@@ -573,14 +586,14 @@ export class AgentHubOverlayComponent extends Container {
573
586
  }
574
587
  return;
575
588
  }
576
- if (keyData === "j" || matchesSelectDown(keyData)) {
589
+ if (matchesKey(keyData, "j") || matchesSelectDown(keyData)) {
577
590
  if (this.#rows.length > 0) {
578
591
  this.#selectedRow = Math.min(this.#selectedRow + 1, this.#rows.length - 1);
579
592
  }
580
593
  this.#requestRender();
581
594
  return;
582
595
  }
583
- if (keyData === "k" || matchesSelectUp(keyData)) {
596
+ if (matchesKey(keyData, "k") || matchesSelectUp(keyData)) {
584
597
  if (this.#rows.length > 0) {
585
598
  this.#selectedRow = Math.max(this.#selectedRow - 1, 0);
586
599
  }
@@ -496,9 +496,9 @@ export class AgentTranscriptViewer implements Component {
496
496
  this.deps.requestRender();
497
497
  return true;
498
498
  }
499
- if (data === "j" || matchesSelectDown(data)) {
499
+ if (matchesKey(data, "j") || matchesSelectDown(data)) {
500
500
  this.#scrollView.scroll(1);
501
- } else if (data === "k" || matchesSelectUp(data)) {
501
+ } else if (matchesKey(data, "k") || matchesSelectUp(data)) {
502
502
  this.#scrollView.scroll(-1);
503
503
  } else if (data === "g") {
504
504
  this.#scrollView.scrollToTop();
@@ -35,6 +35,7 @@ import {
35
35
  buildIrcMessageCard,
36
36
  normalizeToolArgs,
37
37
  resolveAssistantErrorPresentation,
38
+ splitAssistantMessageToolTimeline,
38
39
  } from "../utils/transcript-render-helpers";
39
40
  import { createAdvisorMessageCard } from "./advisor-message";
40
41
  import { AssistantMessageComponent } from "./assistant-message";
@@ -273,12 +274,13 @@ export class ChatTranscriptBuilder {
273
274
  #appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
274
275
  const hideThinkingBlock = this.deps.hideThinkingBlock?.() ?? false;
275
276
  const proseOnlyThinking = this.deps.proseOnlyThinking ? this.deps.proseOnlyThinking() : true;
277
+ const timeline = splitAssistantMessageToolTimeline(message);
276
278
  const assistantComponent = new AssistantMessageComponent(
277
- message,
279
+ timeline.beforeTools,
278
280
  hideThinkingBlock,
279
281
  () => this.deps.requestRender(),
280
282
  this.deps.getMessageRenderer ? undefined : [], // placeholder for thinkingRenderers
281
- undefined, // placeholder for imageBudget
283
+ this.deps.ui.imageBudget,
282
284
  proseOnlyThinking,
283
285
  );
284
286
  this.container.addChild(assistantComponent);
@@ -301,11 +303,24 @@ export class ChatTranscriptBuilder {
301
303
  const errorPresentation = resolveAssistantErrorPresentation(message);
302
304
  const hasErrorStop = errorPresentation.kind === "full";
303
305
  const errorMessage = hasErrorStop ? errorPresentation.text : null;
306
+ const appendAssistantSegment = (segment: Extract<AgentMessage, { role: "assistant" }> | undefined) => {
307
+ if (!segment || !assistantHasVisibleContent(segment)) return;
308
+ const component = new AssistantMessageComponent(
309
+ segment,
310
+ hideThinkingBlock,
311
+ () => this.deps.requestRender(),
312
+ this.deps.getMessageRenderer ? undefined : [],
313
+ undefined,
314
+ proseOnlyThinking,
315
+ );
316
+ this.container.addChild(component);
317
+ };
304
318
 
305
319
  for (const content of message.content) {
306
320
  if (content.type !== "toolCall") continue;
307
321
  this.#resolveWaitingPoll(content.name);
308
322
 
323
+ const afterToolSegment = timeline.afterToolCalls.get(content.id);
309
324
  if (
310
325
  content.name === "read" &&
311
326
  readArgsHaveTarget(content.arguments) &&
@@ -319,10 +334,15 @@ export class ChatTranscriptBuilder {
319
334
  false,
320
335
  content.id,
321
336
  );
337
+ } else if (afterToolSegment) {
338
+ const group = this.#ensureReadGroup();
339
+ group.updateArgs(content.arguments, content.id);
340
+ this.#pendingTools.set(content.id, group);
322
341
  } else {
323
342
  const normalizedArgs = normalizeToolArgs(content.arguments);
324
343
  this.#readArgs.set(content.id, normalizedArgs);
325
344
  }
345
+ appendAssistantSegment(afterToolSegment);
326
346
  continue;
327
347
  }
328
348
 
@@ -332,8 +352,9 @@ export class ChatTranscriptBuilder {
332
352
  content.name,
333
353
  content.arguments,
334
354
  {
335
- // Images can't be sliced through the scroll viewport; keep them off.
336
- showImages: false,
355
+ // Stable ids and Kitty placeholder cells keep images anchored
356
+ // while the transcript viewport scrolls and reflows.
357
+ showImages: settings.get("terminal.showImages"),
337
358
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
338
359
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
339
360
  liveRegion: this.container,
@@ -355,6 +376,7 @@ export class ChatTranscriptBuilder {
355
376
  } else {
356
377
  this.#pendingTools.set(content.id, component);
357
378
  }
379
+ appendAssistantSegment(afterToolSegment);
358
380
  }
359
381
 
360
382
  this.#pendingUsage =
@@ -1,19 +1,21 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
- import { theme } from "../../modes/theme/theme";
2
+ import { fgOrPlain, theme } from "../../modes/theme/theme";
3
3
 
4
4
  /**
5
5
  * Dynamic border component that adjusts to viewport width.
6
6
  *
7
- * Note: When used from hooks loaded via jiti, the global `theme` may be undefined
8
- * because jiti creates a separate module cache. Always pass an explicit color
9
- * function when using DynamicBorder in components exported for hook use.
7
+ * Note: the module-level `theme` may be `undefined` — when loaded through jiti
8
+ * (separate module cache) or from a second `src` module graph in npm-package
9
+ * installs, where the host bundle assigns `theme` but this copy never sees it
10
+ * (issue #5366). Both the default color and `render()` degrade to plain,
11
+ * unstyled output instead of crashing the TUI.
10
12
  */
11
13
  export class DynamicBorder implements Component {
12
14
  #color: (str: string) => string;
13
15
  #cachedWidth = -1;
14
16
  #cachedLines: string[] | undefined;
15
17
 
16
- constructor(color: (str: string) => string = str => theme.fg("border", str)) {
18
+ constructor(color: (str: string) => string = str => fgOrPlain("border", str)) {
17
19
  this.#color = color;
18
20
  }
19
21
 
@@ -26,7 +28,8 @@ export class DynamicBorder implements Component {
26
28
  if (this.#cachedLines && this.#cachedWidth === width) {
27
29
  return this.#cachedLines;
28
30
  }
29
- const lines = [this.#color(theme.boxRound.horizontal.repeat(Math.max(1, width)))];
31
+ const horizontal = typeof theme === "undefined" ? "─" : theme.boxRound.horizontal;
32
+ const lines = [this.#color(horizontal.repeat(Math.max(1, width)))];
30
33
  this.#cachedWidth = width;
31
34
  this.#cachedLines = lines;
32
35
  return lines;
@@ -449,12 +449,12 @@ export class ExtensionList implements Component {
449
449
 
450
450
  handleInput(data: string): void {
451
451
  // Navigation
452
- if (matchesSelectUp(data) || data === "k") {
452
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
453
453
  this.#moveSelectionUp();
454
454
  return;
455
455
  }
456
456
 
457
- if (matchesSelectDown(data) || data === "j") {
457
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
458
458
  this.#moveSelectionDown();
459
459
  return;
460
460
  }
@@ -652,17 +652,23 @@ export class HookSelectorComponent extends Container {
652
652
  return;
653
653
  }
654
654
 
655
- if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && keyData === "k")) {
655
+ if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "k"))) {
656
656
  this.#moveSelection(-1);
657
- } else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && keyData === "j")) {
657
+ } else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "j"))) {
658
658
  this.#moveSelection(1);
659
659
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
660
660
  const selected = this.#filteredOptions[this.#selectedIndex];
661
661
  if (selected && !this.#isDisabled(selected.index)) this.#onSelectCallback(selected.option.label);
662
- } else if (matchesKey(keyData, "left") || (this.#slider && !this.#isSearchEnabled() && keyData === "h")) {
662
+ } else if (
663
+ matchesKey(keyData, "left") ||
664
+ (this.#slider && !this.#isSearchEnabled() && matchesKey(keyData, "h"))
665
+ ) {
663
666
  if (this.#slider) this.#moveSlider(-1);
664
667
  else this.#onLeftCallback?.();
665
- } else if (matchesKey(keyData, "right") || (this.#slider && !this.#isSearchEnabled() && keyData === "l")) {
668
+ } else if (
669
+ matchesKey(keyData, "right") ||
670
+ (this.#slider && !this.#isSearchEnabled() && matchesKey(keyData, "l"))
671
+ ) {
666
672
  if (this.#slider) this.#moveSlider(1);
667
673
  else this.#onRightCallback?.();
668
674
  } else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
@@ -164,6 +164,11 @@ export class LoginDialogComponent extends Container {
164
164
  this.#tui.requestRender();
165
165
  }
166
166
 
167
+ /** Route non-bracketed paste transports into the active login input. */
168
+ pasteText(text: string): void {
169
+ this.#input.pasteText(text);
170
+ }
171
+
167
172
  handleInput(data: string): void {
168
173
  const kb = getKeybindings();
169
174
 
@@ -63,6 +63,7 @@ export interface MCPAddWizardOAuthResult {
63
63
  interface MCPAddWizardOAuthOptions {
64
64
  serverUrl?: string;
65
65
  resource?: string;
66
+ registrationUrl?: string;
66
67
  /**
67
68
  * External cancellation source. Aborting it tears down the in-flight OAuth
68
69
  * flow and surfaces a neutral cancellation error. The wizard wires its own
@@ -82,6 +83,7 @@ interface WizardState {
82
83
  authMethod: AuthMethod;
83
84
  oauthAuthUrl: string;
84
85
  oauthTokenUrl: string;
86
+ oauthRegistrationUrl: string;
85
87
  oauthClientId: string;
86
88
  oauthClientSecret: string;
87
89
  oauthScopes: string;
@@ -113,6 +115,7 @@ export class MCPAddWizard extends Container {
113
115
  authMethod: "none",
114
116
  oauthAuthUrl: "",
115
117
  oauthTokenUrl: "",
118
+ oauthRegistrationUrl: "",
116
119
  oauthClientId: "",
117
120
  oauthClientSecret: "",
118
121
  oauthScopes: "",
@@ -1026,6 +1029,7 @@ export class MCPAddWizard extends Container {
1026
1029
  if (oauth) {
1027
1030
  this.#state.oauthAuthUrl = oauth.authorizationUrl;
1028
1031
  this.#state.oauthTokenUrl = oauth.tokenUrl;
1032
+ this.#state.oauthRegistrationUrl = oauth.registrationUrl || "";
1029
1033
  this.#state.oauthClientId = oauth.clientId || "";
1030
1034
  this.#state.oauthScopes = oauth.scopes || "";
1031
1035
  this.#state.oauthResource = oauth.resource || (this.#state.transport === "stdio" ? "" : this.#state.url);
@@ -1196,6 +1200,7 @@ export class MCPAddWizard extends Container {
1196
1200
  this.#state.oauthScopes,
1197
1201
  {
1198
1202
  serverUrl: this.#state.url || undefined,
1203
+ registrationUrl: this.#state.oauthRegistrationUrl || undefined,
1199
1204
  resource: oauthResource || undefined,
1200
1205
  abortSignal: this.#oauthAbort.signal,
1201
1206
  },
@@ -400,8 +400,8 @@ export class PlanReviewOverlay implements Component {
400
400
  // Left/right always drive the slider. The sidebar sits beside the body
401
401
  // (above this row), not the slider, so stealing left for it would strand
402
402
  // the operator unable to step the model tier back — reach the ToC via Tab.
403
- const isLeft = matchesKey(data, "left") || (this.#slider !== undefined && data === "h");
404
- const isRight = matchesKey(data, "right") || (this.#slider !== undefined && data === "l");
403
+ const isLeft = matchesKey(data, "left") || (this.#slider !== undefined && matchesKey(data, "h"));
404
+ const isRight = matchesKey(data, "right") || (this.#slider !== undefined && matchesKey(data, "l"));
405
405
  if (isLeft) {
406
406
  this.#moveSlider(-1);
407
407
  return;
@@ -410,12 +410,12 @@ export class PlanReviewOverlay implements Component {
410
410
  this.#moveSlider(1);
411
411
  return;
412
412
  }
413
- if (matchesSelectUp(data) || data === "k") {
413
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
414
414
  if (this.#selectedIndex === this.#firstEnabledIndex()) this.#setFocus("body");
415
415
  else this.#moveSelection(-1);
416
416
  return;
417
417
  }
418
- if (matchesSelectDown(data) || data === "j") {
418
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
419
419
  this.#moveSelection(1);
420
420
  return;
421
421
  }
@@ -427,13 +427,13 @@ export class PlanReviewOverlay implements Component {
427
427
  }
428
428
 
429
429
  #handleBody(data: string): void {
430
- if (matchesKey(data, "left") || data === "h") {
430
+ if (matchesKey(data, "left") || matchesKey(data, "h")) {
431
431
  if (this.#sidebarShown) this.#setFocus("toc");
432
432
  return;
433
433
  }
434
434
  if (
435
435
  matchesKey(data, "right") ||
436
- data === "l" ||
436
+ matchesKey(data, "l") ||
437
437
  matchesKey(data, "enter") ||
438
438
  matchesKey(data, "return") ||
439
439
  data === "\n"
@@ -444,12 +444,12 @@ export class PlanReviewOverlay implements Component {
444
444
  // Vertical nav flows between regions at the edges: scrolling off the bottom
445
445
  // drops into the actions ("next step"); scrolling off the top steps back up
446
446
  // to the ToC.
447
- if (matchesSelectUp(data) || data === "k") {
447
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
448
448
  if (this.#scrollView.getScrollOffset() <= 0 && this.#sidebarShown) this.#setFocus("toc");
449
449
  else this.#scrollView.scroll(-1);
450
450
  return;
451
451
  }
452
- if (matchesSelectDown(data) || data === "j") {
452
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
453
453
  if (this.#scrollView.getScrollOffset() >= this.#scrollView.getMaxScrollOffset()) this.#setFocus("actions");
454
454
  else this.#scrollView.scroll(1);
455
455
  return;
@@ -470,11 +470,11 @@ export class PlanReviewOverlay implements Component {
470
470
  }
471
471
 
472
472
  #handleToc(data: string): void {
473
- if (matchesSelectUp(data) || data === "k") {
473
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
474
474
  this.#moveTocCursor(-1);
475
475
  return;
476
476
  }
477
- if (matchesSelectDown(data) || data === "j") {
477
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
478
478
  // Past the last section, fall through to the actions ("next step").
479
479
  if (this.#tocCursor >= this.#toc.length - 1) this.#setFocus("actions");
480
480
  else this.#moveTocCursor(1);
@@ -482,7 +482,7 @@ export class PlanReviewOverlay implements Component {
482
482
  }
483
483
  if (
484
484
  matchesKey(data, "right") ||
485
- data === "l" ||
485
+ matchesKey(data, "l") ||
486
486
  matchesKey(data, "enter") ||
487
487
  matchesKey(data, "return") ||
488
488
  data === "\n"
@@ -37,9 +37,6 @@ export function readArgsTargetInternalUrl(args: unknown): boolean {
37
37
  type ReadRenderArgs = {
38
38
  path?: string;
39
39
  file_path?: string;
40
- selector?: string;
41
- // Legacy field from the old schema; tolerated for rebuilt transcripts.
42
- sel?: string;
43
40
  };
44
41
 
45
42
  type ReadToolSuffixResolution = {
@@ -344,11 +341,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
344
341
 
345
342
  updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
346
343
  if (!toolCallId) return;
347
- const basePath = args.file_path || args.path || "";
348
- const rawSelector =
349
- typeof args.selector === "string" ? args.selector : typeof args.sel === "string" ? args.sel : undefined;
350
- const selector = rawSelector?.trim().replace(/^:+/, "");
351
- const rawPath = selector && selector.length > 0 ? `${basePath}:${selector}` : basePath;
344
+ const rawPath = args.file_path || args.path || "";
352
345
  const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
353
346
  toolCallId,
354
347
  path: rawPath,
@@ -780,7 +780,16 @@ export class StatusLineComponent implements Component {
780
780
  const activeProvider = session.state.model?.provider ?? session.model?.provider ?? "";
781
781
  if (!activeProvider) return "";
782
782
  const identity = session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId);
783
- return [activeProvider, identity?.accountId ?? "", identity?.email ?? "", identity?.projectId ?? ""].join("\0");
783
+ // orgId is part of the key: rotating between two same-email Anthropic
784
+ // subscriptions must invalidate the cached usage immediately instead of
785
+ // showing the previous org's quota for the rest of the cache TTL.
786
+ return [
787
+ activeProvider,
788
+ identity?.accountId ?? "",
789
+ identity?.email ?? "",
790
+ identity?.projectId ?? "",
791
+ identity?.orgId ?? "",
792
+ ].join("\0");
784
793
  }
785
794
 
786
795
  /**