@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.1

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 (136) hide show
  1. package/CHANGELOG.md +81 -5
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/auth-broker-cli.d.ts +1 -1
  4. package/dist/types/commands/launch.d.ts +8 -0
  5. package/dist/types/config/settings-schema.d.ts +42 -1
  6. package/dist/types/edit/index.d.ts +2 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
  8. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  9. package/dist/types/hashline/executor.d.ts +6 -3
  10. package/dist/types/lsp/index.d.ts +9 -1
  11. package/dist/types/mcp/client.d.ts +2 -1
  12. package/dist/types/mcp/oauth-discovery.d.ts +4 -3
  13. package/dist/types/mcp/timeout.d.ts +9 -0
  14. package/dist/types/mcp/types.d.ts +1 -1
  15. package/dist/types/sdk.d.ts +2 -0
  16. package/dist/types/session/streaming-output.d.ts +1 -1
  17. package/dist/types/task/index.d.ts +2 -0
  18. package/dist/types/task/types.d.ts +4 -0
  19. package/dist/types/tools/approval.d.ts +46 -0
  20. package/dist/types/tools/ask.d.ts +1 -0
  21. package/dist/types/tools/ast-edit.d.ts +2 -0
  22. package/dist/types/tools/ast-grep.d.ts +1 -0
  23. package/dist/types/tools/bash.d.ts +11 -1
  24. package/dist/types/tools/browser.d.ts +2 -0
  25. package/dist/types/tools/calculator.d.ts +1 -0
  26. package/dist/types/tools/checkpoint.d.ts +2 -0
  27. package/dist/types/tools/debug.d.ts +9 -1
  28. package/dist/types/tools/eval.d.ts +2 -0
  29. package/dist/types/tools/find.d.ts +10 -0
  30. package/dist/types/tools/gh.d.ts +2 -1
  31. package/dist/types/tools/hindsight-recall.d.ts +1 -0
  32. package/dist/types/tools/hindsight-reflect.d.ts +1 -0
  33. package/dist/types/tools/hindsight-retain.d.ts +1 -0
  34. package/dist/types/tools/inspect-image.d.ts +1 -0
  35. package/dist/types/tools/irc.d.ts +1 -0
  36. package/dist/types/tools/job.d.ts +1 -0
  37. package/dist/types/tools/read.d.ts +1 -0
  38. package/dist/types/tools/recipe/index.d.ts +1 -0
  39. package/dist/types/tools/render-mermaid.d.ts +1 -0
  40. package/dist/types/tools/resolve.d.ts +1 -0
  41. package/dist/types/tools/search-tool-bm25.d.ts +1 -0
  42. package/dist/types/tools/search.d.ts +1 -0
  43. package/dist/types/tools/ssh.d.ts +2 -0
  44. package/dist/types/tools/todo-write.d.ts +1 -0
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/tools/yield.d.ts +1 -0
  47. package/dist/types/web/search/index.d.ts +1 -0
  48. package/package.json +7 -7
  49. package/src/cli/args.ts +14 -0
  50. package/src/cli/auth-broker-cli.ts +171 -22
  51. package/src/commands/auth-broker.ts +3 -0
  52. package/src/commands/launch.ts +16 -0
  53. package/src/config/mcp-schema.json +2 -2
  54. package/src/config/model-registry.ts +19 -4
  55. package/src/config/prompt-templates.ts +0 -125
  56. package/src/config/settings-schema.ts +59 -1
  57. package/src/config/settings.ts +2 -1
  58. package/src/dap/session.ts +35 -2
  59. package/src/discovery/builtin.ts +2 -2
  60. package/src/discovery/mcp-json.ts +1 -1
  61. package/src/edit/index.ts +26 -0
  62. package/src/edit/modes/patch.ts +1 -1
  63. package/src/edit/streaming.ts +12 -2
  64. package/src/exec/bash-executor.ts +6 -2
  65. package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
  66. package/src/extensibility/custom-tools/types.ts +16 -2
  67. package/src/extensibility/extensions/wrapper.ts +36 -1
  68. package/src/extensibility/hooks/types.ts +8 -1
  69. package/src/hashline/apply.ts +47 -2
  70. package/src/hashline/executor.ts +46 -24
  71. package/src/internal-urls/docs-index.generated.ts +8 -7
  72. package/src/lsp/edits.ts +82 -29
  73. package/src/lsp/index.ts +38 -1
  74. package/src/lsp/utils.ts +1 -1
  75. package/src/main.ts +6 -0
  76. package/src/mcp/client.ts +8 -6
  77. package/src/mcp/oauth-discovery.ts +120 -32
  78. package/src/mcp/oauth-flow.ts +34 -6
  79. package/src/mcp/timeout.ts +59 -0
  80. package/src/mcp/transports/http.ts +42 -44
  81. package/src/mcp/transports/stdio.ts +8 -5
  82. package/src/mcp/types.ts +1 -1
  83. package/src/modes/components/hook-editor.ts +11 -3
  84. package/src/modes/components/mcp-add-wizard.ts +6 -2
  85. package/src/modes/components/model-selector.ts +33 -11
  86. package/src/modes/controllers/command-controller.ts +6 -4
  87. package/src/modes/controllers/mcp-command-controller.ts +8 -4
  88. package/src/prompts/review-custom-request.md +22 -0
  89. package/src/prompts/review-headless-request.md +16 -0
  90. package/src/prompts/review-request.md +2 -3
  91. package/src/prompts/system/project-prompt.md +4 -0
  92. package/src/prompts/tools/debug.md +1 -0
  93. package/src/prompts/tools/find.md +4 -2
  94. package/src/prompts/tools/hashline.md +43 -93
  95. package/src/sdk.ts +47 -73
  96. package/src/session/agent-session.ts +93 -27
  97. package/src/session/streaming-output.ts +1 -1
  98. package/src/slash-commands/helpers/usage-report.ts +3 -1
  99. package/src/task/executor.ts +11 -0
  100. package/src/task/index.ts +19 -0
  101. package/src/task/render.ts +12 -2
  102. package/src/task/types.ts +4 -0
  103. package/src/tools/approval.ts +185 -0
  104. package/src/tools/ask.ts +1 -0
  105. package/src/tools/ast-edit.ts +25 -1
  106. package/src/tools/ast-grep.ts +1 -0
  107. package/src/tools/bash.ts +69 -1
  108. package/src/tools/browser/tab-supervisor.ts +1 -1
  109. package/src/tools/browser.ts +15 -0
  110. package/src/tools/calculator.ts +1 -0
  111. package/src/tools/checkpoint.ts +2 -0
  112. package/src/tools/debug.ts +38 -0
  113. package/src/tools/eval.ts +15 -0
  114. package/src/tools/find.ts +17 -8
  115. package/src/tools/gh.ts +21 -1
  116. package/src/tools/hindsight-recall.ts +1 -0
  117. package/src/tools/hindsight-reflect.ts +1 -0
  118. package/src/tools/hindsight-retain.ts +1 -0
  119. package/src/tools/image-gen.ts +1 -0
  120. package/src/tools/inspect-image.ts +1 -0
  121. package/src/tools/irc.ts +1 -0
  122. package/src/tools/job.ts +1 -0
  123. package/src/tools/path-utils.ts +14 -1
  124. package/src/tools/read.ts +1 -0
  125. package/src/tools/recipe/index.ts +1 -0
  126. package/src/tools/render-mermaid.ts +1 -0
  127. package/src/tools/report-tool-issue.ts +1 -0
  128. package/src/tools/resolve.ts +1 -0
  129. package/src/tools/review.ts +1 -0
  130. package/src/tools/search-tool-bm25.ts +1 -0
  131. package/src/tools/search.ts +1 -0
  132. package/src/tools/ssh.ts +8 -0
  133. package/src/tools/todo-write.ts +1 -0
  134. package/src/tools/write.ts +12 -1
  135. package/src/tools/yield.ts +1 -0
  136. package/src/web/search/index.ts +2 -0
