@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.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 (70) hide show
  1. package/CHANGELOG.md +140 -133
  2. package/dist/cli.js +250 -218
  3. package/dist/types/config/model-resolver.d.ts +14 -0
  4. package/dist/types/config/settings-schema.d.ts +22 -0
  5. package/dist/types/discovery/helpers.d.ts +7 -0
  6. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
  7. package/dist/types/exec/non-interactive-env.d.ts +2 -0
  8. package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
  9. package/dist/types/modes/types.d.ts +5 -0
  10. package/dist/types/session/agent-session.d.ts +11 -1
  11. package/dist/types/session/messages.d.ts +3 -0
  12. package/dist/types/session/session-manager.d.ts +4 -1
  13. package/dist/types/task/index.d.ts +21 -0
  14. package/dist/types/tools/github-cache.d.ts +5 -4
  15. package/dist/types/tools/job.d.ts +1 -0
  16. package/dist/types/utils/markit.d.ts +8 -0
  17. package/dist/types/web/search/index.d.ts +2 -2
  18. package/dist/types/web/search/provider.d.ts +2 -0
  19. package/package.json +12 -12
  20. package/src/advisor/__tests__/advisor.test.ts +44 -0
  21. package/src/cli/args.ts +2 -0
  22. package/src/collab/host.ts +1 -1
  23. package/src/config/model-resolver.ts +35 -1
  24. package/src/config/settings-schema.ts +23 -1
  25. package/src/discovery/claude-plugins.ts +3 -42
  26. package/src/discovery/github.ts +189 -6
  27. package/src/discovery/helpers.ts +11 -0
  28. package/src/eval/__tests__/prelude-agent.test.ts +73 -0
  29. package/src/eval/js/shared/prelude.txt +12 -3
  30. package/src/eval/py/prelude.py +26 -2
  31. package/src/exec/bash-executor.ts +2 -2
  32. package/src/exec/non-interactive-env.ts +71 -0
  33. package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
  34. package/src/extensibility/extensions/runner.ts +17 -1
  35. package/src/extensibility/plugins/loader.ts +157 -23
  36. package/src/extensibility/plugins/manager.ts +44 -36
  37. package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
  38. package/src/extensibility/plugins/runtime-config.ts +9 -0
  39. package/src/internal-urls/docs-index.generated.ts +9 -9
  40. package/src/internal-urls/issue-pr-protocol.ts +8 -4
  41. package/src/main.ts +5 -1
  42. package/src/modes/acp/acp-agent.ts +3 -3
  43. package/src/modes/components/settings-defs.ts +7 -0
  44. package/src/modes/components/tips.txt +1 -1
  45. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  46. package/src/modes/controllers/input-controller.ts +1 -0
  47. package/src/modes/controllers/selector-controller.ts +7 -0
  48. package/src/modes/interactive-mode.ts +47 -0
  49. package/src/modes/rpc/rpc-mode.ts +3 -3
  50. package/src/modes/runtime-init.ts +2 -1
  51. package/src/modes/types.ts +5 -0
  52. package/src/prompts/agents/designer.md +8 -0
  53. package/src/prompts/review-request.md +1 -1
  54. package/src/prompts/system/subagent-system-prompt.md +4 -1
  55. package/src/prompts/tools/eval.md +13 -3
  56. package/src/prompts/tools/irc.md +1 -1
  57. package/src/sdk.ts +9 -1
  58. package/src/session/agent-session.ts +260 -50
  59. package/src/session/messages.ts +1 -1
  60. package/src/session/session-manager.ts +3 -1
  61. package/src/slash-commands/builtin-registry.ts +5 -2
  62. package/src/system-prompt.ts +7 -1
  63. package/src/task/executor.ts +105 -8
  64. package/src/task/index.ts +70 -9
  65. package/src/tools/github-cache.ts +32 -7
  66. package/src/tools/job.ts +14 -1
  67. package/src/utils/lang-from-path.ts +5 -0
  68. package/src/utils/markit.ts +24 -1
  69. package/src/web/search/index.ts +2 -2
  70. package/src/web/search/provider.ts +14 -2
