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

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 (120) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-d9xenpn9.md} +40 -0
  3. package/dist/cli.js +3025 -3016
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/command-help.d.ts +3 -0
  7. package/dist/types/commands/share.d.ts +25 -0
  8. package/dist/types/commit/agentic/index.d.ts +3 -1
  9. package/dist/types/commit/execute.d.ts +32 -0
  10. package/dist/types/commit/index.d.ts +2 -1
  11. package/dist/types/commit/pipeline.d.ts +7 -1
  12. package/dist/types/config/model-registry.d.ts +4 -0
  13. package/dist/types/config/settings-schema.d.ts +1 -34
  14. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  15. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  16. package/dist/types/discovery/contained-path.d.ts +34 -0
  17. package/dist/types/discovery/index.d.ts +1 -0
  18. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  19. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  21. package/dist/types/extensibility/shared-events.d.ts +3 -2
  22. package/dist/types/extensibility/skills.d.ts +5 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  24. package/dist/types/mcp/types.d.ts +15 -0
  25. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  26. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  29. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  30. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  31. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  32. package/dist/types/secrets/index.d.ts +23 -1
  33. package/dist/types/session/agent-session-events.d.ts +2 -2
  34. package/dist/types/session/agent-session.d.ts +1 -1
  35. package/dist/types/session/messages.d.ts +1 -0
  36. package/dist/types/session/prewalk.d.ts +3 -1
  37. package/dist/types/session/session-tools.d.ts +8 -0
  38. package/dist/types/session/turn-recovery.d.ts +7 -2
  39. package/dist/types/task/index.d.ts +2 -0
  40. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  41. package/package.json +13 -13
  42. package/scripts/legacy-pi-virtual-module.ts +4 -1
  43. package/src/capability/mcp.ts +11 -0
  44. package/src/capability/skill.ts +6 -0
  45. package/src/cli/command-help.ts +4 -0
  46. package/src/cli/gallery-cli.ts +1 -1
  47. package/src/cli-commands.ts +5 -0
  48. package/src/commands/commit.ts +16 -3
  49. package/src/commands/share.ts +71 -0
  50. package/src/commit/agentic/index.ts +39 -21
  51. package/src/commit/execute.ts +56 -0
  52. package/src/commit/index.ts +2 -1
  53. package/src/commit/pipeline.ts +20 -7
  54. package/src/config/model-discovery.ts +1 -1
  55. package/src/config/model-registry.ts +21 -1
  56. package/src/config/settings-schema.ts +2 -33
  57. package/src/config/settings.ts +46 -0
  58. package/src/discovery/agent-plugin-format.ts +551 -0
  59. package/src/discovery/agent-plugins.ts +341 -0
  60. package/src/discovery/claude-plugins.ts +21 -5
  61. package/src/discovery/contained-path.ts +78 -0
  62. package/src/discovery/helpers.ts +23 -9
  63. package/src/discovery/index.ts +1 -0
  64. package/src/discovery/omp-plugins.ts +20 -7
  65. package/src/exec/non-interactive-env.ts +1 -0
  66. package/src/extensibility/custom-tools/types.ts +2 -2
  67. package/src/extensibility/extensions/loader.ts +78 -14
  68. package/src/extensibility/extensions/runner.ts +6 -0
  69. package/src/extensibility/extensions/types.ts +12 -0
  70. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  71. package/src/extensibility/shared-events.ts +3 -2
  72. package/src/extensibility/skills.ts +9 -0
  73. package/src/internal-urls/skill-protocol.ts +14 -0
  74. package/src/internal-urls/vault-protocol.ts +2 -2
  75. package/src/launch/client.ts +11 -1
  76. package/src/launch/protocol.ts +7 -2
  77. package/src/main.ts +3 -2
  78. package/src/mcp/config.ts +3 -0
  79. package/src/mcp/manager.ts +12 -9
  80. package/src/mcp/transports/header-policy.ts +95 -0
  81. package/src/mcp/transports/http.ts +35 -52
  82. package/src/mcp/transports/sse.ts +20 -27
  83. package/src/mcp/types.ts +15 -0
  84. package/src/modes/acp/acp-agent.ts +15 -4
  85. package/src/modes/components/agent-dashboard.ts +2 -0
  86. package/src/modes/components/agent-hub.ts +5 -0
  87. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  88. package/src/modes/components/chat-transcript-builder.ts +3 -0
  89. package/src/modes/components/custom-editor.ts +0 -9
  90. package/src/modes/components/status-line/component.ts +1 -0
  91. package/src/modes/components/status-line/segments.ts +6 -4
  92. package/src/modes/components/status-line/types.ts +2 -0
  93. package/src/modes/components/tool-execution.ts +13 -16
  94. package/src/modes/components/tree-selector.ts +62 -3
  95. package/src/modes/controllers/command-controller.ts +4 -1
  96. package/src/modes/controllers/event-controller.ts +15 -15
  97. package/src/modes/controllers/input-controller.ts +20 -2
  98. package/src/modes/controllers/selector-controller.ts +1 -0
  99. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  100. package/src/modes/utils/ui-helpers.ts +1 -0
  101. package/src/sdk.ts +6 -49
  102. package/src/secrets/index.ts +52 -1
  103. package/src/session/agent-session-events.ts +2 -2
  104. package/src/session/agent-session.ts +33 -14
  105. package/src/session/messages.ts +1 -0
  106. package/src/session/prewalk.ts +57 -17
  107. package/src/session/session-context.ts +3 -5
  108. package/src/session/session-handoff.ts +5 -1
  109. package/src/session/session-tools.ts +37 -3
  110. package/src/session/todo-tracker.ts +11 -1
  111. package/src/session/turn-recovery.ts +94 -72
  112. package/src/slash-commands/builtin-registry.ts +12 -8
  113. package/src/task/index.ts +21 -6
  114. package/src/tools/bash-skill-urls.ts +15 -0
  115. package/src/tools/bash.ts +10 -11
  116. package/src/tools/debug.ts +1 -1
  117. package/src/tools/jtd-to-json-schema.ts +123 -21
  118. package/src/tools/read.ts +5 -4
  119. package/src/tools/shell-tokenize.ts +98 -0
  120. package/src/web/search/providers/exa.ts +1 -1
