@oh-my-pi/pi-coding-agent 17.0.8 → 17.0.9

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 (90) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/cli.js +4850 -4774
  3. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  4. package/dist/types/config/model-registry.d.ts +27 -0
  5. package/dist/types/config/settings-schema.d.ts +34 -3
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  7. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  8. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
  9. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  10. package/dist/types/lsp/client.d.ts +2 -0
  11. package/dist/types/mcp/manager.d.ts +6 -2
  12. package/dist/types/mcp/render.d.ts +2 -2
  13. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  14. package/dist/types/mcp/types.d.ts +6 -0
  15. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  16. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  17. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  18. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  19. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  20. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  21. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  22. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  23. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  24. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  25. package/dist/types/session/agent-session.d.ts +8 -1
  26. package/dist/types/session/session-loader.d.ts +6 -4
  27. package/dist/types/task/types.d.ts +14 -0
  28. package/dist/types/tools/hub/jobs.d.ts +1 -1
  29. package/dist/types/tools/index.d.ts +3 -0
  30. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  31. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  32. package/dist/types/web/search/types.d.ts +1 -1
  33. package/package.json +13 -12
  34. package/scripts/legacy-pi-virtual-module.ts +1 -1
  35. package/src/cli/grievances-cli.ts +2 -2
  36. package/src/cli/usage-cli.ts +1 -1
  37. package/src/config/__tests__/model-registry.test.ts +147 -0
  38. package/src/config/model-registry.ts +40 -0
  39. package/src/config/model-resolver.ts +0 -1
  40. package/src/config/settings-schema.ts +41 -3
  41. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  42. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  43. package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
  44. package/src/internal-urls/registry-helpers.ts +40 -0
  45. package/src/lsp/client.ts +23 -0
  46. package/src/lsp/clients/biome-client.ts +3 -4
  47. package/src/lsp/index.ts +25 -7
  48. package/src/main.ts +1 -0
  49. package/src/mcp/manager.ts +39 -8
  50. package/src/mcp/render.ts +94 -35
  51. package/src/mcp/tool-bridge.ts +107 -11
  52. package/src/mcp/types.ts +7 -0
  53. package/src/modes/components/agent-hub.ts +26 -9
  54. package/src/modes/components/oauth-selector.ts +24 -7
  55. package/src/modes/components/settings-defs.ts +5 -2
  56. package/src/modes/components/settings-selector.ts +9 -5
  57. package/src/modes/components/tool-execution.test.ts +63 -2
  58. package/src/modes/controllers/command-controller.ts +14 -7
  59. package/src/modes/controllers/mcp-command-controller.ts +70 -40
  60. package/src/modes/interactive-mode.ts +3 -0
  61. package/src/modes/rpc/rpc-client.ts +94 -3
  62. package/src/modes/rpc/rpc-frame.ts +164 -4
  63. package/src/modes/rpc/rpc-messages.ts +127 -0
  64. package/src/modes/rpc/rpc-mode.ts +79 -7
  65. package/src/modes/rpc/rpc-types.ts +34 -2
  66. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  67. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  68. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  69. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  70. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  71. package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
  72. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  73. package/src/prompts/goals/guided-goal-system.md +24 -3
  74. package/src/prompts/tools/task.md +12 -2
  75. package/src/sdk.ts +45 -3
  76. package/src/session/agent-session.ts +63 -32
  77. package/src/session/session-context.test.ts +224 -1
  78. package/src/session/session-context.ts +41 -2
  79. package/src/session/session-loader.ts +10 -5
  80. package/src/slash-commands/helpers/usage-report.ts +3 -1
  81. package/src/task/executor.ts +3 -0
  82. package/src/task/index.ts +52 -8
  83. package/src/task/structured-subagent.ts +3 -1
  84. package/src/task/types.ts +16 -0
  85. package/src/tools/hub/index.ts +1 -1
  86. package/src/tools/hub/jobs.ts +67 -8
  87. package/src/tools/index.ts +3 -0
  88. package/src/tools/report-tool-issue.ts +79 -28
  89. package/src/web/search/providers/firecrawl.ts +46 -13
  90. package/src/web/search/types.ts +5 -1