@@ -37,7 +37,21 @@ export declare function parseModelString(modelStr: string): {
37
37
  * Format a model as "provider/modelId" string.
38
38
  */
39
39
  export declare function formatModelString(model: Model<Api>): string;
40
+ export declare function formatModelStringWithRouting(model: Model<Api>): string;
40
41
  export declare function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string;
42
+ /**
43
+ * Split a trailing `@<upstream>` provider-routing selector off a model pattern.
44
+ *
45
+ * `openrouter/z-ai/glm-4.7@cerebras` -> base `openrouter/z-ai/glm-4.7`, upstream
46
+ * `cerebras`. A `:thinking` suffix after the slug is kept on the base
47
+ * (`...@cerebras:high` -> base `...:high`). Returns undefined when there is no
48
+ * `@` or the suffix is not a bare provider slug, so model ids that legitimately
49
+ * contain `@` (`claude-opus-4-8@default`, `workers-ai/@cf/...`) are never split.
50
+ */
51
+ export declare function splitUpstreamRouting(pattern: string): {
52
+ base: string;
53
+ upstream: string;
54
+ } | undefined;
41
55
  export declare function resolveProviderModelReference(provider: string, modelId: string, availableModels: readonly Model<Api>[]): Model<Api> | undefined;
42
56
  export interface ModelMatchPreferences {
43
57
  /** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
@@ -1,3 +1,4 @@
1
+ import { type SearchProviderId } from "../web/search/types";
1
2
  /** Unified settings schema - single source of truth for all settings.
2
3
  *
3
4
  * Each setting is defined once here with:
@@ -3605,6 +3606,17 @@ export declare const SETTINGS_SCHEMA: {
3605
3606
  readonly description: "Enable plan mode for read-only exploration and planning before execution";
3606
3607
  };
3607
3608
  };
3609
+ readonly "plan.defaultOnStartup": {
3610
+ readonly type: "boolean";
3611
+ readonly default: false;
3612
+ readonly ui: {
3613
+ readonly tab: "tasks";
3614
+ readonly group: "Modes";
3615
+ readonly label: "Start in Plan Mode";
3616
+ readonly description: "Automatically enter plan mode at the start of every new session";
3617
+ readonly condition: "planModeEnabled";
3618
+ };
3619
+ };
3608
3620
  readonly "goal.enabled": {
3609
3621
  readonly type: "boolean";
3610
3622
  readonly default: true;
@@ -4120,6 +4132,16 @@ export declare const SETTINGS_SCHEMA: {
4120
4132
  }];
4121
4133
  };
4122
4134
  };
4135
+ readonly "providers.webSearchExclude": {
4136
+ readonly type: "array";
4137
+ readonly default: SearchProviderId[];
4138
+ readonly ui: {
4139
+ readonly tab: "providers";
4140
+ readonly group: "Services";
4141
+ readonly label: "Excluded Web Search Providers";
4142
+ readonly description: "Providers that web_search should never use, even as fallbacks";
4143
+ };
4144
+ };
4123
4145
  readonly "providers.image": {
4124
4146
  readonly type: "enum";
4125
4147
  readonly values: readonly ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"];
@@ -67,6 +67,13 @@ export declare function getUserPath(ctx: LoadContext, source: SourceId, subpath:
67
67
  * Get project-level path for a source (cwd only).
68
68
  */
69
69
  export declare function getProjectPath(ctx: LoadContext, source: SourceId, subpath: string): string | null;
70
+ /**
71
+ * Resolve GitHub Copilot CLI's user-global config root. Copilot stores per-user
72
+ * instructions/prompts/agents/MCP under `~/.copilot`, relocatable via the
73
+ * `COPILOT_HOME` env var (mirrors Copilot CLI's `--config-dir`). Falls back to
74
+ * `<home>/.copilot` when the override is unset.
75
+ */
76
+ export declare function resolveCopilotHome(home: string): string;
70
77
  /**
71
78
  * Create source metadata for an item.
72
79
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,3 @@
1
1
  export declare const NON_INTERACTIVE_ENV: Readonly<Record<string, string>>;
2
+ /** Builds the per-command environment for non-interactive child processes. */
3
+ export declare function buildNonInteractiveEnv(overrides?: Record<string, string>, baseEnv?: Record<string, string | undefined>, platform?: NodeJS.Platform): Record<string, string>;
@@ -0,0 +1,3 @@
1
+ import type { PluginRuntimeConfig } from "./types";
2
+ /** Normalizes persisted plugin runtime config across legacy lockfile shapes. */
3
+ export declare function normalizePluginRuntimeConfig(config: Partial<PluginRuntimeConfig>): PluginRuntimeConfig;
@@ -45,6 +45,11 @@ export type SubmittedUserInput = {
45
45
  * as a hidden agent-authored `developer` message rather than a visible user
46
46
  * turn. Used by the `c`/`.` continue shortcut. */
47
47
  synthetic?: boolean;
48
+ /** Marks this submission as a deliberate user resume (set by the `.`/`c`
49
+ * continue shortcut, which is also `synthetic`). Forwarded to
50
+ * `session.prompt({ userInitiated })` so it clears advisor auto-resume
51
+ * suppression even though it is synthetic. */
52
+ userInitiated?: boolean;
48
53
  display?: boolean;
49
54
  /** Queue intent if the session is (or becomes) busy when this submission is
50
55
  * dispatched: "steer" (interrupt the active turn) or "followUp" (process after
@@ -250,6 +250,12 @@ export interface PromptOptions {
250
250
  toolChoice?: ToolChoice;
251
251
  /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
252
252
  synthetic?: boolean;
253
+ /** Marks this prompt as a deliberate user action (typed message, `.`/`c`
254
+ * continue). Clears advisor auto-resume suppression that a user interrupt set.
255
+ * Defaults to `!synthetic`; manual-continue is synthetic yet user-initiated, so
256
+ * it sets this explicitly. Agent-initiated synthetic prompts (auto-continue,
257
+ * plan re-prime, reminders) leave it unset and keep suppression latched. */
258
+ userInitiated?: boolean;
253
259
  /** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
254
260
  attribution?: MessageAttribution;
255
261
  /** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
@@ -1129,7 +1135,7 @@ export declare class AgentSession {
1129
1135
  compact?: boolean;
1130
1136
  }): string;
1131
1137
  /**
1132
- * Enable or disable the advisor for this session. The setting is persisted,
1138
+ * Enable or disable the advisor for this session. The setting is overridden for the session,
1133
1139
  * and the runtime is started or stopped to match.
1134
1140
  *
1135
1141
  * @returns true when the advisor is actively running after the call.
@@ -1141,6 +1147,10 @@ export declare class AgentSession {
1141
1147
  * @returns true when the advisor is actively running after the call.
1142
1148
  */
1143
1149
  toggleAdvisorEnabled(): boolean;
1150
+ /**
1151
+ * Whether the advisor setting is enabled for this session.
1152
+ */
1153
+ isAdvisorEnabled(): boolean;
1144
1154
  /**
1145
1155
  * Whether a live advisor agent is attached to this session. True only when
1146
1156
  * `advisor.enabled` is set AND a model resolved for the `advisor` role AND
@@ -55,6 +55,9 @@ export declare function isSilentAbort(errorMessage: string | undefined): boolean
55
55
  export declare const USER_INTERRUPT_LABEL = "Interrupted by user";
56
56
  export declare function isUserInterruptAbort(errorMessage: string | undefined): boolean;
57
57
  export declare function shouldRenderAbortReason(errorMessage: string | undefined): boolean;
58
+ /** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
59
+ * reason (bare `abort()`). Renderers treat it as "no specific reason given". */
60
+ export declare const GENERIC_ABORT_SENTINEL = "Request was aborted";
58
61
  /** Resolve the operator-facing label for an aborted assistant turn. A custom
59
62
  * abort reason threaded onto `errorMessage` is returned verbatim; aborts with
60
63
  * no threaded reason fall back to the retry-aware generic label. Call
@@ -250,8 +250,11 @@ export declare class SessionManager {
250
250
  /**
251
251
  * Open a specific session file.
252
252
  * @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
253
+ * @param options.initialCwd Cwd to use when the file is empty or missing.
253
254
  */
254
- static open(filePath: string, sessionDir?: string, storage?: SessionStorage): Promise<SessionManager>;
255
+ static open(filePath: string, sessionDir?: string, storage?: SessionStorage, options?: {
256
+ initialCwd?: string;
257
+ }): Promise<SessionManager>;
255
258
  /** Continue the most recent session, or create a new one if none exists. */
256
259
  static continueRecent(cwd: string, sessionDir?: string, storage?: SessionStorage): Promise<SessionManager>;
257
260
  /** Create an in-memory session (no file persistence). */
@@ -26,6 +26,27 @@ export declare function formatResultOutputFallback(result: Pick<SingleResult, "o
26
26
  * the same agent ≥2× all without roles. Returns undefined when no nudge applies.
27
27
  */
28
28
  export declare function buildSpecializationAdvisory(agentName: string | undefined, items: TaskItem[], depthCapacity: boolean): string | undefined;
29
+ /**
30
+ * Suggestion — never a rejection — nudging the spawner to coordinate via `irc`
31
+ * when one call creates ≥2 live siblings and it still holds spawn capacity.
32
+ * Returns undefined when there is nothing to coordinate or IRC is unavailable.
33
+ */
34
+ export declare function buildCoordinationAdvisory(items: TaskItem[], depthCapacity: boolean, ircEnabled: boolean): string | undefined;
35
+ /**
36
+ * Compose the non-blocking advisory appended to a `task` result: the
37
+ * specialization nudge, plus — only when the siblings keep running after this
38
+ * call (`willRunAsync`) — the coordination suggestion. Coordination is gated on
39
+ * async because a sync fanout's siblings have already finished, so a
40
+ * "coordinate while they run" hint would misfire. Returns undefined when
41
+ * neither applies.
42
+ */
43
+ export declare function composeSpawnAdvisory(args: {
44
+ agentName: string | undefined;
45
+ items: TaskItem[];
46
+ depthCapacity: boolean;
47
+ ircEnabled: boolean;
48
+ willRunAsync: boolean;
49
+ }): string | undefined;
29
50
  /**
30
51
  * Task tool - Delegate tasks to specialized agents.
31
52
  *
@@ -8,10 +8,11 @@
8
8
  * helpers swallow open/IO failures and degrade to "no cache" so a corrupt or
9
9
  * unreadable DB never blocks a `gh` call.
10
10
  *
11
- * TTL:
12
11
  * Soft TTL → return cached row directly.
13
- * Past soft TTL but within hard TTL → return cached row AND schedule a
14
- * background refresh (errors logged, never thrown).
12
+ * Stateful issue/PR rows past soft TTL but within hard TTL → refresh
13
+ * synchronously, falling back to the cached row if the live fetch fails.
14
+ * Expensive PR diff rows past soft TTL but within hard TTL → return cached
15
+ * row AND schedule a background refresh (errors logged, never thrown).
15
16
  * Past hard TTL → treat as miss and fetch fresh.
16
17
  */
17
18
  import { Database } from "bun:sqlite";
@@ -101,7 +102,7 @@ export interface CacheLookupOptions<T> {
101
102
  settings?: Settings | undefined;
102
103
  now?: number;
103
104
  }
104
- export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
105
+ export type CacheStatus = "miss" | "fresh" | "refreshed" | "stale" | "disabled";
105
106
  export interface CacheLookupResult<T> {
106
107
  rendered: string;
107
108
  sourceUrl: string | undefined;
@@ -56,6 +56,7 @@ export declare class JobTool implements AgentTool<typeof jobSchema, JobToolDetai
56
56
  interface JobRenderArgs {
57
57
  poll?: string[];
58
58
  cancel?: string[];
59
+ list?: boolean;
59
60
  }
60
61
  export declare const jobToolRenderer: {
61
62
  inline: boolean;
@@ -3,5 +3,13 @@ export interface MarkitConversionResult {
3
3
  ok: boolean;
4
4
  error?: string;
5
5
  }
6
+ interface MuPdfWasmModuleConfig {
7
+ print?: (...values: unknown[]) => void;
8
+ printErr?: (...values: unknown[]) => void;
9
+ }
10
+ declare global {
11
+ var $libmupdf_wasm_Module: MuPdfWasmModuleConfig | undefined;
12
+ }
6
13
  export declare function convertFileWithMarkit(filePath: string, signal?: AbortSignal): Promise<MarkitConversionResult>;
7
14
  export declare function convertBufferWithMarkit(buffer: Uint8Array, extension: string, signal?: AbortSignal): Promise<MarkitConversionResult>;
15
+ export {};
@@ -80,6 +80,6 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
80
80
  /** Web search tool as CustomTool (for TUI rendering support) */
81
81
  export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
82
82
  export declare function getSearchTools(): CustomTool<any, any>[];
83
- export { getSearchProvider, setPreferredSearchProvider } from "./provider";
83
+ export { getSearchProvider, setExcludedSearchProviders, setPreferredSearchProvider } from "./provider";
84
84
  export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
85
- export { isSearchProviderPreference } from "./types";
85
+ export { isSearchProviderId, isSearchProviderPreference } from "./types";
@@ -13,6 +13,8 @@ export declare function getSearchProviderLabel(id: SearchProviderId): string;
13
13
  export declare function getSearchProvider(id: SearchProviderId): Promise<SearchProvider>;
14
14
  /** Set the preferred web search provider from settings */
15
15
  export declare function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void;
16
+ /** Set providers that web search should never use, including fallbacks. */
17
+ export declare function setExcludedSearchProviders(providers: readonly SearchProviderId[]): void;
16
18
  /**
17
19
  * Determine which providers are configured and currently available.
18
20
  * Each candidate is loaded (and its `isAvailable()` called) only as the chain
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.0.0",
4
+ "version": "16.0.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,17 +47,17 @@
47
47
  "@agentclientprotocol/sdk": "0.25.0",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "16.0.0",
51
- "@oh-my-pi/omp-stats": "16.0.0",
52
- "@oh-my-pi/pi-agent-core": "16.0.0",
53
- "@oh-my-pi/pi-ai": "16.0.0",
54
- "@oh-my-pi/pi-catalog": "16.0.0",
55
- "@oh-my-pi/pi-mnemopi": "16.0.0",
56
- "@oh-my-pi/pi-natives": "16.0.0",
57
- "@oh-my-pi/pi-tui": "16.0.0",
58
- "@oh-my-pi/pi-utils": "16.0.0",
59
- "@oh-my-pi/pi-wire": "16.0.0",
60
- "@oh-my-pi/snapcompact": "16.0.0",
50
+ "@oh-my-pi/hashline": "16.0.2",
51
+ "@oh-my-pi/omp-stats": "16.0.2",
52
+ "@oh-my-pi/pi-agent-core": "16.0.2",
53
+ "@oh-my-pi/pi-ai": "16.0.2",
54
+ "@oh-my-pi/pi-catalog": "16.0.2",
55
+ "@oh-my-pi/pi-mnemopi": "16.0.2",
56
+ "@oh-my-pi/pi-natives": "16.0.2",
57
+ "@oh-my-pi/pi-tui": "16.0.2",
58
+ "@oh-my-pi/pi-utils": "16.0.2",
59
+ "@oh-my-pi/pi-wire": "16.0.2",
60
+ "@oh-my-pi/snapcompact": "16.0.2",
61
61
  "@opentelemetry/api": "^1.9.1",
62
62
  "@opentelemetry/context-async-hooks": "^2.7.1",
63
63
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "bun:test";
2
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
3
  import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
4
4
  import { getThemeByName } from "../../modes/theme/theme";
5
+ import { formatSessionDumpText } from "../../session/session-dump-format";
5
6
  import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
6
7
  import { YieldQueue } from "../../session/yield-queue";
7
8
  import {
@@ -583,4 +584,47 @@ describe("advisor", () => {
583
584
  expect(text).toContain("truncated.");
584
585
  });
585
586
  });
587
+ describe("formatSessionDumpText raw thinking", () => {
588
+ it("does not nest literal thinking envelopes", () => {
589
+ const md = formatSessionDumpText({
590
+ messages: [
591
+ {
592
+ role: "assistant",
593
+ content: [
594
+ {
595
+ type: "thinking",
596
+ thinking: "<thinking>\nCheck logs before accepting container health.\n</thinking>",
597
+ },
598
+ ],
599
+ timestamp: Date.now(),
600
+ } as AgentMessage,
601
+ ],
602
+ thinkingLevel: "high",
603
+ });
604
+
605
+ expect(md).toContain("Assistant: <thinking>\nCheck logs before accepting container health.\n</thinking>");
606
+ expect(md).not.toContain("<thinking>\n<thinking>");
607
+ });
608
+
609
+ it("unwraps sibling literal thinking envelopes independently", () => {
610
+ const md = formatSessionDumpText({
611
+ messages: [
612
+ {
613
+ role: "assistant",
614
+ content: [
615
+ { type: "thinking", thinking: "<thinking>\nfirst\n</thinking>" },
616
+ { type: "toolCall", id: "tc-1", name: "read", arguments: { path: "file.ts" } },
617
+ { type: "thinking", thinking: "<thinking>\nsecond\n</thinking>" },
618
+ ],
619
+ timestamp: Date.now(),
620
+ } as AgentMessage,
621
+ ],
622
+ tools: [{ name: "read", description: "Read a file", parameters: { type: "object" } }],
623
+ thinkingLevel: "high",
624
+ });
625
+
626
+ expect(md).toContain("Assistant: <thinking>\nfirst\nsecond\n</thinking>");
627
+ expect(md).not.toContain("first\n</thinking>\n<thinking>\nsecond");
628
+ });
629
+ });
586
630
  });
package/src/cli/args.ts CHANGED
@@ -279,6 +279,8 @@ export function getExtraHelpText(): string {
279
279
  KILO_API_KEY - Kilo Gateway models
280
280
  MISTRAL_API_KEY - Mistral models
281
281
  ZAI_API_KEY - z.ai models (ZhipuAI/GLM)
282
+ UMANS_AI_CODING_PLAN_API_KEY - Umans AI Coding Plan models
283
+ UMANS_WEBSEARCH_PROVIDER - Umans gateway web search backend (native or exa)
282
284
  MINIMAX_API_KEY - MiniMax models
283
285
  OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models
284
286
  CURSOR_ACCESS_TOKEN - Cursor AI models
@@ -396,7 +396,7 @@ export class CollabHost {
396
396
  }
397
397
  const name = peer.name;
398
398
  void this.#ctx.session
399
- .abort()
399
+ .abort({ reason: USER_INTERRUPT_LABEL })
400
400
  .then(() => this.#ctx.session.emitNotice("info", `${name} interrupted`, "collab"))
401
401
  .catch(err => logger.warn("collab guest abort failed", { error: String(err) }));
402
402
  }
@@ -92,6 +92,33 @@ export function formatModelString(model: Model<Api>): string {
92
92
  return `${model.provider}/${model.id}`;
93
93
  }
94
94
 
95
+ function getSingleRoutingOnly(routing: unknown): string | undefined {
96
+ if (!routing || typeof routing !== "object" || !("only" in routing) || !Array.isArray(routing.only)) {
97
+ return undefined;
98
+ }
99
+ if (routing.only.length !== 1) return undefined;
100
+ const upstream = routing.only[0];
101
+ return typeof upstream === "string" && upstream ? upstream : undefined;
102
+ }
103
+
104
+ function getSingleUpstreamRoute(model: Model<Api>): string | undefined {
105
+ const compat = model.compat;
106
+ if (!compat || typeof compat !== "object") return undefined;
107
+ if (modelMatchesHost(model, "vercelAIGateway") && "vercelGatewayRouting" in compat) {
108
+ return getSingleRoutingOnly(compat.vercelGatewayRouting);
109
+ }
110
+ if (modelMatchesHost(model, "openrouter") && "openRouterRouting" in compat) {
111
+ return getSingleRoutingOnly(compat.openRouterRouting);
112
+ }
113
+ return undefined;
114
+ }
115
+
116
+ export function formatModelStringWithRouting(model: Model<Api>): string {
117
+ const selector = formatModelString(model);
118
+ const upstream = getSingleUpstreamRoute(model);
119
+ return upstream ? `${selector}@${upstream}` : selector;
120
+ }
121
+
95
122
  export function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string {
96
123
  return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector;
97
124
  }
@@ -161,7 +188,7 @@ const UPSTREAM_ROUTING_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
161
188
  * `@` or the suffix is not a bare provider slug, so model ids that legitimately
162
189
  * contain `@` (`claude-opus-4-8@default`, `workers-ai/@cf/...`) are never split.
163
190
  */
164
- function splitUpstreamRouting(pattern: string): { base: string; upstream: string } | undefined {
191
+ export function splitUpstreamRouting(pattern: string): { base: string; upstream: string } | undefined {
165
192
  const at = pattern.lastIndexOf("@");
166
193
  if (at <= 0) return undefined;
167
194
  const rest = pattern.slice(at + 1);
@@ -481,6 +508,13 @@ function matchModel(
481
508
  // The prefix is not a known provider in this candidate set, so treat the
482
509
  // slash as part of the raw model ID and continue with generic matching.
483
510
  } else {
511
+ // Let the routing fallback apply `@upstream` before fuzzy matching can consume the
512
+ // slug — but only for aggregator providers (OpenRouter / Vercel Gateway). Other
513
+ // providers have ids that legitimately end in `@` (Vertex `claude-opus-4-8@default`),
514
+ // and the fallback never routes them, so they must keep fuzzy matching.
515
+ if (splitUpstreamRouting(modelId) && providerModels.some(supportsUpstreamRouting)) {
516
+ return undefined;
517
+ }
484
518
  const scored = providerModels
485
519
  .map(model => ({ model, match: fuzzyMatch(modelId, model.id) }))
486
520
  .filter(entry => entry.match.matches);
@@ -34,7 +34,7 @@ import {
34
34
  TTS_LOCAL_VOICE_VALUES,
35
35
  } from "../tts/models";
36
36
  import { EDIT_MODES } from "../utils/edit-mode";
37
- import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES } from "../web/search/types";
37
+ import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES, type SearchProviderId } from "../web/search/types";
38
38
 
39
39
  /** Unified settings schema - single source of truth for all settings.
40
40
  *
@@ -3454,6 +3454,18 @@ export const SETTINGS_SCHEMA = {
3454
3454
  },
3455
3455
  },
3456
3456
 
3457
+ "plan.defaultOnStartup": {
3458
+ type: "boolean",
3459
+ default: false,
3460
+ ui: {
3461
+ tab: "tasks",
3462
+ group: "Modes",
3463
+ label: "Start in Plan Mode",
3464
+ description: "Automatically enter plan mode at the start of every new session",
3465
+ condition: "planModeEnabled",
3466
+ },
3467
+ },
3468
+
3457
3469
  "goal.enabled": {
3458
3470
  type: "boolean",
3459
3471
  default: true,
@@ -3846,6 +3858,16 @@ export const SETTINGS_SCHEMA = {
3846
3858
  options: SEARCH_PROVIDER_OPTIONS,
3847
3859
  },
3848
3860
  },
3861
+ "providers.webSearchExclude": {
3862
+ type: "array",
3863
+ default: [] as SearchProviderId[],
3864
+ ui: {
3865
+ tab: "providers",
3866
+ group: "Services",
3867
+ label: "Excluded Web Search Providers",
3868
+ description: "Providers that web_search should never use, even as fallbacks",
3869
+ },
3870
+ },
3849
3871
  "providers.image": {
3850
3872
  type: "enum",
3851
3873
  values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
@@ -124,43 +124,6 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
124
124
  }
125
125
  return { items, warnings };
126
126
  }
127
- async function loadSkillSlashCommands(ctx: LoadContext, root: ClaudePluginRoot): Promise<LoadResult<SlashCommand>> {
128
- const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
129
- const warnings: string[] = warning ? [warning] : [];
130
- const skillsResult = await scanSkillsFromDir(ctx, {
131
- dir: skillsDir,
132
- providerId: PROVIDER_ID,
133
- level: root.scope,
134
- });
135
- warnings.push(...(skillsResult.warnings ?? []));
136
-
137
- const commands = await Promise.all(
138
- skillsResult.items.map(async skill => {
139
- const content = await readFile(skill.path);
140
- if (content === null) {
141
- warnings.push(`Failed to read skill slash command: ${skill.path}`);
142
- return null;
143
- }
144
- // Slash command name MUST come from the skill directory basename, not
145
- // frontmatter `name`: `expandSlashCommand` splits the command at the first
146
- // whitespace, so a display name like "Understand Anything" would never match
147
- // `/understand`. The documented layout is `skills/<name>/SKILL.md` → `/<name>`.
148
- const command: SlashCommand = {
149
- name: path.basename(path.dirname(skill.path)),
150
- path: skill.path,
151
- content,
152
- level: skill.level,
153
- _source: skill._source,
154
- };
155
- return command;
156
- }),
157
- );
158
-
159
- return {
160
- items: commands.filter((command): command is SlashCommand => command !== null),
161
- warnings,
162
- };
163
- }
164
127
 
165
128
  // =============================================================================
166
129
  // Slash Commands
@@ -189,16 +152,14 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
189
152
  };
190
153
  },
191
154
  });
192
- const skillCommandResult = await loadSkillSlashCommands(ctx, root);
193
- return { commandResult, skillCommandResult, warning };
155
+ return { commandResult, warning };
194
156
  }),
195
157
  );
196
158
 
197
- for (const { commandResult, skillCommandResult, warning } of results) {
159
+ for (const { commandResult, warning } of results) {
198
160
  if (warning) warnings.push(warning);
199
- items.push(...commandResult.items, ...skillCommandResult.items);
161
+ items.push(...commandResult.items);
200
162
  if (commandResult.warnings) warnings.push(...commandResult.warnings);
201
- if (skillCommandResult.warnings) warnings.push(...skillCommandResult.warnings);
202
163
  }
203
164
 
204
165
  return { items, warnings };