@@ -19,6 +19,7 @@ import type {
19
19
  import { toJsonRpcError } from "../../mcp/types";
20
20
  import { RequestIdAllocator } from "../request-id";
21
21
  import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
22
+ import { type MCPFetchInit, mcpFetch } from "./header-policy";
22
23
 
23
24
  const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
24
25
  /**
@@ -54,6 +55,16 @@ export class HttpTransport implements MCPTransport {
54
55
 
55
56
  constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
56
57
 
58
+ /** Fetch the configured endpoint with header precedence and origin policy. */
59
+ #fetch(init: MCPFetchInit, generated: Record<string, string>): Promise<Response> {
60
+ return mcpFetch(
61
+ this.config.url,
62
+ init,
63
+ { generated, configured: this.config.headers },
64
+ this.config.headerPolicy === "origin-locked",
65
+ );
66
+ }
67
+
57
68
  get connected(): boolean {
58
69
  return this.#connected;
59
70
  }
@@ -81,13 +92,12 @@ export class HttpTransport implements MCPTransport {
81
92
  if (this.#sseConnection) return;
82
93
 
83
94
  this.#sseConnection = new AbortController();
84
- const headers: Record<string, string> = {
95
+ const generated: Record<string, string> = {
85
96
  Accept: "text/event-stream",
86
- ...this.config.headers,
87
97
  };
88
98
 
89
99
  if (this.#sessionId) {
90
- headers["Mcp-Session-Id"] = this.#sessionId;
100
+ generated["Mcp-Session-Id"] = this.#sessionId;
91
101
  }
92
102
 
93
103
  let response: Response | null;
@@ -95,11 +105,7 @@ export class HttpTransport implements MCPTransport {
95
105
  let startupFinished = false;
96
106
  const connection = this.#sseConnection;
97
107
  const startupTimeoutMs = resolveSSEConnectTimeoutMs(this.config.timeout);
98
- const fetchPromise = fetch(this.config.url, {
99
- method: "GET",
100
- headers,
101
- signal: connection.signal,
102
- });
108
+ const fetchPromise = this.#fetch({ method: "GET", signal: connection.signal }, generated);
103
109
  const timeoutPromise =
104
110
  startupTimeoutMs > 0
105
111
  ? new Promise<null>(resolve => {
@@ -219,26 +225,23 @@ export class HttpTransport implements MCPTransport {
219
225
  params: params ?? {},
220
226
  };
221
227
 
222
- const headers: Record<string, string> = {
228
+ const generated: Record<string, string> = {
223
229
  "Content-Type": "application/json",
224
230
  Accept: "application/json, text/event-stream",
225
- ...this.config.headers,
226
231
  };
227
232
 
228
233
  if (this.#sessionId) {
229
- headers["Mcp-Session-Id"] = this.#sessionId;
234
+ generated["Mcp-Session-Id"] = this.#sessionId;
230
235
  }
231
236
 
232
237
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
233
238
  const operation = createMCPTimeout(timeout, options?.signal);
234
239
 
235
240
  try {
236
- const response = await fetch(this.config.url, {
237
- method: "POST",
238
- headers,
239
- body: JSON.stringify(body),
240
- signal: operation.signal,
241
- });
241
+ const response = await this.#fetch(
242
+ { method: "POST", body: JSON.stringify(body), signal: operation.signal },
243
+ generated,
244
+ );
242
245
 
243
246
  // Check for session ID in response
244
247
  const newSessionId = response.headers.get("Mcp-Session-Id");
@@ -365,23 +368,18 @@ export class HttpTransport implements MCPTransport {
365
368
  const body = error
366
369
  ? { jsonrpc: "2.0" as const, id, error }
367
370
  : { jsonrpc: "2.0" as const, id, result: result ?? {} };
368
- const headers: Record<string, string> = {
371
+ const generated: Record<string, string> = {
369
372
  "Content-Type": "application/json",
370
373
  Accept: "application/json, text/event-stream",
371
- ...this.config.headers,
372
374
  };
373
375
  if (this.#sessionId) {
374
- headers["Mcp-Session-Id"] = this.#sessionId;
376
+ generated["Mcp-Session-Id"] = this.#sessionId;
375
377
  }
378
+ const payload = JSON.stringify(body);
376
379
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
377
380
  const operation = createMCPTimeout(timeout);
378
381
  try {
379
- const resp = await fetch(this.config.url, {
380
- method: "POST",
381
- headers,
382
- body: JSON.stringify(body),
383
- signal: operation.signal,
384
- });
382
+ const resp = await this.#fetch({ method: "POST", body: payload, signal: operation.signal }, generated);
385
383
  // Retry once on auth failure if onAuthError is wired
386
384
  if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
387
385
  await resp.body?.cancel();
@@ -389,16 +387,13 @@ export class HttpTransport implements MCPTransport {
389
387
  if (newHeaders) {
390
388
  this.config.headers ??= {};
391
389
  Object.assign(this.config.headers, newHeaders);
392
- Object.assign(headers, newHeaders);
393
390
  operation.clear();
394
391
  const retryOperation = createMCPTimeout(timeout);
395
392
  try {
396
- const retry = await fetch(this.config.url, {
397
- method: "POST",
398
- headers,
399
- body: JSON.stringify(body),
400
- signal: retryOperation.signal,
401
- });
393
+ const retry = await this.#fetch(
394
+ { method: "POST", body: payload, signal: retryOperation.signal },
395
+ generated,
396
+ );
402
397
  await retry.body?.cancel();
403
398
  } finally {
404
399
  retryOperation.clear();
@@ -425,26 +420,23 @@ export class HttpTransport implements MCPTransport {
425
420
  params: params ?? {},
426
421
  };
427
422
 
428
- const headers: Record<string, string> = {
423
+ const generated: Record<string, string> = {
429
424
  "Content-Type": "application/json",
430
425
  Accept: "application/json, text/event-stream",
431
- ...this.config.headers,
432
426
  };
433
427
 
434
428
  if (this.#sessionId) {
435
- headers["Mcp-Session-Id"] = this.#sessionId;
429
+ generated["Mcp-Session-Id"] = this.#sessionId;
436
430
  }
437
431
 
438
432
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
439
433
  const operation = createMCPTimeout(timeout);
440
434
 
441
435
  try {
442
- const response = await fetch(this.config.url, {
443
- method: "POST",
444
- headers,
445
- body: JSON.stringify(body),
446
- signal: operation.signal,
447
- });
436
+ const response = await this.#fetch(
437
+ { method: "POST", body: JSON.stringify(body), signal: operation.signal },
438
+ generated,
439
+ );
448
440
 
449
441
  // 202 Accepted is success for notifications
450
442
  if (!response.ok && response.status !== 202) {
@@ -492,16 +484,7 @@ export class HttpTransport implements MCPTransport {
492
484
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
493
485
  const operation = createMCPTimeout(timeout);
494
486
  try {
495
- const headers: Record<string, string> = {
496
- ...this.config.headers,
497
- "Mcp-Session-Id": this.#sessionId,
498
- };
499
-
500
- await fetch(this.config.url, {
501
- method: "DELETE",
502
- headers,
503
- signal: operation.signal,
504
- });
487
+ await this.#fetch({ method: "DELETE", signal: operation.signal }, { "Mcp-Session-Id": this.#sessionId });
505
488
  operation.clear();
506
489
  } catch {
507
490
  operation.clear();
@@ -12,6 +12,7 @@ import type {
12
12
  import { toJsonRpcError } from "../../mcp/types";
13
13
  import { RequestIdAllocator } from "../request-id";
14
14
  import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
15
+ import { type MCPFetchInit, mcpFetch } from "./header-policy";
15
16
 
16
17
  interface MCPTimeoutOperation {
17
18
  signal?: AbortSignal;
@@ -46,6 +47,16 @@ export class LegacySseTransport implements MCPTransport {
46
47
  this.#config = config;
47
48
  }
48
49
 
50
+ /** Fetch an endpoint with header precedence and origin policy. */
51
+ #fetch(url: string, init: MCPFetchInit, generated: Record<string, string>): Promise<Response> {
52
+ return mcpFetch(
53
+ url,
54
+ init,
55
+ { generated, configured: this.#config.headers },
56
+ this.#config.headerPolicy === "origin-locked",
57
+ );
58
+ }
59
+
49
60
  get connected(): boolean {
50
61
  return this.#connected;
51
62
  }
@@ -65,14 +76,11 @@ export class LegacySseTransport implements MCPTransport {
65
76
  this.#sseConnection = connection;
66
77
 
67
78
  try {
68
- const response = await fetch(this.#config.url, {
69
- method: "GET",
70
- headers: {
71
- Accept: "text/event-stream",
72
- ...this.#config.headers,
73
- },
74
- signal: operation.signal,
75
- });
79
+ const response = await this.#fetch(
80
+ this.#config.url,
81
+ { method: "GET", signal: operation.signal },
82
+ { Accept: "text/event-stream" },
83
+ );
76
84
 
77
85
  if (!response.ok) {
78
86
  const text = await response.text();
@@ -286,17 +294,12 @@ export class LegacySseTransport implements MCPTransport {
286
294
  ): Promise<Response> {
287
295
  const endpointUrl = this.#endpointUrl;
288
296
  if (!endpointUrl) throw new Error("Transport not connected");
289
- let headers: Record<string, string> = {
297
+ const generated: Record<string, string> = {
290
298
  "Content-Type": "application/json",
291
299
  Accept: "application/json, text/event-stream",
292
- ...this.#config.headers,
293
300
  };
294
- let response = await fetch(endpointUrl, {
295
- method: "POST",
296
- headers,
297
- body: JSON.stringify(body),
298
- signal,
299
- });
301
+ const payload = JSON.stringify(body);
302
+ let response = await this.#fetch(endpointUrl, { method: "POST", body: payload, signal }, generated);
300
303
  const status = AIError.status(response);
301
304
  if (!this.onAuthError || (status !== 401 && status !== 403)) return response;
302
305
 
@@ -304,17 +307,7 @@ export class LegacySseTransport implements MCPTransport {
304
307
  if (!refreshedHeaders) return response;
305
308
  await response.body?.cancel();
306
309
  this.#config.headers = refreshedHeaders;
307
- headers = {
308
- "Content-Type": "application/json",
309
- Accept: "application/json, text/event-stream",
310
- ...this.#config.headers,
311
- };
312
- response = await fetch(endpointUrl, {
313
- method: "POST",
314
- headers,
315
- body: JSON.stringify(body),
316
- signal,
317
- });
310
+ response = await this.#fetch(endpointUrl, { method: "POST", body: payload, signal }, generated);
318
311
  return response;
319
312
  }
320
313
 
package/src/mcp/types.ts CHANGED
@@ -100,6 +100,13 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
100
100
  command: string;
101
101
  args?: string[];
102
102
  env?: Record<string, string>;
103
+ /**
104
+ * `literal`: env values are opaque plugin package data (Agent Plugins
105
+ * §§4.1/9.2) — no env-name lookup, no `!command` execution, no dropping of
106
+ * empty values. The provider already applied the only permitted expansion
107
+ * (`${PLUGIN_ROOT}`/`${PLUGIN_DATA}`).
108
+ */
109
+ envPolicy?: "literal";
103
110
  cwd?: string;
104
111
  }
105
112
 
@@ -108,6 +115,12 @@ export interface MCPHttpServerConfig extends MCPServerConfigBase {
108
115
  type: "http";
109
116
  url: string;
110
117
  headers?: Record<string, string>;
118
+ /**
119
+ * `origin-locked`: configured headers are literal package data pinned to the
120
+ * configured URL's origin (Agent Plugins §7.2.1) — never expanded, never
121
+ * forwarded cross-origin, and client-generated headers win case-insensitively.
122
+ */
123
+ headerPolicy?: "origin-locked";
111
124
  }
112
125
 
113
126
  /** SSE server configuration (deprecated, use HTTP) */
@@ -115,6 +128,8 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
115
128
  type: "sse";
116
129
  url: string;
117
130
  headers?: Record<string, string>;
131
+ /** See {@link MCPHttpServerConfig.headerPolicy}. */
132
+ headerPolicy?: "origin-locked";
118
133
  }
119
134
 
120
135
  export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
@@ -70,6 +70,7 @@ import { SessionManager } from "../../session/session-manager";
70
70
  import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
71
71
  import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
72
72
  import { DEFAULT_STT_MODEL_KEY, STT_MODEL_OPTIONS } from "../../stt/models";
73
+ import { refreshAgentDiscovery } from "../../task";
73
74
  import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
74
75
  import { normalizeLocalScheme } from "../../tools/path-utils";
75
76
  import { ToolError } from "../../tools/tool-errors";
@@ -1930,14 +1931,15 @@ export class AcpAgent implements Agent {
1930
1931
  /**
1931
1932
  * Reload plugin/registry state for an ACP session. Mirrors the interactive
1932
1933
  * `/reload-plugins` and `/move` flows: invalidates the plugin-roots cache,
1933
- * resets the capability cache, refreshes the session's slash-command state,
1934
- * then re-advertises commands so the client sees newly installed/disabled
1935
- * plugins.
1934
+ * refreshes task agents, resets the capability cache, refreshes the
1935
+ * session's slash-command state, then re-advertises commands so the client
1936
+ * sees newly installed/disabled plugins.
1936
1937
  */
1937
1938
  async #reloadPluginState(record: ManagedSessionRecord): Promise<void> {
1938
1939
  const cwd = record.session.sessionManager.getCwd();
1939
1940
  const projectPath = await resolveActiveProjectRegistryPath(cwd);
1940
1941
  clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1942
+ await refreshAgentDiscovery(cwd);
1941
1943
  resetCapabilities();
1942
1944
  await record.session.refreshSkills();
1943
1945
  const fileCommands = await loadSlashCommands({ cwd });
@@ -2019,7 +2021,16 @@ export class AcpAgent implements Agent {
2019
2021
 
2020
2022
  async #findStoredSession(sessionId: string, cwd: string): Promise<StoredSessionInfo | undefined> {
2021
2023
  const sessions = await this.#listStoredSessions(cwd);
2022
- return sessions.find(session => session.id === sessionId);
2024
+ const scoped = sessions.find(session => session.id === sessionId);
2025
+ if (scoped) {
2026
+ return scoped;
2027
+ }
2028
+ // The cwd-derived directory only covers sessions stored under the current
2029
+ // naming scheme. Sessions written under a legacy/hashed project directory
2030
+ // (the 17.2.5+ scheme reverted in #7656) live elsewhere, so fall back to a
2031
+ // global by-id scan: the session id is globally unique, and
2032
+ // #openStoredSession reopens the file with the request cwd. See #7779.
2033
+ return this.#findStoredSessionById(sessionId);
2023
2034
  }
2024
2035
 
2025
2036
  async #findStoredSessionById(sessionId: string): Promise<StoredSessionInfo | undefined> {
@@ -48,6 +48,7 @@ import { Settings } from "../../config/settings";
48
48
  import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
49
49
  import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
50
50
  import { createAgentSession } from "../../sdk";
51
+ import { refreshAgentDiscovery } from "../../task";
51
52
  import { discoverAgents } from "../../task/discovery";
52
53
  import { resolveAgentPrewalkDefault } from "../../task/prewalk";
53
54
  import type { AgentDefinition, AgentSource } from "../../task/types";
@@ -824,6 +825,7 @@ export class AgentDashboard extends Container {
824
825
  ).trimEnd();
825
826
  const content = `---\n${frontmatter}\n---\n\n${spec.systemPrompt.trim()}\n`;
826
827
  await Bun.write(filePath, content);
828
+ await refreshAgentDiscovery(this.cwd);
827
829
  await this.#reloadData();
828
830
  this.#clearCreateFlow();
829
831
  this.#notice = `Created agent ${spec.identifier} at ${shortenPath(filePath)}`;
@@ -123,6 +123,8 @@ export interface AgentHubDeps {
123
123
  ui?: TUI;
124
124
  /** Tool lookup for transcript renderers (labels, custom render functions). */
125
125
  getTool?: (name: string) => AgentTool | undefined;
126
+ /** Whether the active registry entry came from a built-in factory. */
127
+ isBuiltInTool?: (name: string) => boolean;
126
128
  /** Extension message renderers for custom messages in the transcript. */
127
129
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
128
130
  /** Cwd used by tool renderers for path shortening; defaults to the project dir. */
@@ -206,6 +208,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
206
208
  // Transcript-viewer launch deps (passed through to AgentTranscriptViewer).
207
209
  #ui: TUI;
208
210
  #getTool: ((name: string) => AgentTool | undefined) | undefined;
211
+ #isBuiltInTool: ((name: string) => boolean) | undefined;
209
212
  #getMessageRenderer: ((customType: string) => MessageRenderer | undefined) | undefined;
210
213
  #cwd: string;
211
214
  #hideThinkingBlock: (() => boolean) | undefined;
@@ -238,6 +241,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
238
241
  requestComponentRender: () => deps.requestRender(),
239
242
  } as unknown as TUI);
240
243
  this.#getTool = deps.getTool;
244
+ this.#isBuiltInTool = deps.isBuiltInTool;
241
245
  this.#getMessageRenderer = deps.getMessageRenderer;
242
246
  this.#cwd = deps.cwd ?? getProjectDir();
243
247
  this.#hideThinkingBlock = deps.hideThinkingBlock;
@@ -364,6 +368,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
364
368
  lifecycle: this.#remote ? undefined : this.#lifecycle,
365
369
  ui: this.#ui,
366
370
  getTool: this.#getTool,
371
+ isBuiltInTool: this.#isBuiltInTool,
367
372
  getMessageRenderer: this.#getMessageRenderer,
368
373
  cwd: this.#cwd,
369
374
  hideThinkingBlock: this.#hideThinkingBlock,
@@ -43,6 +43,8 @@ export interface AgentTranscriptViewerDeps {
43
43
  lifecycle?: () => AgentLifecycleManager;
44
44
  ui: TUI;
45
45
  getTool?: (name: string) => AgentTool | undefined;
46
+ /** Whether the active registry entry came from a built-in factory. */
47
+ isBuiltInTool?: (name: string) => boolean;
46
48
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
47
49
  cwd: string;
48
50
  hideThinkingBlock?: () => boolean;
@@ -161,6 +163,7 @@ export class AgentTranscriptViewer implements Component {
161
163
  this.#builder = new ChatTranscriptBuilder({
162
164
  ui: deps.ui,
163
165
  getTool: deps.getTool,
166
+ isBuiltInTool: deps.isBuiltInTool,
164
167
  getMessageRenderer: deps.getMessageRenderer,
165
168
  cwd: deps.cwd,
166
169
  hideThinkingBlock: deps.hideThinkingBlock,
@@ -61,6 +61,8 @@ import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user
61
61
  export interface ChatTranscriptBuilderDeps {
62
62
  ui: TUI;
63
63
  getTool?: (name: string) => AgentTool | undefined;
64
+ /** Whether the active registry entry came from a built-in factory. */
65
+ isBuiltInTool?: (name: string) => boolean;
64
66
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
65
67
  cwd: string;
66
68
  hideThinkingBlock?: () => boolean;
@@ -392,6 +394,7 @@ export class ChatTranscriptBuilder {
392
394
  content.name,
393
395
  content.arguments,
394
396
  {
397
+ useBuiltInRenderer: this.deps.isBuiltInTool?.(content.name) ?? true,
395
398
  // Stable ids and Kitty placeholder cells keep images anchored
396
399
  // while the transcript viewport scrolls and reflows.
397
400
  showImages: settings.get("terminal.showImages"),
@@ -30,7 +30,6 @@ type ConfigurableEditorAction = Extract<
30
30
  | "app.model.cycleBackward"
31
31
  | "app.model.select"
32
32
  | "app.model.selectTemporary"
33
- | "app.tools.expand"
34
33
  | "app.tools.toggleVisibility"
35
34
  | "app.thinking.toggle"
36
35
  | "app.editor.external"
@@ -53,7 +52,6 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
53
52
  "app.model.cycleBackward": ["shift+ctrl+p"],
54
53
  "app.model.select": ["alt+m"],
55
54
  "app.model.selectTemporary": ["alt+p"],
56
- "app.tools.expand": ["ctrl+o"],
57
55
  "app.tools.toggleVisibility": ["ctrl+shift+o"],
58
56
  "app.thinking.toggle": ["ctrl+t"],
59
57
  "app.editor.external": ["ctrl+g"],
@@ -551,7 +549,6 @@ export class CustomEditor extends Editor {
551
549
  onCycleModelForward?: () => void;
552
550
  onCycleModelBackward?: () => void;
553
551
  onSelectModel?: () => void;
554
- onExpandTools?: () => void;
555
552
  onToggleToolActivity?: () => void;
556
553
  onToggleThinking?: () => void;
557
554
  onExternalEditor?: () => void;
@@ -907,12 +904,6 @@ export class CustomEditor extends Editor {
907
904
  return;
908
905
  }
909
906
 
910
- // Intercept configured tool output expansion shortcut
911
- if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
912
- this.onExpandTools();
913
- return;
914
- }
915
-
916
907
  // Intercept configured tool activity visibility toggle
917
908
  if (this.#matchesAction(canonical, "app.tools.toggleVisibility") && this.onToggleToolActivity) {
918
909
  this.onToggleToolActivity();
@@ -1556,6 +1556,7 @@ export class StatusLineComponent implements Component {
1556
1556
  return {
1557
1557
  session: this.session,
1558
1558
  focusedAgentId: this.#focusedAgentId,
1559
+ sessionAccent: this.#resolveSettings().sessionAccent !== false,
1559
1560
  activeRepo: activeRepoCache.activeRepo,
1560
1561
  width,
1561
1562
  options: segmentOptions ?? {},
@@ -592,10 +592,12 @@ const sessionNameSegment: StatusLineSegment = {
592
592
  const name = sessionManager?.getSessionName();
593
593
  if (!name) return { content: "", visible: false };
594
594
 
595
- const ansi =
596
- getSessionAccentAnsi(
597
- getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
598
- ) ?? theme.getFgAnsi("accent");
595
+ const accentEnabled = ctx.sessionAccent !== false;
596
+ const ansi = accentEnabled
597
+ ? (getSessionAccentAnsi(
598
+ getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
599
+ ) ?? theme.getFgAnsi("accent"))
600
+ : theme.getFgAnsi("accent");
599
601
  return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
600
602
  },
601
603
  };
@@ -52,6 +52,8 @@ export interface SegmentContext {
52
52
  session: AgentSession;
53
53
  /** Focused subagent id while the view is proxied at its session, undefined otherwise. */
54
54
  focusedAgentId?: string | undefined;
55
+ /** Effective `statusLine.sessionAccent`; `false` disables hash-derived accent colors, while `true` or omission enables them. */
56
+ sessionAccent?: boolean;
55
57
  activeRepo: ActiveRepoContext | null;
56
58
  width: number;
57
59
  options: StatusLineSegmentOptions;
@@ -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) {