@@ -83,6 +83,7 @@ export declare const taskItemSchema: import("arktype/internal/variants/object.ts
83
83
  name?: string | undefined;
84
84
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
85
85
  task: string;
86
+ model?: string | string[] | undefined;
86
87
  outputSchema?: string | boolean | object | null | undefined;
87
88
  schemaMode?: "permissive" | "strict" | undefined;
88
89
  }, {}>;
@@ -94,6 +95,8 @@ export interface TaskItem {
94
95
  agent?: string;
95
96
  /** The work; required by the schema. */
96
97
  task?: string;
98
+ /** Explicit model selector or fallback chain for this spawn, including optional reasoning suffixes. */
99
+ model?: string | string[];
97
100
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
98
101
  outputSchema?: unknown;
99
102
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -105,6 +108,7 @@ export declare const taskSchema: import("arktype/internal/variants/object.ts").O
105
108
  name?: string | undefined;
106
109
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
107
110
  task: string;
111
+ model?: string | string[] | undefined;
108
112
  outputSchema?: string | boolean | object | null | undefined;
109
113
  schemaMode?: "permissive" | "strict" | undefined;
110
114
  isolated?: boolean | undefined;
@@ -113,6 +117,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
113
117
  name?: string | undefined;
114
118
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
115
119
  task: string;
120
+ model?: string | string[] | undefined;
116
121
  outputSchema?: string | boolean | object | null | undefined;
117
122
  schemaMode?: "permissive" | "strict" | undefined;
118
123
  isolated?: boolean | undefined;
@@ -120,6 +125,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
120
125
  name?: string | undefined;
121
126
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
122
127
  task: string;
128
+ model?: string | string[] | undefined;
123
129
  outputSchema?: string | boolean | object | null | undefined;
124
130
  schemaMode?: "permissive" | "strict" | undefined;
125
131
  }, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
@@ -128,6 +134,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
128
134
  name?: string | undefined;
129
135
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
130
136
  task: string;
137
+ model?: string | string[] | undefined;
131
138
  outputSchema?: string | boolean | object | null | undefined;
132
139
  schemaMode?: "permissive" | "strict" | undefined;
133
140
  isolated?: boolean | undefined;
@@ -138,6 +145,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
138
145
  name?: string | undefined;
139
146
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
140
147
  task: string;
148
+ model?: string | string[] | undefined;
141
149
  outputSchema?: string | boolean | object | null | undefined;
142
150
  schemaMode?: "permissive" | "strict" | undefined;
143
151
  }[];