@@ -16,6 +16,7 @@ import type {
16
16
  MCPTransport,
17
17
  } from "../../mcp/types";
18
18
  import { toJsonRpcError } from "../../mcp/types";
19
+ import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
19
20
 
20
21
  /**
21
22
  * HTTP transport for MCP servers.
@@ -180,23 +181,18 @@ export class HttpTransport implements MCPTransport {
180
181
  headers["Mcp-Session-Id"] = this.#sessionId;
181
182
  }
182
183
 
183
- // Create AbortController for timeout
184
- const timeout = this.config.timeout ?? 30000;
185
- const abortController = new AbortController();
186
- const timeoutId = setTimeout(() => abortController.abort(), timeout);
187
- const operationSignal = options?.signal
188
- ? AbortSignal.any([options.signal, abortController.signal])
189
- : abortController.signal;
184
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
185
+ const operation = createMCPTimeout(timeout, options?.signal);
190
186
 
191
187
  try {
192
188
  const response = await fetch(this.config.url, {
193
189
  method: "POST",
194
190
  headers,
195
191
  body: JSON.stringify(body),
196
- signal: operationSignal,
192
+ signal: operation.signal,
197
193
  });
198
194
 
199
- clearTimeout(timeoutId);
195
+ operation.clear();
200
196
 
201
197
  // Check for session ID in response
202
198
  const newSessionId = response.headers.get("Mcp-Session-Id");
@@ -234,11 +230,8 @@ export class HttpTransport implements MCPTransport {
234
230
 
235
231
  return result.result as T;
236
232
  } catch (error) {
237
- clearTimeout(timeoutId);
238
- if (error instanceof Error && error.name === "AbortError") {
239
- if (options?.signal?.aborted) {
240
- throw error;
241
- }
233
+ operation.clear();
234
+ if (operation.isTimeoutAbort(error)) {
242
235
  throw new Error(`Request timeout after ${timeout}ms`);
243
236
  }
244
237
  throw error;
@@ -250,12 +243,9 @@ export class HttpTransport implements MCPTransport {
250
243
  throw new Error("No response body");
251
244
  }
252
245
 
253
- const timeout = this.config.timeout ?? 30000;
254
- const abortController = new AbortController();
255
- const timeoutId = setTimeout(() => abortController.abort(), timeout);
256
- const operationSignal = options?.signal
257
- ? AbortSignal.any([options.signal, abortController.signal])
258
- : abortController.signal;
246
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
247
+ const operation = createMCPTimeout(timeout, options?.signal);
248
+ const signal = operation.signal ?? getNeverAbortSignal();
259
249
 
260
250
  const { promise, resolve, reject } = Promise.withResolvers<T>();
261
251
  let captured = false;
@@ -268,7 +258,7 @@ export class HttpTransport implements MCPTransport {
268
258
  // controller", so we must not exit the loop early.
269
259
  const drain = async (): Promise<void> => {
270
260
  try {
271
- for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(response.body!, operationSignal)) {
261
+ for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(response.body!, signal)) {
272
262
  const messages = Array.isArray(raw) ? raw : [raw];
273
263
  for (const message of messages) {
274
264
  if (
@@ -278,7 +268,7 @@ export class HttpTransport implements MCPTransport {
278
268
  ("result" in message || "error" in message)
279
269
  ) {
280
270
  captured = true;
281
- clearTimeout(timeoutId);
271
+ operation.clear();
282
272
  if (message.error) {
283
273
  reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
284
274
  } else {
@@ -295,17 +285,13 @@ export class HttpTransport implements MCPTransport {
295
285
  }
296
286
  } catch (error) {
297
287
  if (captured) return;
298
- if (error instanceof Error && error.name === "AbortError") {
299
- if (options?.signal?.aborted) {
300
- reject(error);
301
- } else {
302
- reject(new Error(`SSE response timeout after ${timeout}ms`));
303
- }
288
+ if (operation.isTimeoutAbort(error)) {
289
+ reject(new Error(`SSE response timeout after ${timeout}ms`));
304
290
  } else {
305
291
  reject(error as Error);
306
292
  }
307
293
  } finally {
308
- clearTimeout(timeoutId);
294
+ operation.clear();
309
295
  }
310
296
  };
311
297
 
@@ -340,13 +326,16 @@ export class HttpTransport implements MCPTransport {
340
326
  if (this.#sessionId) {
341
327
  headers["Mcp-Session-Id"] = this.#sessionId;
342
328
  }
329
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
330
+ let operation = createMCPTimeout(timeout);
343
331
  try {
344
332
  const resp = await fetch(this.config.url, {
345
333
  method: "POST",
346
334
  headers,
347
335
  body: JSON.stringify(body),
348
- signal: AbortSignal.timeout(this.config.timeout ?? 30000),
336
+ signal: operation.signal,
349
337
  });
338
+ operation.clear();
350
339
  // Retry once on auth failure if onAuthError is wired
351
340
  if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
352
341
  await resp.body?.cancel();
@@ -355,18 +344,21 @@ export class HttpTransport implements MCPTransport {
355
344
  this.config.headers ??= {};
356
345
  Object.assign(this.config.headers, newHeaders);
357
346
  Object.assign(headers, newHeaders);
347
+ operation = createMCPTimeout(timeout);
358
348
  const retry = await fetch(this.config.url, {
359
349
  method: "POST",
360
350
  headers,
361
351
  body: JSON.stringify(body),
362
- signal: AbortSignal.timeout(this.config.timeout ?? 30000),
352
+ signal: operation.signal,
363
353
  });
354
+ operation.clear();
364
355
  await retry.body?.cancel();
365
356
  return;
366
357
  }
367
358
  }
368
359
  await resp.body?.cancel();
369
360
  } catch {
361
+ operation.clear();
370
362
  // Best-effort response delivery — server may have disconnected
371
363
  }
372
364
  }
@@ -392,20 +384,18 @@ export class HttpTransport implements MCPTransport {
392
384
  headers["Mcp-Session-Id"] = this.#sessionId;
393
385
  }
394
386
 
395
- // Create AbortController for timeout
396
- const timeout = this.config.timeout ?? 30000;
397
- const abortController = new AbortController();
398
- const timeoutId = setTimeout(() => abortController.abort(), timeout);
387
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
388
+ const operation = createMCPTimeout(timeout);
399
389
 
400
390
  try {
401
391
  const response = await fetch(this.config.url, {
402
392
  method: "POST",
403
393
  headers,
404
394
  body: JSON.stringify(body),
405
- signal: abortController.signal,
395
+ signal: operation.signal,
406
396
  });
407
397
 
408
- clearTimeout(timeoutId);
398
+ operation.clear();
409
399
 
410
400
  // 202 Accepted is success for notifications
411
401
  if (!response.ok && response.status !== 202) {
@@ -417,15 +407,20 @@ export class HttpTransport implements MCPTransport {
417
407
  // on the notification response (MCP Streamable HTTP spec). Read them.
418
408
  const contentType = response.headers.get("Content-Type") ?? "";
419
409
  if (contentType.includes("text/event-stream") && response.body) {
420
- // Use the SSE connection's signal if available, otherwise read until stream ends
421
- const signal = this.#sseConnection?.signal ?? AbortSignal.timeout(this.config.timeout ?? 30000);
422
- void this.#readSSEStream(response.body, signal);
410
+ // Use the SSE connection's signal if available; otherwise keep the existing finite read timeout.
411
+ if (this.#sseConnection) {
412
+ void this.#readSSEStream(response.body, this.#sseConnection.signal);
413
+ } else {
414
+ const readOperation = createMCPTimeout(timeout);
415
+ const signal = readOperation.signal ?? getNeverAbortSignal();
416
+ void this.#readSSEStream(response.body, signal).finally(() => readOperation.clear());
417
+ }
423
418
  } else {
424
419
  await response.body?.cancel();
425
420
  }
426
421
  } catch (error) {
427
- clearTimeout(timeoutId);
428
- if (error instanceof Error && error.name === "AbortError") {
422
+ operation.clear();
423
+ if (operation.isTimeoutAbort(error)) {
429
424
  throw new Error(`Notify timeout after ${timeout}ms`);
430
425
  }
431
426
  throw error;
@@ -444,8 +439,9 @@ export class HttpTransport implements MCPTransport {
444
439
 
445
440
  // Send session termination if we have a session
446
441
  if (this.#sessionId) {
442
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
443
+ const operation = createMCPTimeout(timeout);
447
444
  try {
448
- const timeout = this.config.timeout ?? 30000;
449
445
  const headers: Record<string, string> = {
450
446
  ...this.config.headers,
451
447
  "Mcp-Session-Id": this.#sessionId,
@@ -454,9 +450,11 @@ export class HttpTransport implements MCPTransport {
454
450
  await fetch(this.config.url, {
455
451
  method: "DELETE",
456
452
  headers,
457
- signal: AbortSignal.timeout(timeout),
453
+ signal: operation.signal,
458
454
  });
455
+ operation.clear();
459
456
  } catch {
457
+ operation.clear();
460
458
  // Ignore termination errors
461
459
  }
462
460
  this.#sessionId = null;
@@ -17,6 +17,7 @@ import type {
17
17
  MCPTransport,
18
18
  } from "../../mcp/types";
19
19
  import { toJsonRpcError } from "../../mcp/types";
20
+ import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
20
21
 
21
22
  /**
22
23
  * Stdio transport for MCP servers.
@@ -208,7 +209,7 @@ export class StdioTransport implements MCPTransport {
208
209
  params: params ?? {},
209
210
  };
210
211
 
211
- const timeout = this.config.timeout ?? 30000;
212
+ const timeout = resolveMCPTimeoutMs(this.config.timeout);
212
213
  const signal = options?.signal;
213
214
 
214
215
  if (signal?.aborted) {
@@ -254,10 +255,12 @@ export class StdioTransport implements MCPTransport {
254
255
  },
255
256
  });
256
257
 
257
- timer = setTimeout(() => {
258
- cleanup();
259
- reject(new Error(`Request timeout after ${timeout}ms`));
260
- }, timeout);
258
+ if (isMCPTimeoutEnabled(timeout)) {
259
+ timer = setTimeout(() => {
260
+ cleanup();
261
+ reject(new Error(`Request timeout after ${timeout}ms`));
262
+ }, timeout);
263
+ }
261
264
 
262
265
  const message = `${JSON.stringify(request)}\n`;
263
266
  try {
package/src/mcp/types.ts CHANGED
@@ -61,7 +61,7 @@ export interface MCPAuthConfig {
61
61
  interface MCPServerConfigBase {
62
62
  /** Whether this server is enabled (default: true) */
