@oh-my-pi/pi-coding-agent 16.1.3 → 16.1.4

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.
package/src/sdk.ts CHANGED
@@ -687,6 +687,37 @@ export async function loadSessionExtensions(
687
687
  return result;
688
688
  }
689
689
 
690
+ /**
691
+ * Load discovered/configured extensions and register their providers into
692
+ * `modelRegistry`, then discover the dynamic provider catalogs. One-shot CLIs
693
+ * (`omp bench`, dry-balance) build a bare {@link ModelRegistry} that only knows
694
+ * built-in catalog providers; without this, providers contributed by an
695
+ * extension (e.g. a custom OpenAI-compatible provider under
696
+ * `~/.omp/agent/extensions/`) never reach model resolution. Mirrors the
697
+ * session / `omp models` path: drain the queued provider registrations, then
698
+ * `refreshRuntimeProviders` so dynamically-discovered models exist before
699
+ * selectors are resolved.
700
+ */
701
+ export async function loadCliExtensionProviders(
702
+ modelRegistry: ModelRegistry,
703
+ settings: Settings,
704
+ cwd: string,
705
+ options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths"> = {},
706
+ ): Promise<void> {
707
+ const eventBus = new EventBus();
708
+ const extensionsResult = await loadSessionExtensions(options, cwd, settings, eventBus);
709
+ const activeSources = extensionsResult.extensions.map(extension => extension.path);
710
+ modelRegistry.syncExtensionSources(activeSources);
711
+ for (const sourceId of new Set(activeSources)) {
712
+ modelRegistry.clearSourceRegistrations(sourceId);
713
+ }
714
+ for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
715
+ modelRegistry.registerProvider(name, config, sourceId);
716
+ }
717
+ extensionsResult.runtime.pendingProviderRegistrations = [];
718
+ await modelRegistry.refreshRuntimeProviders();
719
+ }
720
+
690
721
  /**
691
722
  * Discover skills from cwd and agentDir.
692
723
  */
@@ -1518,6 +1549,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1518
1549
  timestamp: Date.now(),
1519
1550
  }),
1520
1551
  peekQueueInvoker: () => session.peekQueueInvoker(),
1552
+ peekPendingInvoker: () => session.peekPendingInvoker(),
1553
+ clearPendingInvokers: () => session.clearPendingInvokers(),
1521
1554
  peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
1522
1555
  setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
1523
1556
  allocateOutputArtifact: async toolType => {
@@ -2181,6 +2181,11 @@ export class AgentSession {
2181
2181
  return this.#toolChoiceQueue.peekPendingInvoker();
2182
2182
  }
2183
2183
 
2184
+ /** Clear stale non-forcing pending preview invokers after `resolve` proves none can run. */
2185
+ clearPendingInvokers(): void {
2186
+ this.#toolChoiceQueue.clearPendingInvokers();
2187
+ }
2188
+
2184
2189
  /**
2185
2190
  * Force the next model call to target a specific active tool, then terminate
2186
2191
  * the agent loop. Pushes a two-step sequence [forced, "none"] so the model
@@ -231,6 +231,12 @@ export class ToolChoiceQueue {
231
231
  this.#pendingInvokers = this.#pendingInvokers.filter(p => p.id !== id);
232
232
  }
233
233
 
234
+ /** Drop every pending preview invoker without touching hard tool-choice directives. */
235
+ clearPendingInvokers(): void {
236
+ if (this.#pendingInvokers.length === 0) return;
237
+ this.#pendingInvokers = [];
238
+ }
239
+
234
240
  /** True when at least one non-forcing pending preview is registered. */
235
241
  get hasPendingInvoker(): boolean {
236
242
  return this.#pendingInvokers.length > 0;
@@ -316,6 +316,8 @@ export interface ToolSession {
316
316
  * tool dispatches to it so a staged preview resolves WITHOUT forcing tool_choice — the
317
317
  * agent-loop's SoftToolRequirement lifecycle owns reminder injection and escalation. */
318
318
  peekPendingInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
319
+ /** Clear stale pending preview markers when `resolve` cannot dispatch them. */
320
+ clearPendingInvokers?(): void;
319
321
  /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
320
322
  * Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
321
323
  * letting modes accept `resolve` invocations without forcing the tool choice every turn. */
@@ -212,6 +212,7 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
212
212
  this.session.peekPendingInvoker?.() ??
213
213
  this.session.peekStandingResolveHandler?.();
214
214
  if (!invoker) {
215
+ this.session.clearPendingInvokers?.();
215
216
  // `discard` is a request to cancel/abort a staged action. When nothing is
216
217
  // pending, the desired end-state (no staged change) already holds, so honor
217
218
  // it as a successful cancellation instead of surfacing a hard error to the
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import * as url from "node:url";
9
9
  import { TERMINAL } from "@oh-my-pi/pi-tui";
10
- import { settings } from "../config/settings";
10
+ import { isSettingsInitialized, settings } from "../config/settings";
11
11
  import {
12
12
  LocalProtocolHandler,
13
13
  memoryRootsFromRegistry,
@@ -45,8 +45,10 @@ function buildFileUri(filePath: string, opts?: { line?: number; col?: number }):
45
45
  * - `"off"`: never
46
46
  * - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support
47
47
  * - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
48
+ * Before settings initialization, returns false so early render paths stay plain text.
48
49
  */
49
50
  export function isHyperlinkEnabled(): boolean {
51
+ if (!isSettingsInitialized()) return false;
50
52
  const mode = settings.get("tui.hyperlinks");
51
53
  if (mode === "off") return false;
52
54
  if (mode === "always") return true;
@@ -104,10 +106,11 @@ export function urlHyperlink(url: string, displayText: string): string {
104
106
  * Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL,
105
107
  * bypassing terminal capability auto-detection. Used for auth prompts where
106
108
  * an inert "click" label blocks login on terminals whose capabilities are
107
- * not advertised. Still returns plain text when the user has explicitly
108
- * opted out via `tui.hyperlinks=off`.
109
+ * not advertised. Still returns plain text before settings initialization or
110
+ * when the user has explicitly opted out via `tui.hyperlinks=off`.
109
111
  */
110
112
  export function urlHyperlinkAlways(url: string, displayText: string): string {
113
+ if (!isSettingsInitialized()) return displayText;
111
114
  if (settings.get("tui.hyperlinks") === "off") return displayText;
112
115
  const normalized = url.match(/^www\./i) ? `https://${url}` : url;
113
116
  try {