@@ -168,6 +176,8 @@ export interface TaskParams {
168
176
  agent?: string;
169
177
  /** The work (flat form). */
170
178
  task?: string;
179
+ /** Explicit model selector or fallback chain for the spawn, including optional reasoning suffixes. */
180
+ model?: string | string[];
171
181
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
172
182
  outputSchema?: unknown;
173
183
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -291,6 +301,8 @@ export interface AgentProgress {
291
301
  modelOverride?: string | string[];
292
302
  /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
293
303
  resolvedModel?: string;
304
+ /** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
305
+ resolvedModelIsFallback?: boolean;
294
306
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
295
307
  extractedToolData?: Record<string, unknown[]>;
296
308
  /**
@@ -357,6 +369,8 @@ export interface SingleResult {
357
369
  modelOverride?: string | string[];
358
370
  /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Omitted from tool-result JSON when undefined to keep wire payloads small. */
359
371
  resolvedModel?: string;
372
+ /** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
373
+ resolvedModelIsFallback?: boolean;
360
374
  error?: string;
361
375
  aborted?: boolean;
362
376
  abortReason?: string;
@@ -55,7 +55,7 @@ export declare function noMatchingJobsResult(session: ToolSession, ids: string[]
55
55
  /** Bare `wait` with no running jobs and nobody who could message: nothing to block on. */
56
56
  export declare function nothingToWaitForResult(session: ToolSession): AgentToolResult<CoordinationDetails>;
57
57
  /** `cancel`: kill the named jobs; returns immediately with outcomes + snapshots. */
58
- export declare function executeCancel(session: ToolSession, manager: AsyncJobManager, ownerId: string | undefined, ids: string[]): AgentToolResult<CoordinationDetails>;
58
+ export declare function executeCancel(session: ToolSession, manager: AsyncJobManager, ownerId: string | undefined, ids: string[]): Promise<AgentToolResult<CoordinationDetails>>;
59
59
  /** `jobs`: read-only snapshot of every job plus the jobless running-agent roster. */
60
60
  export declare function executeJobsSnapshot(session: ToolSession, manager: AsyncJobManager, ownerId: string | undefined): AgentToolResult<CoordinationDetails>;
61
61
  /** Pending-call frame for job ops (wait/cancel/jobs). */
@@ -13,6 +13,7 @@ import type { LocalProtocolOptions } from "../internal-urls/index.js";
13
13
  import type { MCPManager } from "../mcp/index.js";
14
14
  import type { MnemopiSessionState } from "../mnemopi/state.js";
15
15
  import type { PlanModeState } from "../plan-mode/state.js";
16
+ import type { AgentLifecycleManager } from "../registry/agent-lifecycle.js";
16
17
  import type { AgentRegistry } from "../registry/agent-registry.js";
17
18
  import type { ArtifactManager } from "../session/artifacts.js";
18
19
  import type { ClientBridge } from "../session/client-bridge.js";
@@ -200,6 +201,8 @@ export interface ToolSession {
200
201
  xdevRegistry?: XdevRegistry;
201
202
  /** Agent registry for IRC routing across live sessions. */
202
203
  agentRegistry?: AgentRegistry;
204
+ /** Idle→parked→revive lifecycle owner; lets the hub kill a non-job-backed agent registration. Default: AgentLifecycleManager.global(). */
205
+ agentLifecycle?: () => AgentLifecycleManager;
203
206
  /** Get artifacts directory for artifact:// URLs */
204
207
  getArtifactsDir?: () => string | null;
205
208
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
@@ -5,16 +5,21 @@
5
5
  * `xd://report_issue`, and the system prompt tells the model to write
6
6
  * `<tool>: <concise description>` there when auto-QA is enabled.
7
7
  *
8
- * Enabled by default; gated behind PI_AUTO_QA=1 / `dev.autoqa` so a user who
9
- * flips the setting off short-circuits injection entirely.
8
+ * Enabled by default (`dev.autoqa` defaults to true); `PI_AUTO_QA=0` or an
9
+ * explicit `dev.autoqa: false` short-circuits injection entirely. When the
10
+ * user is only enabled by default (never configured `dev.autoqa` themselves),
11
+ * a persisted `dev.autoqaConsent: "denied"` also disables injection so a "No"
12
+ * in the consent dialog fully turns the feature off.
10
13
  * Records grievances to a local SQLite database; never throws from the device
11
14
  * dispatch path.
12
15
  *
13
- * Before the first record lands, the user's consent is checked. If they've
14
- * never been asked (`dev.autoqaConsent === "unset"`) the process-global
15
- * consent handler — wired by `InteractiveMode` to a Yes/No popup — is invoked
16
- * exactly once and the decision is persisted. Subsequent calls (including from
17
- * subagents) read the cached decision without prompting.
16
+ * Nothing is written until consent resolves. If the user has never been asked
17
+ * (`dev.autoqaConsent === "unset"`) the process-global consent handler —
18
+ * wired by `InteractiveMode` to a Yes/No popup — is invoked exactly once and
19
+ * the decision is persisted; a denial (or dismissal) drops the pending report
20
+ * without touching the database. Subsequent calls (including from subagents)
21
+ * read the cached decision without prompting. `PI_AUTO_QA_PUSH=1` bypasses
22
+ * the dialog for headless environments.
18
23
  *
19
24
  * When the user grants consent, push is automatically active against the
20
25
  * bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
@@ -42,6 +47,14 @@ export declare function isReportIssueToolCall(toolCall: {
42
47
  }): boolean;
43
48
  /** Call preview for an `xd://report_issue` write. */
44
49
  export declare function renderReportIssueDeviceCall(content: unknown, uiTheme: Theme): Component;
50
+ /**
51
+ * Whether Auto-QA is active for this session.
52
+ *
53
+ * Precedence: `PI_AUTO_QA` env flag > explicit `dev.autoqa` setting >
54
+ * default-on unless the user previously denied consent. The denial veto only
55
+ * applies to the default: explicitly configuring `dev.autoqa: true` re-enables
56
+ * injection (recording still no-ops until consent is granted).
57
+ */
45
58
  export declare function isAutoQaEnabled(settings?: Settings): boolean;
46
59
  /**
47
60
  * Resolver for the user's "share grievances?" consent.
@@ -82,8 +95,8 @@ export declare function __resetAutoQaConsentForTests(): void;
82
95
  export declare function resolveAutoQaConsent(settings: Settings | undefined): Promise<boolean>;
83
96
  /**
84
97
  * Open (or return the cached handle for) the auto-QA SQLite database at
85
- * `~/.omp/agent/autoqa.db`, creating the schema lazily. Returns `null` when
86
- * the agent data dir cannot be resolved.
98
+ * `~/.omp/autoqa.db` (XDG: `$XDG_DATA_HOME/omp/autoqa.db`), creating the
99
+ * schema lazily. Returns `null` when the path cannot be resolved or opened.
87
100
  */
88
101
  export declare function openAutoQaDb(): Database | null;
89
102
  export interface FlushResult {
@@ -127,6 +140,8 @@ export declare function __resetAutoQaFlushStateForTests(): void;
127
140
  * Flush queued grievances to the configured backend.
128
141
  */
129
142
  export declare function flushGrievances(db?: Database, settings?: Settings, options?: FlushOptions): Promise<FlushResult>;
143
+ /** Test-only: await the last consent → insert → flush pipeline. */
144
+ export declare function __awaitAutoQaRecordPipelineForTests(): Promise<void>;
130
145
  /**
131
146
  * Execute `write xd://report_issue`. `text` must be either:
132
147
  * - `<tool>: <concise description>` on one line, or
@@ -23,6 +23,15 @@ export declare function searchFirecrawl(params: SearchParams): Promise<SearchRes
23
23
  export declare class FirecrawlProvider extends SearchProvider {
24
24
  readonly id = "firecrawl";
25
25
  readonly label = "Firecrawl";
26
+ /**
27
+ * Auto-chain admission: requires a credential so an unconfigured Firecrawl
28
+ * doesn't displace other providers that the user has set up with API keys.
29
+ */
26
30
  isAvailable(authStorage: AuthStorage): boolean;
31
+ /**
32
+ * Firecrawl supports keyless mode, so an explicit user selection
33
+ * (`webSearch: firecrawl`) works without any credential configured.
34
+ */
35
+ isExplicitlyAvailable(_authStorage: AuthStorage): boolean;
27
36
  search(params: SearchParams): Promise<SearchResponse>;
28
37
  }
@@ -54,7 +54,7 @@ export declare const SEARCH_PROVIDER_OPTIONS: readonly [{
54
54
  }, {
55
55
  readonly value: "firecrawl";
56
56
  readonly label: "Firecrawl";
57
- readonly description: "Requires FIRECRAWL_API_KEY";
57
+ readonly description: "Uses Firecrawl API when FIRECRAWL_API_KEY is set; falls back to keyless mode";
58
58
  }, {
59
59
  readonly value: "brave";
60
60
  readonly label: "Brave";
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": "17.0.8",
4
+ "version": "17.0.9",
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",
@@ -51,18 +51,19 @@
51
51
  "dependencies": {
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
+ "@babel/traverse": "^7.29.7",
54
55
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.0.8",
56
- "@oh-my-pi/omp-stats": "17.0.8",
57
- "@oh-my-pi/pi-agent-core": "17.0.8",
58
- "@oh-my-pi/pi-ai": "17.0.8",
59
- "@oh-my-pi/pi-catalog": "17.0.8",
60
- "@oh-my-pi/pi-mnemopi": "17.0.8",
61
- "@oh-my-pi/pi-natives": "17.0.8",
62
- "@oh-my-pi/pi-tui": "17.0.8",
63
- "@oh-my-pi/pi-utils": "17.0.8",
64
- "@oh-my-pi/pi-wire": "17.0.8",
65
- "@oh-my-pi/snapcompact": "17.0.8",
56
+ "@oh-my-pi/hashline": "17.0.9",
57
+ "@oh-my-pi/omp-stats": "17.0.9",
58
+ "@oh-my-pi/pi-agent-core": "17.0.9",
59
+ "@oh-my-pi/pi-ai": "17.0.9",
60
+ "@oh-my-pi/pi-catalog": "17.0.9",
61
+ "@oh-my-pi/pi-mnemopi": "17.0.9",
62
+ "@oh-my-pi/pi-natives": "17.0.9",
63
+ "@oh-my-pi/pi-tui": "17.0.9",
64
+ "@oh-my-pi/pi-utils": "17.0.9",
65
+ "@oh-my-pi/pi-wire": "17.0.9",
66
+ "@oh-my-pi/snapcompact": "17.0.9",
66
67
  "@opentelemetry/api": "^1.9.1",
67
68
  "@opentelemetry/api-logs": "^0.220.0",
68
69
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -19,7 +19,7 @@ const BUNDLED_PACKAGES: readonly BundledPackage[] = [
19
19
  { dir: "ai", identifier: "PiAi", rootShim: "legacy-pi-ai-shim.ts" },
20
20
  { dir: "coding-agent", identifier: "PiCodingAgent", rootShim: "legacy-pi-coding-agent-shim.ts" },
21
21
  { dir: "natives", identifier: "PiNatives", rootShim: null },
22
- { dir: "tui", identifier: "PiTui", rootShim: null },
22
+ { dir: "tui", identifier: "PiTui", rootShim: "legacy-pi-tui-shim.ts" },
23
23
  { dir: "utils", identifier: "PiUtils", rootShim: null },
24
24
  ];
25
25
 
@@ -41,7 +41,7 @@ export async function listGrievances(options: ListGrievancesOptions): Promise<vo
41
41
  console.log("[]");
42
42
  } else {
43
43
  console.log(
44
- chalk.dim("No grievances database found. Enable auto-QA with PI_AUTO_QA=1 or the dev.autoqa setting."),
44
+ chalk.dim("No grievances database found. Auto-QA has not recorded any reports yet (or was disabled)."),
45
45
  );
46
46
  }
47
47
  return;
@@ -110,7 +110,7 @@ export async function cleanGrievances(options: CleanGrievancesOptions): Promise<
110
110
  console.log(JSON.stringify({ deleted: 0 }));
111
111
  } else {
112
112
  console.log(
113
- chalk.dim("No grievances database found. Enable auto-QA with PI_AUTO_QA=1 or the dev.autoqa setting."),
113
+ chalk.dim("No grievances database found. Auto-QA has not recorded any reports yet (or was disabled)."),
114
114
  );
115
115
  }
116
116
  return;
@@ -409,7 +409,7 @@ function formatLimitLine(limit: UsageLimit, labelWidth: number, nowMs: number):
409
409
  const details: string[] = [describeAmount(limit)];
410
410
  const resetsAt = limit.window?.resetsAt;
411
411
  if (resetsAt !== undefined && resetsAt > nowMs) {
412
- details.push(`resets in ${formatDuration(resetsAt - nowMs)}`);
412
+ details.push(`${limit.window?.resetLabel ?? "resets"} in ${formatDuration(resetsAt - nowMs)}`);
413
413
  }
414
414
  const lines = [
415
415
  ` ${STATUS_COLOR[status]("●")} ${padded} ${renderBar(limit)} ${chalk.dim(details.join(" · "))}`,
@@ -0,0 +1,147 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
6
+ import { ModelRegistry } from "../model-registry";
7
+
8
+ /**
9
+ * Stub AuthStorage that satisfies the surface used by ModelRegistry's
10
+ * constructor (#loadModels → clearConfigApiKeys, constructor →
11
+ * setFallbackResolver). The awaiter under test never reaches auth-gated code
12
+ * paths, so no real credential store is required.
13
+ */
14
+ function createStubAuthStorage(): AuthStorage {
15
+ const stub = {
16
+ setFallbackResolver: () => {},
17
+ clearConfigApiKeys: () => {},
18
+ hasAuth: () => false,
19
+ };
20
+ return stub as unknown as AuthStorage;
21
+ }
22
+
23
+ describe("ModelRegistry.awaitBackgroundRefresh", () => {
24
+ let tmpDir: string;
25
+ let registry: ModelRegistry;
26
+
27
+ beforeEach(() => {
28
+ tmpDir = mkdtempSync(path.join(os.tmpdir(), "omp-reg-"));
29
+ // Construct with an explicit modelsPath inside the temp dir so the
30
+ // constructor's #loadModels read returns "not-found" rather than
31
+ // touching the host's ~/.omp/agent/models.yaml. isBunTestRuntime()
32
+ // auto-stubs #fetch in the constructor.
33
+ registry = new ModelRegistry(createStubAuthStorage(), path.join(tmpDir, "models.yaml"));
34
+ });
35
+
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ rmSync(tmpDir, { recursive: true, force: true });
39
+ });
40
+
41
+ test("resolves immediately when no background refresh is in flight", async () => {
42
+ // No refreshInBackground() called → #backgroundRefresh is undefined.
43
+ // The awaiter must settle within a single microtask, never hanging.
44
+ let settled = false;
45
+ const p = registry.awaitBackgroundRefresh().then(() => {
46
+ settled = true;
47
+ });
48
+ await Promise.resolve();
49
+ await p;
50
+ expect(settled).toBe(true);
51
+ });
52
+
53
+ test("blocks until the in-flight background refresh resolves, then resolves", async () => {
54
+ // Drive refreshInBackground with a controlled refresh() return value so
55
+ // #backgroundRefresh is captured but not yet settled.
56
+ const { promise, resolve } = Promise.withResolvers<void>();
57
+ const refreshSpy = vi.spyOn(registry, "refresh").mockReturnValue(promise);
58
+
59
+ registry.refreshInBackground();
60
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
61
+
62
+ let settled = false;
63
+ const awaitPromise = registry.awaitBackgroundRefresh().then(() => {
64
+ settled = true;
65
+ });
66
+
67
+ // Yield to the microtask queue: the awaiter must still be pending.
68
+ for (let i = 0; i < 5; i++) await Promise.resolve();
69
+ expect(settled).toBe(false);
70
+
71
+ resolve();
72
+
73
+ await awaitPromise;
74
+ expect(settled).toBe(true);
75
+ });
76
+
77
+ test("resolves even when the underlying refresh rejects (refreshInBackground swallows)", async () => {
78
+ // refreshInBackground wraps refresh() in .catch(...) so discovery errors
79
+ // never reach awaitBackgroundRefresh callers. The awaiter must resolve,
80
+ // not propagate the rejection.
81
+ const { promise, reject } = Promise.withResolvers<void>();
82
+ vi.spyOn(registry, "refresh").mockReturnValue(promise);
83
+
84
+ registry.refreshInBackground();
85
+
86
+ let settled = false;
87
+ const awaitPromise = registry.awaitBackgroundRefresh().then(() => {
88
+ settled = true;
89
+ });
90
+
91
+ reject(new Error("synthetic discovery failure"));
92
+
93
+ await awaitPromise;
94
+ expect(settled).toBe(true);
95
+ });
96
+
97
+ test("awaiter is a no-op after the in-flight refresh settles and clears #backgroundRefresh", async () => {
98
+ // Once refreshInBackground's promise resolves, #backgroundRefresh is
99
+ // cleared in the .finally. A subsequent awaitBackgroundRefresh must be
100
+ // an immediate no-op (microtask), not hang waiting for a stale promise
101
+ // or a second refresh that was never started.
102
+ const { promise, resolve } = Promise.withResolvers<void>();
103
+ vi.spyOn(registry, "refresh").mockReturnValue(promise);
104
+
105
+ registry.refreshInBackground();
106
+ resolve();
107
+ await registry.awaitBackgroundRefresh();
108
+
109
+ // Now #backgroundRefresh is cleared. A fresh await must resolve in a
110
+ // single microtask — measure by asserting it settles before a second
111
+ // microtask tick.
112
+ let settled = false;
113
+ const p = registry.awaitBackgroundRefresh().then(() => {
114
+ settled = true;
115
+ });
116
+ await Promise.resolve();
117
+ expect(settled).toBe(true);
118
+ await p;
119
+ });
120
+
121
+ test("refreshInBackground deduplicates: a second call while in-flight starts no new refresh", async () => {
122
+ // The guard `if (this.#backgroundRefresh) return` at the top of
123
+ // refreshInBackground prevents concurrent refreshes. A second call
124
+ // while the first is still pending must not invoke refresh() again.
125
+ const { promise, resolve } = Promise.withResolvers<void>();
126
+ const refreshSpy = vi.spyOn(registry, "refresh").mockReturnValue(promise);
127
+
128
+ registry.refreshInBackground();
129
+ registry.refreshInBackground();
130
+ registry.refreshInBackground();
131
+
132
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
133
+
134
+ resolve();
135
+ await registry.awaitBackgroundRefresh();
136
+
137
+ // After settle, #backgroundRefresh is cleared — a new call DOES start
138
+ // a fresh refresh.
139
+ const { promise: secondPromise, resolve: secondResolve } = Promise.withResolvers<void>();
140
+ refreshSpy.mockReturnValue(secondPromise);
141
+ registry.refreshInBackground();
142
+ expect(refreshSpy).toHaveBeenCalledTimes(2);
143
+
144
+ secondResolve();
145
+ await registry.awaitBackgroundRefresh();
146
+ });
147
+ });
@@ -869,6 +869,28 @@ export class ModelRegistry {
869
869
  this.#backgroundRefresh = refreshPromise;
870
870
  }
871
871
 
872
+ /**
873
+ * Wait for any in-flight background model discovery to settle.
874
+ *
875
+ * Background discovery started by {@link refreshInBackground} is
876
+ * fire-and-forget; RPC consumers (e.g. `get_available_models`,
877
+ * `set_model`) and deferred `--model` resolution that read the registry
878
+ * immediately after session creation can otherwise observe a partial
879
+ * catalog before discovery-backed providers have populated `#models`.
880
+ * Awaiting the tracked promise ensures the response reflects every
881
+ * configured provider once the initial background refresh resolves.
882
+ *
883
+ * No-op when no refresh is in flight (`#backgroundRefresh` cleared in the
884
+ * `finally` of `refreshInBackground` on completion). Resolves immediately
885
+ * in that case so already-warm sessions are unaffected. Discovery errors
886
+ * remain swallowed by `refreshInBackground`'s existing `.catch`.
887
+ */
888
+ async awaitBackgroundRefresh(): Promise<void> {
889
+ if (this.#backgroundRefresh) {
890
+ await this.#backgroundRefresh;
891
+ }
892
+ }
893
+
872
894
  async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
873
895
  this.#reloadStaticModels();
874
896
  for (const selector of this.#suppressedSelectors.keys()) {
@@ -2101,6 +2123,24 @@ export class ModelRegistry {
2101
2123
  .map(provider => provider.provider);
2102
2124
  }
2103
2125
 
2126
+ /**
2127
+ * Whether `providerId` is known to the registry: it has at least one live
2128
+ * model, or it is configured for dynamic discovery (models.yml `discovery:`
2129
+ * or a runtime extension provider) and is not disabled. Discovery-only
2130
+ * providers can hold zero models at startup — cached rows never persist
2131
+ * live auth headers (#5780), so a provider whose discovered models all
2132
+ * carry config headers (`authHeader: true`) only materializes models after
2133
+ * the online refresh completes.
2134
+ */
2135
+ hasProvider(providerId: string): boolean {
2136
+ if (this.#models.some(model => model.provider === providerId)) return true;
2137
+ if (getDisabledProviderIdsFromSettings().has(providerId)) return false;
2138
+ return (
2139
+ this.#discoverableProviders.some(provider => provider.provider === providerId) ||
2140
+ this.#runtimeModelManagers.has(providerId)
2141
+ );
2142
+ }
2143
+
2104
2144
  getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
2105
2145
  return this.#providerDiscoveryStates.get(provider);
2106
2146
  }
@@ -782,7 +782,6 @@ function parseModelPatternWithContext(
782
782
  options?: { allowInvalidThinkingSelectorFallback?: boolean },
783
783
  ): ParsedModelResult {
784
784
  // Exact match on the full pattern first (no fuzzy): a literal id that
785
- // contains a colon (`coding-router:max`) wins over any suffix split.
786
785
  const exactMatch = matchModel(pattern, availableModels, context, { exactOnly: true });
787
786
  if (exactMatch) {
788
787
  return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
@@ -207,6 +207,8 @@ interface UiNumber extends UiBase {
207
207
  }
208
208
 
209
209
  interface UiString extends UiBase {
210
+ /** Mask the value in both the settings row and text editor. */
211
+ secret?: boolean;
210
212
  /**
211
213
  * Submenu options.
212
214
  * - Array → submenu with these choices.
@@ -219,6 +221,7 @@ interface UiString extends UiBase {
219
221
  /** Wide ui shape exposed to consumers that walk the schema generically. */
220
222
  export type AnyUiMetadata = UiBase & {
221
223
  options?: ReadonlyArray<SubmenuOption> | "runtime";
224
+ secret?: boolean;
222
225
  };
223
226
 
224
227
  interface BooleanDef {
@@ -2753,7 +2756,18 @@ export const SETTINGS_SCHEMA = {
2753
2756
  },
2754
2757
  },
2755
2758
 
2756
- "hindsight.apiToken": { type: "string", default: undefined },
2759
+ "hindsight.apiToken": {
2760
+ type: "string",
2761
+ default: undefined,
2762
+ ui: {
2763
+ tab: "memory",
2764
+ group: "Hindsight",
2765
+ label: "Hindsight API Token",
2766
+ description: "Bearer token for authenticated Hindsight servers",
2767
+ condition: "hindsightActive",
2768
+ secret: true,
2769
+ },
2770
+ },
2757
2771
 
2758
2772
  "hindsight.bankId": {
2759
2773
  type: "string",
@@ -3945,6 +3959,17 @@ export const SETTINGS_SCHEMA = {
3945
3959
  },
3946
3960
  },
3947
3961
 
3962
+ "mcp.renderMarkdownResults": {
3963
+ type: "boolean",
3964
+ default: true,
3965
+ ui: {
3966
+ tab: "tools",
3967
+ group: "Discovery & MCP",
3968
+ label: "MCP Markdown Results",
3969
+ description: "Render non-JSON MCP text results as Markdown in the transcript",
3970
+ },
3971
+ },
3972
+
3948
3973
  "mcp.notifications": {
3949
3974
  type: "boolean",
3950
3975
  default: false,
@@ -4089,6 +4114,18 @@ export const SETTINGS_SCHEMA = {
4089
4114
  },
4090
4115
  },
4091
4116
 
4117
+ "task.isolation.apply": {
4118
+ type: "boolean",
4119
+ default: true,
4120
+ ui: {
4121
+ tab: "tasks",
4122
+ group: "Isolation",
4123
+ label: "Apply Isolated Changes",
4124
+ description:
4125
+ "Automatically apply successful isolated task changes to the parent checkout; disable to retain patch or branch artifacts",
4126
+ },
4127
+ },
4128
+
4092
4129
  "task.isolation.merge": {
4093
4130
  type: "enum",
4094
4131
  values: ["patch", "branch"] as const,
@@ -5032,12 +5069,13 @@ export const SETTINGS_SCHEMA = {
5032
5069
 
5033
5070
  "dev.autoqa": {
5034
5071
  type: "boolean",
5035
- default: false,
5072
+ default: true,
5036
5073
  ui: {
5037
5074
  tab: "tools",
5038
5075
  group: "Developer",
5039
5076
  label: "Auto QA",
5040
- description: "Enable automated tool issue reporting (report_tool_issue) for all agents",
5077
+ description:
5078
+ "Automated tool issue reporting (xd://report_issue). On by default; the first report asks for consent, and denying it disables reporting until re-enabled explicitly",
5041
5079
  },
5042
5080
  },
5043
5081
 
@@ -1362,4 +1362,5 @@ export function getPackageDir(): string {
1362
1362
 
1363
1363
  export * from "../index";
1364
1364
  export { formatBytes as formatSize } from "../tools/render-utils";
1365
+ export { copyToClipboard } from "../utils/clipboard";
1365
1366
  export { Type } from "./typebox";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Compatibility shim for legacy extensions importing the package root of
3
+ * `@earendil-works/pi-tui` or `@mariozechner/pi-tui`.
4
+ *
5
+ * The historical root exported `decodeKittyPrintable`; the canonical TUI now
6
+ * exposes the equivalent, broader `decodePrintableKey` helper. Keep the legacy
7
+ * name available without reintroducing it into the canonical package surface.
8
+ */
9
+ export * from "@oh-my-pi/pi-tui";
10
+ export { decodePrintableKey as decodeKittyPrintable } from "@oh-my-pi/pi-tui";