63
63
  enabled?: boolean;
64
- /** Connection timeout in milliseconds (default: 30000) */
64
+ /** MCP request timeout in milliseconds (default: 30000, 0 to disable) */
65
65
  timeout?: number;
66
66
  /** Authentication configuration (optional) */
67
67
  auth?: MCPAuthConfig;
@@ -17,6 +17,10 @@ export interface HookEditorOptions {
17
17
  promptStyle?: boolean;
18
18
  }
19
19
 
20
+ function isCtrlEnterSubmit(keyData: string): boolean {
21
+ return matchesKey(keyData, "ctrl+enter") || (keyData.charCodeAt(0) === 10 && keyData.length > 1);
22
+ }
23
+
20
24
  export class HookEditorComponent extends Container {
21
25
  #editor: Editor;
22
26
  #onSubmitCallback: (value: string) => void;
@@ -78,6 +82,10 @@ export class HookEditorComponent extends Container {
78
82
  }
79
83
  }
80
84
 
85
+ #submitCurrentText(): void {
86
+ this.#onSubmitCallback(this.#editor.getExpandedText());
87
+ }
88
+
81
89
  /** Prompt-style: raw Enter submits; Editor owns newline-producing sequences. */
82
90
  #handlePromptStyleInput(keyData: string): void {
