@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.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 (102) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/dist/cli.js +678 -657
  3. package/dist/types/capability/mcp.d.ts +1 -0
  4. package/dist/types/config/settings-schema.d.ts +49 -4
  5. package/dist/types/export/html/template.generated.d.ts +1 -1
  6. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  7. package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
  8. package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
  9. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  10. package/dist/types/extensibility/hooks/types.d.ts +8 -4
  11. package/dist/types/irc/bus.d.ts +15 -2
  12. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  13. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  14. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  15. package/dist/types/mcp/types.d.ts +2 -0
  16. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  17. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  18. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  19. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  20. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  21. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  22. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  23. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  25. package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
  26. package/dist/types/modes/theme/theme.d.ts +3 -2
  27. package/dist/types/session/agent-session.d.ts +17 -3
  28. package/dist/types/slash-commands/available-commands.d.ts +34 -0
  29. package/dist/types/task/index.d.ts +3 -3
  30. package/dist/types/tools/bash.d.ts +1 -1
  31. package/dist/types/tools/browser/attach.d.ts +4 -4
  32. package/dist/types/tools/browser/registry.d.ts +1 -0
  33. package/dist/types/tools/irc.d.ts +3 -2
  34. package/dist/types/tools/path-utils.d.ts +0 -4
  35. package/dist/types/tools/render-utils.d.ts +22 -0
  36. package/package.json +11 -11
  37. package/src/capability/mcp.ts +1 -0
  38. package/src/cli/gallery-cli.ts +5 -4
  39. package/src/config/mcp-schema.json +4 -0
  40. package/src/config/settings-schema.ts +55 -4
  41. package/src/edit/renderer.ts +96 -46
  42. package/src/exec/bash-executor.ts +21 -6
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +6 -1
  45. package/src/extensibility/custom-commands/loader.ts +3 -1
  46. package/src/extensibility/custom-commands/types.ts +6 -3
  47. package/src/extensibility/custom-tools/loader.ts +4 -7
  48. package/src/extensibility/custom-tools/types.ts +8 -4
  49. package/src/extensibility/extensions/loader.ts +2 -1
  50. package/src/extensibility/extensions/types.ts +2 -2
  51. package/src/extensibility/hooks/loader.ts +3 -1
  52. package/src/extensibility/hooks/types.ts +8 -4
  53. package/src/internal-urls/docs-index.generated.ts +8 -8
  54. package/src/irc/bus.ts +14 -3
  55. package/src/lsp/defaults.json +6 -0
  56. package/src/lsp/render.ts +2 -28
  57. package/src/mcp/manager.ts +3 -0
  58. package/src/mcp/oauth-discovery.ts +27 -2
  59. package/src/mcp/oauth-flow.ts +47 -1
  60. package/src/mcp/transports/stdio.ts +3 -0
  61. package/src/mcp/types.ts +2 -0
  62. package/src/memories/index.ts +2 -0
  63. package/src/modes/acp/acp-agent.ts +4 -67
  64. package/src/modes/components/assistant-message.ts +15 -0
  65. package/src/modes/components/btw-panel.ts +5 -1
  66. package/src/modes/components/mcp-add-wizard.ts +13 -0
  67. package/src/modes/components/plan-review-overlay.ts +32 -3
  68. package/src/modes/components/settings-selector.ts +2 -0
  69. package/src/modes/components/status-line/component.ts +22 -12
  70. package/src/modes/components/status-line/types.ts +3 -0
  71. package/src/modes/components/transcript-container.ts +99 -18
  72. package/src/modes/components/tree-selector.ts +6 -1
  73. package/src/modes/controllers/event-controller.ts +28 -4
  74. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  75. package/src/modes/controllers/selector-controller.ts +4 -0
  76. package/src/modes/controllers/streaming-reveal.ts +16 -8
  77. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  78. package/src/modes/interactive-mode.ts +41 -2
  79. package/src/modes/rpc/rpc-client.ts +32 -0
  80. package/src/modes/rpc/rpc-mode.ts +82 -7
  81. package/src/modes/rpc/rpc-types.ts +23 -0
  82. package/src/modes/theme/theme.ts +13 -7
  83. package/src/modes/utils/ui-helpers.ts +13 -4
  84. package/src/prompts/memories/consolidation_system.md +4 -0
  85. package/src/prompts/system/irc-autoreply.md +6 -0
  86. package/src/prompts/system/irc-incoming.md +1 -1
  87. package/src/prompts/tools/bash.md +1 -0
  88. package/src/prompts/tools/irc.md +1 -1
  89. package/src/prompts/tools/task.md +7 -2
  90. package/src/session/agent-session.ts +120 -10
  91. package/src/slash-commands/available-commands.ts +105 -0
  92. package/src/task/index.ts +15 -10
  93. package/src/task/render.ts +10 -4
  94. package/src/tools/bash.ts +5 -1
  95. package/src/tools/browser/attach.ts +26 -7
  96. package/src/tools/browser/registry.ts +11 -1
  97. package/src/tools/irc.ts +16 -4
  98. package/src/tools/job.ts +7 -3
  99. package/src/tools/path-utils.ts +22 -15
  100. package/src/tools/render-utils.ts +56 -0
  101. package/src/tools/write.ts +65 -47
  102. package/src/web/search/providers/anthropic.ts +29 -4
package/src/irc/bus.ts CHANGED
@@ -7,7 +7,11 @@
7
7
  * AgentLifecycleManager, idle agents are woken with a real turn, and busy
8
8
  * agents receive the message as a non-interrupting aside at the next step
9
9
  * boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
10
- * the recipient, observed via `wait`.
10
+ * the recipient, observed via `wait` — with one exception: when the sender
11
+ * awaits a reply and the recipient is mid-turn with async execution
12
+ * disabled, the recipient session generates an ephemeral side-channel
13
+ * auto-reply (it may be blocked in a synchronous task spawn whose batch
14
+ * includes the sender, so a real turn could never happen in time).
11
15
  */
12
16
 
13
17
  import { logger, Snowflake } from "@oh-my-pi/pi-utils";
@@ -80,8 +84,15 @@ export class IrcBus {
80
84
  * context, so buffering it too would double-deliver via a later
81
85
  * `wait`/`inbox` and inflate unread counts. Only a failed live hand-off
82
86
  * is buffered for the recipient to drain later.
87
+ *
88
+ * `opts.expectsReply` marks sends whose caller is blocked on an answer
89
+ * (`send await:true`). It is forwarded to the recipient session so a
90
+ * mid-turn recipient that cannot reach a step boundary (async execution
91
+ * disabled — e.g. blocked in a synchronous task spawn awaiting the
92
+ * sender's own batch) can generate an ephemeral side-channel auto-reply
93
+ * instead of stranding the sender until timeout.
83
94
  */
84
- async send(msg: Omit<IrcMessage, "id" | "ts">): Promise<IrcDeliveryReceipt> {
95
+ async send(msg: Omit<IrcMessage, "id" | "ts">, opts?: { expectsReply?: boolean }): Promise<IrcDeliveryReceipt> {
85
96
  const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
86
97
  const ref = this.#registry.get(message.to);
87
98
  if (!ref || ref.status === "aborted") {
@@ -118,7 +129,7 @@ export class IrcBus {
118
129
  }
119
130
 
120
131
  try {
121
- const delivery = await session.deliverIrcMessage(message);
132
+ const delivery = await session.deliverIrcMessage(message, opts);
122
133
  this.#relayToMainUi(message);
123
134
  return { to: message.to, outcome: revived ? "revived" : delivery };
124
135
  } catch (error) {
@@ -248,6 +248,12 @@
248
248
  }
249
249
  }
250
250
  },
251
+ "expert": {
252
+ "command": "expert",
253
+ "args": ["--stdio"],
254
+ "fileTypes": [".ex", ".exs", ".heex", ".eex"],
255
+ "rootMarkers": ["mix.exs", "mix.lock"]
256
+ },
251
257
  "erlangls": {
252
258
  "command": "erlang_ls",
253
259
  "args": [],
package/src/lsp/render.ts CHANGED
@@ -8,9 +8,8 @@
8
8
  * - Collapsible/expandable views
9
9
  */
10
10
  import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
11
- import { type HighlightColors, highlightCode as nativeHighlightCode, supportsLanguage } from "@oh-my-pi/pi-natives";
12
11
  import { type Component, Text } from "@oh-my-pi/pi-tui";
13
- import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
12
+ import { getLanguageFromPath, highlightCode as highlightThemeCode, type Theme } from "../modes/theme/theme";
14
13
  import {
15
14
  formatExpandHint,
16
15
  formatMoreItems,
@@ -219,7 +218,7 @@ function renderHover(
219
218
  const beforeCode = fullText.slice(0, codeStart).trimEnd();
220
219
  const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
221
220
 
222
- const codeLines = highlightCode(code, lang, theme);
221
+ const codeLines = highlightThemeCode(code, lang, theme);
223
222
  const icon = theme.styledSymbol("status.info", "accent");
224
223
  const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
225
224
 
@@ -274,31 +273,6 @@ function renderHover(
274
273
  return output.split("\n");
275
274
  }
276
275
 
277
- /**
278
- * Syntax highlight code using native highlighter.
279
- */
280
- function highlightCode(codeText: string, language: string, theme: Theme): string[] {
281
- const validLang = language && supportsLanguage(language) ? language : undefined;
282
- try {
283
- const colors: HighlightColors = {
284
- comment: theme.getFgAnsi("syntaxComment"),
285
- keyword: theme.getFgAnsi("syntaxKeyword"),
286
- function: theme.getFgAnsi("syntaxFunction"),
287
- variable: theme.getFgAnsi("syntaxVariable"),
288
- string: theme.getFgAnsi("syntaxString"),
289
- number: theme.getFgAnsi("syntaxNumber"),
290
- type: theme.getFgAnsi("syntaxType"),
291
- operator: theme.getFgAnsi("syntaxOperator"),
292
- punctuation: theme.getFgAnsi("syntaxPunctuation"),
293
- inserted: theme.getFgAnsi("toolDiffAdded"),
294
- deleted: theme.getFgAnsi("toolDiffRemoved"),
295
- };
296
- return nativeHighlightCode(codeText, validLang, colors).split("\n");
297
- } catch {
298
- return codeText.split("\n");
299
- }
300
- }
301
-
302
276
  // =============================================================================
303
277
  // Diagnostics Rendering
304
278
  // =============================================================================
@@ -1174,12 +1174,15 @@ export class MCPManager {
1174
1174
  const shouldRefresh =
1175
1175
  forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1176
1176
  if (shouldRefresh && credential.refresh && auth.tokenUrl) {
1177
+ const resource =
1178
+ auth.resource ?? (config.type === "http" || config.type === "sse" ? config.url : undefined);
1177
1179
  try {
1178
1180
  const refreshed = await refreshMCPOAuthToken(
1179
1181
  auth.tokenUrl,
1180
1182
  credential.refresh,
1181
1183
  auth.clientId,
1182
1184
  auth.clientSecret,
1185
+ resource,
1183
1186
  );
1184
1187
  const refreshedCredential = { type: "oauth" as const, ...refreshed };
1185
1188
  await this.#authStorage.set(credentialId, refreshedCredential);
@@ -11,6 +11,7 @@ export interface OAuthEndpoints {
11
11
  tokenUrl: string;
12
12
  clientId?: string;
13
13
  scopes?: string;
14
+ resource?: string;
14
15
  }
15
16
 
16
17
  export interface AuthDetectionResult {
@@ -94,7 +95,12 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
94
95
  (obj.default_client_id as string | undefined) ||
95
96
  (obj.public_client_id as string | undefined);
96
97
 
97
- return { authorizationUrl, tokenUrl, clientId, scopes };
98
+ const resource =
99
+ (obj.resource as string | undefined) ||
100
+ (obj.resource_uri as string | undefined) ||
101
+ (obj.resourceUri as string | undefined);
102
+
103
+ return { authorizationUrl, tokenUrl, clientId, scopes, resource };
98
104
  };
99
105
 
100
106
  const clientIdFromAuthUrl = (authorizationUrl: string): string | undefined => {
@@ -161,6 +167,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
161
167
  challengeValues.get("realm");
162
168
  const tokenUrl =
163
169
  challengeValues.get("token_url") || challengeValues.get("token_uri") || challengeValues.get("token_endpoint");
170
+ const resource = challengeValues.get("resource") || challengeValues.get("resource_uri");
164
171
 
165
172
  if (authorizationUrl && tokenUrl) {
166
173
  return {
@@ -168,6 +175,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
168
175
  tokenUrl,
169
176
  clientId: challengeValues.get("client_id") || clientIdFromAuthUrl(authorizationUrl),
170
177
  scopes: challengeValues.get("scope") || challengeValues.get("scopes") || scopeFromAuthUrl(authorizationUrl),
178
+ resource,
171
179
  };
172
180
  }
173
181
  }
@@ -250,7 +258,7 @@ export async function discoverOAuthEndpoints(
250
258
  serverUrl: string,
251
259
  authServerUrl?: string,
252
260
  resourceMetadataUrl?: string,
253
- opts?: { fetch?: FetchImpl },
261
+ opts?: { fetch?: FetchImpl; protectedResource?: string },
254
262
  ): Promise<OAuthEndpoints | null> {
255
263
  const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
256
264
  const wellKnownPaths = [
@@ -264,6 +272,8 @@ export async function discoverOAuthEndpoints(
264
272
  const urlsToQuery: string[] = [];
265
273
  const visitedAuthServers = new Set<string>();
266
274
 
275
+ let protectedResource = opts?.protectedResource;
276
+
267
277
  // Step 1: If a resource_metadata URL was provided, fetch it to discover auth servers.
268
278
  // This follows the RFC 9728 chain: resource_metadata → authorization_servers.
269
279
  if (resourceMetadataUrl && !visitedAuthServers.has(resourceMetadataUrl)) {
@@ -276,6 +286,9 @@ export async function discoverOAuthEndpoints(
276
286
  });
277
287
  if (metaResp.ok) {
278
288
  const meta = (await metaResp.json()) as Record<string, unknown>;
289
+ if (typeof meta.resource === "string" && meta.resource.trim() !== "") {
290
+ protectedResource = meta.resource;
291
+ }
279
292
  const authServers = Array.isArray(meta.authorization_servers)
280
293
  ? meta.authorization_servers.filter((entry): entry is string => typeof entry === "string")
281
294
  : [];
@@ -304,6 +317,8 @@ export async function discoverOAuthEndpoints(
304
317
  const scopesSupported = Array.isArray(metadata.scopes_supported)
305
318
  ? metadata.scopes_supported.filter((scope): scope is string => typeof scope === "string").join(" ")
306
319
  : undefined;
320
+ const resource = typeof metadata.resource === "string" ? metadata.resource : protectedResource;
321
+
307
322
  return {
308
323
  authorizationUrl: String(metadata.authorization_endpoint),
309
324
  tokenUrl: String(metadata.token_endpoint),
@@ -324,12 +339,15 @@ export async function discoverOAuthEndpoints(
324
339
  : typeof metadata.scope === "string"
325
340
  ? metadata.scope
326
341
  : undefined),
342
+ resource,
327
343
  };
328
344
  }
329
345
 
330
346
  if (metadata.oauth || metadata.authorization || metadata.auth) {
331
347
  const oauthData = (metadata.oauth || metadata.authorization || metadata.auth) as Record<string, unknown>;
332
348
  if (typeof oauthData.authorization_url === "string" && typeof oauthData.token_url === "string") {
349
+ const resource = typeof oauthData.resource === "string" ? oauthData.resource : protectedResource;
350
+
333
351
  return {
334
352
  authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
335
353
  tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
@@ -349,6 +367,7 @@ export async function discoverOAuthEndpoints(
349
367
  : typeof oauthData.scope === "string"
350
368
  ? oauthData.scope
351
369
  : undefined,
370
+ resource,
352
371
  };
353
372
  }
354
373
  }
@@ -378,12 +397,18 @@ export async function discoverOAuthEndpoints(
378
397
  ? metadata.authorization_servers.filter((entry): entry is string => typeof entry === "string")
379
398
  : [];
380
399
 
400
+ const discoveredProtectedResource =
401
+ typeof metadata.resource === "string" && metadata.resource.trim() !== ""
402
+ ? metadata.resource
403
+ : protectedResource;
404
+
381
405
  for (const discoveredAuthServer of authServers) {
382
406
  if (visitedAuthServers.has(discoveredAuthServer)) {
383
407
  continue;
384
408
  }
385
409
  const discovered = await discoverOAuthEndpoints(serverUrl, discoveredAuthServer, undefined, {
386
410
  fetch: fetchImpl,
411
+ protectedResource: discoveredProtectedResource,
387
412
  });
388
413
  if (discovered) return discovered;
389
414
  }
@@ -98,6 +98,23 @@ function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOption
98
98
  };
99
99
  }
100
100
 
101
+ function resolveResourceUri(resource: string | undefined): string | undefined {
102
+ const trimmed = resource?.trim();
103
+ if (!trimmed) return undefined;
104
+ if (trimmed !== resource) {
105
+ throw new Error("OAuth resource URI must not include surrounding whitespace");
106
+ }
107
+
108
+ const parsed = new URL(trimmed);
109
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
110
+ throw new Error("OAuth resource URI must use http or https");
111
+ }
112
+ if (parsed.hash) {
113
+ throw new Error("OAuth resource URI must not include a fragment");
114
+ }
115
+ return trimmed;
116
+ }
117
+
101
118
  export interface MCPOAuthConfig {
102
119
  /** Authorization endpoint URL */
103
120
  authorizationUrl: string;
@@ -115,6 +132,8 @@ export interface MCPOAuthConfig {
115
132
  callbackPort?: number;
116
133
  /** Custom callback path (default: /callback or redirectUri pathname) */
117
134
  callbackPath?: string;
135
+ /** MCP resource URI for RFC 8707 resource indicators */
136
+ resource?: string;
118
137
  /** Fetch implementation for token exchange and discovery requests. */
119
138
  fetch?: FetchImpl;
120
139
  }
@@ -128,6 +147,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
128
147
  #registeredClientSecret?: string;
129
148
  #codeVerifier?: string;
130
149
  #fetch: FetchImpl;
150
+ #resource?: string;
131
151
 
132
152
  constructor(
133
153
  private config: MCPOAuthConfig,
@@ -136,6 +156,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
136
156
  super(ctrl, resolveCallbackOptions(config));
137
157
  this.#resolvedClientId = this.#resolveClientId(config);
138
158
  this.#fetch = config.fetch ?? ctrl.fetch ?? fetch;
159
+ this.#resource = resolveResourceUri(
160
+ config.resource ?? this.#resourceFromAuthorizationUrl(config.authorizationUrl),
161
+ );
139
162
  }
140
163
 
141
164
  /**
@@ -157,6 +180,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
157
180
  get registeredClientSecret(): string | undefined {
158
181
  return this.#registeredClientSecret;
159
182
  }
183
+ get resource(): string | undefined {
184
+ return this.#resource;
185
+ }
160
186
 
161
187
  async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
162
188
  if (!this.#resolvedClientId) {
@@ -176,6 +202,12 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
176
202
  if (this.config.scopes && !params.get("scope")) {
177
203
  params.set("scope", this.config.scopes);
178
204
  }
205
+ const existingResource = params.get("resource")?.trim();
206
+ if (existingResource) {
207
+ this.#resource = resolveResourceUri(existingResource);
208
+ } else if (this.#resource) {
209
+ params.set("resource", this.#resource);
210
+ }
179
211
  params.set("redirect_uri", redirectUri);
180
212
  params.set("state", state);
181
213
 
@@ -212,6 +244,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
212
244
  this.#codeVerifier = undefined;
213
245
 
214
246
  // Add client secret if provided
247
+ if (this.#resource) {
248
+ params.set("resource", this.#resource);
249
+ }
215
250
  const clientSecret = this.config.clientSecret ?? this.#registeredClientSecret;
216
251
  if (clientSecret) {
217
252
  params.set("client_secret", clientSecret);
@@ -285,6 +320,13 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
285
320
  return undefined;
286
321
  }
287
322
  }
323
+ #resourceFromAuthorizationUrl(authorizationUrl: string): string | undefined {
324
+ try {
325
+ return new URL(authorizationUrl).searchParams.get("resource") ?? undefined;
326
+ } catch {
327
+ return undefined;
328
+ }
329
+ }
288
330
 
289
331
  /**
290
332
  * Try OAuth dynamic client registration when provider requires a client_id.
@@ -407,14 +449,18 @@ export async function refreshMCPOAuthToken(
407
449
  refreshToken: string,
408
450
  clientId?: string,
409
451
  clientSecret?: string,
452
+ resourceOrOpts?: string | { fetch?: FetchImpl },
410
453
  opts?: { fetch?: FetchImpl },
411
454
  ): Promise<OAuthCredentials> {
412
- const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
455
+ const fetchImpl: FetchImpl = (typeof resourceOrOpts === "string" ? opts?.fetch : resourceOrOpts?.fetch) ?? fetch;
456
+ const resource = typeof resourceOrOpts === "string" ? resourceOrOpts : undefined;
413
457
  const params = new URLSearchParams({
414
458
  grant_type: "refresh_token",
415
459
  refresh_token: refreshToken,
416
460
  });
417
461
  if (clientId) params.set("client_id", clientId);
462
+ const resolvedResource = resolveResourceUri(resource);
463
+ if (resolvedResource) params.set("resource", resolvedResource);
418
464
  if (clientSecret) params.set("client_secret", clientSecret);
419
465
 
420
466
  const response = await fetchImpl(tokenUrl, {
@@ -25,6 +25,7 @@ import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
25
25
  /** Subprocess argv for launching an MCP stdio server. */
26
26
  export interface StdioSpawnCommand {
27
27
  cmd: string[];
28
+ windowsHide?: boolean;
28
29
  }
29
30
 
30
31
  /** Inputs used to resolve platform-specific stdio spawn behavior. */
@@ -153,6 +154,7 @@ export async function resolveStdioSpawnCommand(
153
154
 
154
155
  return {
155
156
  cmd: [resolveComSpec(options.env), "/d", "/s", "/c", buildCmdExeCommand(resolvedCommand, args)],
157
+ windowsHide: true,
156
158
  };
157
159
  }
158
160
 
@@ -255,6 +257,7 @@ export class StdioTransport implements MCPTransport {
255
257
  stdin: "pipe",
256
258
  stdout: "pipe",
257
259
  stderr: "pipe",
260
+ windowsHide: spawnCommand.windowsHide,
258
261
  });
259
262
 
260
263
  this.#connected = true;
package/src/mcp/types.ts CHANGED
@@ -55,6 +55,8 @@ export interface MCPAuthConfig {
55
55
  clientId?: string;
56
56
  /** Client secret — persisted for token refresh */
57
57
  clientSecret?: string;
58
+ /** MCP resource URI — persisted for OAuth resource indicators during refresh */
59
+ resource?: string;
58
60
  }
59
61
 
60
62
  /** Base server config with shared options */
@@ -11,6 +11,7 @@ import type { ModelRegistry } from "../config/model-registry";
11
11
  import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
12
12
  import type { Settings } from "../config/settings";
13
13
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
14
+ import consolidationSystemTemplate from "../prompts/memories/consolidation_system.md" with { type: "text" };
14
15
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
15
16
  import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
16
17
  import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
@@ -752,6 +753,7 @@ async function runConsolidationModel(options: {
752
753
  const response = await completeSimple(
753
754
  model,
754
755
  {
756
+ systemPrompt: [consolidationSystemTemplate],
755
757
  messages: [{ role: "user", content: [{ type: "text", text: input }], timestamp: Date.now() }],
756
758
  },
757
759
  {
@@ -56,7 +56,7 @@ import {
56
56
  } from "../../extensibility/extensions";
57
57
  import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
58
58
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
59
- import { buildSkillPromptMessage, getSkillSlashCommandName } from "../../extensibility/skills";
59
+ import { buildSkillPromptMessage } from "../../extensibility/skills";
60
60
  import { loadSlashCommands } from "../../extensibility/slash-commands";
61
61
  import { resolveLocalUrlToPath } from "../../internal-urls";
62
62
  import { MCPManager } from "../../mcp/manager";
@@ -71,12 +71,8 @@ import {
71
71
  type SessionInfo as StoredSessionInfo,
72
72
  type UsageStatistics,
73
73
  } from "../../session/session-manager";
74
- import {
75
- ACP_BUILTIN_RESERVED_NAMES,
76
- ACP_BUILTIN_SLASH_COMMANDS,
77
- executeAcpBuiltinSlashCommand,
78
- isAcpBuiltinShadowedName,
79
- } from "../../slash-commands/acp-builtins";
74
+ import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
75
+ import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
80
76
  import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
81
77
  import { normalizeLocalScheme } from "../../tools/path-utils";
82
78
  import { runResolveInvocation } from "../../tools/resolve";
@@ -1662,66 +1658,7 @@ export class AcpAgent implements Agent {
1662
1658
  }
1663
1659
 
1664
1660
  async #buildAvailableCommands(session: AgentSession): Promise<AvailableCommand[]> {
1665
- const commands: AvailableCommand[] = [];
1666
- const seenNames = new Set<string>();
1667
- const appendCommand = (command: AvailableCommand): void => {
1668
- if (seenNames.has(command.name)) {
1669
- return;
1670
- }
1671
- seenNames.add(command.name);
1672
- commands.push(command);
1673
- };
1674
-
1675
- // Advertise in the order dispatch resolves them (mirrors AgentSession
1676
- // dispatch: builtins → skills → extensions → custom TS → file-based).
1677
- // `appendCommand` dedupes by name so earlier entries win; extension
1678
- // commands therefore correctly shadow custom TS commands of the same
1679
- // name, matching the runtime behaviour of #tryExecuteExtensionCommand
1680
- // running before #tryExecuteCustomCommand.
1681
- for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
1682
- appendCommand(command);
1683
- }
1684
-
1685
- if (session.skillsSettings?.enableSkillCommands) {
1686
- for (const skill of session.skills) {
1687
- appendCommand({
1688
- name: getSkillSlashCommandName(skill),
1689
- description: skill.description || `Run ${skill.name} skill`,
1690
- input: { hint: "arguments" },
1691
- });
1692
- }
1693
- }
1694
-
1695
- for (const command of session.extensionRunner?.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES) ?? []) {
1696
- // Reserved-set filtering in getRegisteredCommands only covers exact
1697
- // names; colon-namespaced names whose prefix is a builtin (e.g.
1698
- // `model:foo`) would still dispatch to the builtin in ACP.
1699
- if (isAcpBuiltinShadowedName(command.name)) {
1700
- continue;
1701
- }
1702
- appendCommand({
1703
- name: command.name,
1704
- description: command.description ?? "(extension command)",
1705
- input: { hint: "arguments" },
1706
- });
1707
- }
1708
-
1709
- for (const command of session.customCommands) {
1710
- appendCommand({
1711
- name: command.command.name,
1712
- description: command.command.description,
1713
- input: { hint: "arguments" },
1714
- });
1715
- }
1716
-
1717
- for (const command of await loadSlashCommands({ cwd: session.sessionManager.getCwd() })) {
1718
- appendCommand({
1719
- name: command.name,
1720
- description: command.description,
1721
- });
1722
- }
1723
-
1724
- return commands;
1661
+ return toAcpAvailableCommands(await buildAvailableSlashCommands(session));
1725
1662
  }
1726
1663
 
1727
1664
  #toSessionInfo(session: StoredSessionInfo): SessionInfo {
@@ -36,6 +36,16 @@ export class AssistantMessageComponent extends Container {
36
36
  * transcript keeps the error in history.
37
37
  */
38
38
  #errorPinned = false;
39
+ /**
40
+ * Monotonic content version reported to the transcript container via
41
+ * {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
42
+ * choke point every mutator funnels through, including the post-finalize
43
+ * ones: `setErrorPinned(false)` restoring the inline error at the next
44
+ * turn's `agent_start`, late tool-result images, async Kitty conversions,
45
+ * and `setUsageInfo`. Without it, the container's committed-scrollback
46
+ * bypass would replay this block's pre-mutation bytes forever.
47
+ */
48
+ #blockVersion = 0;
39
49
  /** Whether the last updateContent carried an in-flight streaming partial; such
40
50
  * renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
41
51
  #lastUpdateTransient = false;
@@ -86,6 +96,10 @@ export class AssistantMessageComponent extends Container {
86
96
  return this.#transcriptBlockFinalized;
87
97
  }
88
98
 
99
+ getTranscriptBlockVersion(): number {
100
+ return this.#blockVersion;
101
+ }
102
+
89
103
  markTranscriptBlockFinalized(): void {
90
104
  this.#transcriptBlockFinalized = true;
91
105
  }
@@ -215,6 +229,7 @@ export class AssistantMessageComponent extends Container {
215
229
  }
216
230
 
217
231
  updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
232
+ this.#blockVersion++;
218
233
  this.#lastMessage = message;
219
234
  this.#lastUpdateTransient = opts?.transient === true;
220
235
 
@@ -73,7 +73,11 @@ export class BtwPanelComponent extends Container {
73
73
  this.addChild(new Text(this.#footerLine(), 1, 0));
74
74
  this.addChild(new Spacer(1));
75
75
  this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
76
- this.#tui.requestRender();
76
+ // Component-scoped: a rebuild replaces only this panel's own children
77
+ // (streaming deltas arrive per token, and a full compose would re-walk
78
+ // the whole transcript each time). Before the panel is mounted the TUI
79
+ // cannot resolve it and falls back to a full compose on its own.
80
+ this.#tui.requestComponentRender(this);
77
81
  }
78
82
 
79
83
  #footerLine(): string {
@@ -57,6 +57,7 @@ export interface MCPAddWizardOAuthResult {
57
57
  credentialId: string;
58
58
  clientId?: string;
59
59
  clientSecret?: string;
60
+ resource?: string;
60
61
  }
61
62
 
62
63
  interface WizardState {
@@ -71,6 +72,7 @@ interface WizardState {
71
72
  oauthClientId: string;
72
73
  oauthClientSecret: string;
73
74
  oauthScopes: string;
75
+ oauthResource: string;
74
76
  oauthCredentialId: string | null;
75
77
  apiKey: string;
76
78
  authLocation: AuthLocation | null;
@@ -101,6 +103,7 @@ export class MCPAddWizard extends Container {
101
103
  oauthClientId: "",
102
104
  oauthClientSecret: "",
103
105
  oauthScopes: "",
106
+ oauthResource: "",
104
107
  oauthCredentialId: null,
105
108
  apiKey: "",
106
109
  authLocation: null,
@@ -122,6 +125,7 @@ export class MCPAddWizard extends Container {
122
125
  clientId: string,
123
126
  clientSecret: string,
124
127
  scopes: string,
128
+ resource?: string,
125
129
  ) => Promise<MCPAddWizardOAuthResult>)
126
130
  | null = null;
127
131
  #onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
@@ -136,6 +140,7 @@ export class MCPAddWizard extends Container {
136
140
  clientId: string,
137
141
  clientSecret: string,
138
142
  scopes: string,
143
+ resource?: string,
139
144
  ) => Promise<MCPAddWizardOAuthResult>,
140
145
  onTestConnection?: (config: MCPServerConfig) => Promise<void>,
141
146
  onRender?: () => void,
@@ -987,6 +992,7 @@ export class MCPAddWizard extends Container {
987
992
  this.#state.oauthTokenUrl = oauth.tokenUrl;
988
993
  this.#state.oauthClientId = oauth.clientId || "";
989
994
  this.#state.oauthScopes = oauth.scopes || "";
995
+ this.#state.oauthResource = oauth.resource || (this.#state.transport === "stdio" ? "" : this.#state.url);
990
996
  this.#state.authMethod = "oauth";
991
997
 
992
998
  this.#contentContainer.clear();
@@ -1054,6 +1060,7 @@ export class MCPAddWizard extends Container {
1054
1060
  type: "oauth",
1055
1061
  credentialId: this.#state.oauthCredentialId,
1056
1062
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1063
+ resource: this.#state.oauthResource || undefined,
1057
1064
  clientId: this.#state.oauthClientId || undefined,
1058
1065
  clientSecret: this.#state.oauthClientSecret || undefined,
1059
1066
  };
@@ -1081,6 +1088,7 @@ export class MCPAddWizard extends Container {
1081
1088
  type: "oauth",
1082
1089
  credentialId: this.#state.oauthCredentialId,
1083
1090
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1091
+ resource: this.#state.oauthResource || undefined,
1084
1092
  clientId: this.#state.oauthClientId || undefined,
1085
1093
  clientSecret: this.#state.oauthClientSecret || undefined,
1086
1094
  };
@@ -1142,12 +1150,14 @@ export class MCPAddWizard extends Container {
1142
1150
 
1143
1151
  try {
1144
1152
  // Call OAuth handler
1153
+ const oauthResource = this.#state.oauthResource || (this.#state.transport === "stdio" ? "" : this.#state.url);
1145
1154
  const oauthResult = await this.#onOAuthCallback(
1146
1155
  this.#state.oauthAuthUrl,
1147
1156
  this.#state.oauthTokenUrl,
1148
1157
  this.#state.oauthClientId,
1149
1158
  this.#state.oauthClientSecret,
1150
1159
  this.#state.oauthScopes,
1160
+ oauthResource || undefined,
1151
1161
  );
1152
1162
 
1153
1163
  // Store credential ID + any dynamically-registered client credentials,
@@ -1155,6 +1165,7 @@ export class MCPAddWizard extends Container {
1155
1165
  this.#state.oauthCredentialId = oauthResult.credentialId;
1156
1166
  if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
1157
1167
  if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
1168
+ this.#state.oauthResource = oauthResult.resource ?? oauthResource;
1158
1169
 
1159
1170
  // Show success message
1160
1171
  this.#contentContainer.clear();
@@ -1284,6 +1295,7 @@ export class MCPAddWizard extends Container {
1284
1295
  type: "oauth",
1285
1296
  credentialId: this.#state.oauthCredentialId,
1286
1297
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1298
+ resource: this.#state.oauthResource || undefined,
1287
1299
  clientId: this.#state.oauthClientId || undefined,
1288
1300
  clientSecret: this.#state.oauthClientSecret || undefined,
1289
1301
  };
@@ -1312,6 +1324,7 @@ export class MCPAddWizard extends Container {
1312
1324
  type: "oauth",
1313
1325
  credentialId: this.#state.oauthCredentialId,
1314
1326
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1327
+ resource: this.#state.oauthResource || undefined,
1315
1328
  clientId: this.#state.oauthClientId || undefined,
1316
1329
  clientSecret: this.#state.oauthClientSecret || undefined,
1317
1330
  };