83
91
  // Prompt-style keeps Escape as an explicit cancel key and also honors app.interrupt remaps.
@@ -94,7 +102,7 @@ export class HookEditorComponent extends Container {
94
102
 
95
103
  // Submit on any plain Enter encoding, including terminals that report unmodified Enter as LF.
96
104
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
97
- this.#onSubmitCallback(this.#editor.getText());
105
+ this.#submitCurrentText();
98
106
  return;
99
107
  }
100
108
 
@@ -105,8 +113,8 @@ export class HookEditorComponent extends Container {
105
113
  /** Hook-style: Enter=newline, Ctrl+Enter=submit (original behavior) */
106
114
  #handleHookStyleInput(keyData: string): void {
107
115
  // Ctrl+Enter to submit. Use key matching so lock-key and keypad Enter variants work.
108
- if (matchesKey(keyData, "ctrl+enter")) {
109
- this.#onSubmitCallback(this.#editor.getText());
116
+ if (isCtrlEnterSubmit(keyData)) {
117
+ this.#submitCurrentText();
110
118
  return;
111
119
  }
112
120
 
@@ -965,14 +965,18 @@ export class MCPAddWizard extends Container {
965
965
  }, 1000);
966
966
  } catch (error) {
967
967
  // Connection failed - check if it's an auth error
968
- const authResult = analyzeAuthError(error as Error);
968
+ const authResult = analyzeAuthError(error as Error, this.#state.url);
969
969
 
970
970
  if (authResult.requiresAuth) {
971
971
  // Prefer OAuth first: use error metadata, then well-known discovery fallback.
972
972
  let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
973
973
  if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
974
974
  try {
975
- oauth = await discoverOAuthEndpoints(this.#state.url, authResult.authServerUrl);
975
+ oauth = await discoverOAuthEndpoints(
976
+ this.#state.url,
977
+ authResult.authServerUrl,
978
+ authResult.resourceMetadataUrl,
979
+ );
976
980
  } catch {
977
981
  // Ignore discovery failures and fallback to manual auth.
978
982
  }
@@ -277,7 +277,12 @@ export class ModelSelectorComponent extends Container {
277
277
  }
278
278
  }
279
279
 
280
- #sortModels(models: ModelItem[]): void {
280
+ /**
281
+ * @param skipRoleRank When a search query is narrowing the list, role assignments
282
+ * should NOT promote a weakly-matching default model above a perfect text
283
+ * match — defer to MRU/version instead so user affinity drives the order.
284
+ */
285
+ #sortModels(models: ModelItem[], { skipRoleRank = false }: { skipRoleRank?: boolean } = {}): void {
281
286
  // Sort: tagged models (default/smol/slow/plan) first, then MRU, then alphabetical
282
287
  const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
283
288
  const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
@@ -291,9 +296,11 @@ export class ModelSelectorComponent extends Container {
291
296
  const aKey = a.selector;
292
297
  const bKey = b.selector;
293
298
 
294
- const aRank = modelRank(a);
295
- const bRank = modelRank(b);
296
- if (aRank !== bRank) return aRank - bRank;
299
+ if (!skipRoleRank) {
300
+ const aRank = modelRank(a);
301
+ const bRank = modelRank(b);
302
+ if (aRank !== bRank) return aRank - bRank;
303
+ }
297
304
 
298
305
  // Then MRU order (models in mruIndex come before those not in it)
299
306
  const aMru = mruIndex.get(aKey) ?? Number.MAX_SAFE_INTEGER;
@@ -340,16 +347,18 @@ export class ModelSelectorComponent extends Container {
340
347
  });
341
348
  }
342
349
 
343
- #sortCanonicalModels(models: CanonicalModelItem[]): void {
350
+ #sortCanonicalModels(models: CanonicalModelItem[], { skipRoleRank = false }: { skipRoleRank?: boolean } = {}): void {
344
351
  const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
345
352
  const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
346
353
 
347
354
  const modelRank = (item: CanonicalModelItem) => computeModelRank(item.model, this.#roles);
348
355
 
349
356
  models.sort((a, b) => {
350
- const aRank = modelRank(a);
351
- const bRank = modelRank(b);
352
- if (aRank !== bRank) return aRank - bRank;
357
+ if (!skipRoleRank) {
358
+ const aRank = modelRank(a);
359
+ const bRank = modelRank(b);
360
+ if (aRank !== bRank) return aRank - bRank;
361
+ }
353
362
 
354
363
  const aMru = mruIndex.get(`${a.model.provider}/${a.model.id}`) ?? Number.MAX_SAFE_INTEGER;
355
364
  const bMru = mruIndex.get(`${b.model.provider}/${b.model.id}`) ?? Number.MAX_SAFE_INTEGER;
@@ -558,12 +567,25 @@ export class ModelSelectorComponent extends Container {
558
567
  : alphaFiltered.length > 0
559
568
  ? alphaFiltered
560
569
  : baseCanonicalModels;
570
+ // Fuzzy provides the candidate set, but `${provider}/${id}` scoring
571
+ // is biased by provider-prefix length (e.g. `openai/X` beats
572
+ // `openai-codex/X` purely because the prefix is shorter). Re-sort by
573
+ // affinity — MRU then version — so the user's actually-used model
574
+ // wins. Role rank is skipped: when narrowing by query, a weakly
575
+ // matching default model should not be promoted above a stronger
576
+ // non-default match.
561
577
  const fuzzyMatches = fuzzyFilter(fuzzySource, query, ({ searchText }) => searchText);
562
- this.#sortCanonicalModels(fuzzyMatches);
578
+ this.#sortCanonicalModels(fuzzyMatches, { skipRoleRank: true });
563
579
  this.#filteredCanonicalModels = fuzzyMatches;
564
580
  } else {
565
- const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
566
- this.#sortModels(fuzzyMatches);
581
+ // Match against the displayed "provider/id" string so the user can
582
+ // type what they see: bare names (`mimo`, `kimi`), provider prefixes
583
+ // (`openrouter`), or scoped queries (`openrouter/mimo`) all flow
584
+ // through the same fuzzy matcher. The score is biased by provider-
585
+ // prefix length, so re-sort by MRU/version afterwards; skip role
586
+ // rank so a weakly matching default doesn't trump a stronger match.
587
+ const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${provider}/${id}`);
588
+ this.#sortModels(fuzzyMatches, { skipRoleRank: true });
567
589
  this.#filteredModels = fuzzyMatches;
568
590
  }
569
591
  } else {
@@ -1368,10 +1368,12 @@ function formatUnlimitedReportLabel(report: UsageReport, index: number): string
1368
1368
  }
1369
1369
 
1370
1370
  function formatResetShort(limit: UsageLimit, nowMs: number): string | undefined {
1371
- if (limit.window?.resetsAt !== undefined) {
1372
- return formatDuration(limit.window.resetsAt - nowMs);
1373
- }
1374
- return undefined;
1371
+ const resetsAt = limit.window?.resetsAt;
1372
+ if (resetsAt === undefined) return undefined;
1373
+ // Codex returns the prior window's reset_at until a new request opens a fresh window —
1374
+ // rendering a negative delta is meaningless, so drop the suffix in that case.
1375
+ if (resetsAt <= nowMs) return undefined;
1376
+ return formatDuration(resetsAt - nowMs);
1375
1377
  }
1376
1378
 
1377
1379
  function formatAccountHeaderRow(
@@ -401,12 +401,16 @@ export class MCPCommandController {
401
401
  );
402
402
  return;
403
403
  }
404
- const authResult = analyzeAuthError(error as Error);
404
+ const authResult = analyzeAuthError(error as Error, finalConfig.url);
405
405
  if (authResult.requiresAuth) {
406
406
  let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
407
407
  if (!oauth && finalConfig.url) {
408
408
  try {
409
- oauth = await discoverOAuthEndpoints(finalConfig.url, authResult.authServerUrl);
409
+ oauth = await discoverOAuthEndpoints(
410
+ finalConfig.url,
411
+ authResult.authServerUrl,
412
+ authResult.resourceMetadataUrl,
413
+ );
410
414
  } catch {
411
415
  // Ignore discovery error and handle below.
412
416
  }
@@ -742,11 +746,11 @@ export class MCPCommandController {
742
746
  }
743
747
 
744
748
  // Analyze the connection error to extract OAuth endpoints
745
- const authResult = analyzeAuthError(connectionError!);
749
+ const authResult = analyzeAuthError(connectionError!, "url" in config ? config.url : undefined);
746
750
  let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
747
751
 
748
752
  if (!oauth && (config.type === "http" || config.type === "sse") && config.url) {
749
- oauth = await discoverOAuthEndpoints(config.url, authResult.authServerUrl);
753
+ oauth = await discoverOAuthEndpoints(config.url, authResult.authServerUrl, authResult.resourceMetadataUrl);
750
754
  }
751
755
 
752
756
  if (!oauth) {
@@ -0,0 +1,22 @@
1
+ ## Code Review Request
2
+
3
+ ### Mode
4
+
5
+ Custom review instructions
6
+
7
+ ### Distribution Guidelines
8
+
9
+ Use the `task` tool with `agent: "reviewer"` and a `tasks` array.
10
+ Create exactly **1 reviewer task**. Its assignment must include the custom instructions below.
11
+
12
+ ### Reviewer Instructions
13
+
14
+ Reviewer MUST:
15
+ 1. Follow the custom instructions below
16
+ 2. Read the referenced files or workspace context needed to evaluate them
17
+ 3. Call `report_finding` per issue
18
+ 4. Call `yield` with verdict when done
19
+
20
+ ### Custom Instructions
21
+
22
+ {{instructions}}
@@ -0,0 +1,16 @@
1
+ ## Code Review Request
2
+
3
+ ### Mode
4
+
5
+ Headless review request
6
+
7
+ ### Distribution Guidelines
8
+
9
+ Use the `task` tool with `agent: "reviewer"` and a `tasks` array.
10
+ Create exactly **1 reviewer task** for recent code changes.
11
+
12
+ {{#if focus}}
13
+ ### Focus
14
+
15
+ {{focus}}
16
+ {{/if}}
@@ -23,14 +23,13 @@ _No files to review._
23
23
 
24
24
  ### Distribution Guidelines
25
25
 
26
- {{#when agentCount "==" 1}}Use **1 reviewer agent**.{{else}}Spawn **{{agentCount}} reviewer agents** in parallel.{{/when}}
26
+ Use the `task` tool with `agent: "reviewer"` and a `tasks` array.
27
+ {{#when agentCount "==" 1}}Create exactly **1 reviewer task**.{{else}}Spawn **{{agentCount}} reviewer agents** in parallel.{{/when}}
27
28
  {{#if multiAgent}}
28
29
  Group files by locality, e.g.:
29
30
  - Same directory/module → same agent
30
31
  - Related functionality → same agent
31
32
  - Tests with their implementation files → same agent
32
-
33
- You MUST use Task tool with `agent: "reviewer"` and `tasks` array.
34
33
  {{/if}}
35
34
 
36
35
  ### Reviewer Instructions
@@ -22,6 +22,10 @@ MUST read before making changes within:
22
22
  </dir-context>
23
23
  {{/if}}
24
24
 
25
+ {{#ifAny contextFiles.length agentsMdSearch.files.length}}
26
+ The context files above are loaded automatically. You NEVER `search`/`find` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.
27
+ {{/ifAny}}
28
+
25
29
  {{#if workspaceTree.rendered}}
26
30
  <workspace-tree>
27
31
  Working directory layout (sorted by mtime, recent first; depth ≤ 3):
@@ -17,6 +17,7 @@ Use for launching or attaching debuggers, setting breakpoints, stepping through
17
17
  - Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.
18
18
  - Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -m debugpy.adapter`, `dlv dap`.
19
19
  - `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.
20
+ - Python debugging requires `debugpy`; install with `pip install debugpy` if the adapter is unavailable.
20
21
  </caution>
21
22
 
22
23
  <examples>
@@ -1,4 +1,4 @@
1
- Finds files using fast pattern matching that works with any codebase size.
1
+ Finds files and directories using fast pattern matching that works with any codebase size.
2
2
 
3
3
  <instruction>
4
4
  - `paths` is required and accepts an array of globs, files, or directories
@@ -10,7 +10,7 @@ Finds files using fast pattern matching that works with any codebase size.
10
10
  </instruction>
11
11
 
12
12
  <output>
13
- Matching file paths sorted by modification time (most recent first). Truncated at 1000 entries or 50KB (configurable via `limit`).
13
+ Matching file and directory paths sorted by modification time (most recent first). Directories are suffixed with `/`. Truncated at 1000 entries or 50KB (configurable via `limit`).
14
14
  </output>
15
15
 
16
16
  <examples>
@@ -20,6 +20,8 @@ Matching file paths sorted by modification time (most recent first). Truncated a
20
20
  `{"paths": ["src/**/*.ts", "test/**/*.ts"]}`
21
21
  # Find gitignored files like .env
22
22
  `{"paths": [".env*"], "gitignore": false}`
23
+ # Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)
24
+ `{"paths": ["**/tests"]}`
23
25
  # Long-running search on a slow volume
24
26
  `{"paths": ["/Volumes/Storage/**/*.py"], "timeout": 30}`
25
27
  </examples>