@oh-my-pi/pi-coding-agent 15.1.8 → 15.2.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 (71) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/types/cli/update-cli.d.ts +18 -0
  3. package/dist/types/config/settings-schema.d.ts +10 -0
  4. package/dist/types/eval/py/kernel.d.ts +6 -0
  5. package/dist/types/goals/state.d.ts +1 -1
  6. package/dist/types/goals/tools/goal-tool.d.ts +4 -0
  7. package/dist/types/hashline/parser.d.ts +6 -2
  8. package/dist/types/internal-urls/memory-protocol.d.ts +6 -0
  9. package/dist/types/main.d.ts +25 -1
  10. package/dist/types/modes/theme/shimmer.d.ts +27 -0
  11. package/dist/types/slash-commands/helpers/format.d.ts +4 -1
  12. package/dist/types/tools/ast-edit.d.ts +3 -0
  13. package/dist/types/tools/ast-grep.d.ts +3 -0
  14. package/dist/types/tools/find.d.ts +3 -0
  15. package/dist/types/tools/search.d.ts +3 -0
  16. package/dist/types/tui/file-list.d.ts +6 -0
  17. package/dist/types/tui/hyperlink.d.ts +42 -0
  18. package/dist/types/tui/index.d.ts +1 -0
  19. package/dist/types/utils/tool-choice.d.ts +2 -1
  20. package/dist/types/web/search/providers/utils.d.ts +27 -1
  21. package/package.json +7 -7
  22. package/src/cli/update-cli.ts +78 -36
  23. package/src/config/model-registry.ts +23 -12
  24. package/src/config/settings-schema.ts +12 -0
  25. package/src/config/settings.ts +28 -5
  26. package/src/edit/renderer.ts +5 -3
  27. package/src/eval/py/executor.ts +12 -1
  28. package/src/eval/py/kernel.ts +24 -8
  29. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  30. package/src/goals/runtime.ts +9 -3
  31. package/src/goals/state.ts +1 -1
  32. package/src/goals/tools/goal-tool.ts +12 -2
  33. package/src/hashline/diff.ts +1 -1
  34. package/src/hashline/execute.ts +2 -2
  35. package/src/hashline/parser.ts +87 -12
  36. package/src/internal-urls/memory-protocol.ts +1 -1
  37. package/src/main.ts +13 -2
  38. package/src/modes/interactive-mode.ts +29 -1
  39. package/src/modes/theme/shimmer.ts +79 -0
  40. package/src/prompts/agents/oracle.md +15 -16
  41. package/src/prompts/tools/goal.md +7 -2
  42. package/src/session/agent-session.ts +12 -75
  43. package/src/slash-commands/helpers/format.ts +23 -3
  44. package/src/task/executor.ts +115 -19
  45. package/src/tools/ast-edit.ts +39 -6
  46. package/src/tools/ast-grep.ts +38 -6
  47. package/src/tools/find.ts +13 -2
  48. package/src/tools/read.ts +46 -6
  49. package/src/tools/search.ts +447 -265
  50. package/src/tui/file-list.ts +10 -2
  51. package/src/tui/hyperlink.ts +126 -0
  52. package/src/tui/index.ts +1 -0
  53. package/src/utils/tool-choice.ts +7 -7
  54. package/src/web/kagi.ts +2 -2
  55. package/src/web/parallel.ts +3 -3
  56. package/src/web/search/index.ts +20 -9
  57. package/src/web/search/providers/anthropic.ts +4 -2
  58. package/src/web/search/providers/brave.ts +4 -2
  59. package/src/web/search/providers/codex.ts +4 -1
  60. package/src/web/search/providers/exa.ts +4 -1
  61. package/src/web/search/providers/gemini.ts +4 -1
  62. package/src/web/search/providers/jina.ts +4 -2
  63. package/src/web/search/providers/kagi.ts +5 -1
  64. package/src/web/search/providers/kimi.ts +4 -2
  65. package/src/web/search/providers/parallel.ts +5 -1
  66. package/src/web/search/providers/perplexity.ts +7 -2
  67. package/src/web/search/providers/searxng.ts +4 -1
  68. package/src/web/search/providers/synthetic.ts +4 -2
  69. package/src/web/search/providers/tavily.ts +4 -2
  70. package/src/web/search/providers/utils.ts +63 -1
  71. package/src/web/search/providers/zai.ts +4 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.2.1] - 2026-05-21
6
+
7
+ ### Fixed
8
+
9
+ - Fixed compaction routing to the wrong provider when `modelRoles.default` is set to a different model than the active chat. Auto- and manual compaction now prefer the active session's model and only fall back to role-based candidates when the current model has no usable credentials. Previously, an Anthropic chat with `modelRoles.default = openai/gpt-5` would compact through OpenAI (including the remote-compaction endpoint), even though the live conversation never used OpenAI.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed context overflow not being detected when the session's model (e.g. `hf:zai-org/GLM-5.1` via `synthetic` provider) returns a 400 with the upstream "400 status code (no body)" message wrapped inside a JSON envelope. The `isContextOverflow` check now matches the no-body status phrase anywhere in the error string rather than requiring it at the very start, so auto-compaction fires correctly instead of leaving the session silently stuck ([#1251](https://github.com/can1357/oh-my-pi/issues/1251)).
14
+ - Fixed `formatCapturedHttpError` not extracting the error text from `{"error":"string"}` response bodies (only object-valued `error` fields were previously handled), resulting in raw JSON in error messages instead of the human-readable string.
15
+
16
+ ## [15.2.0] - 2026-05-21
17
+
18
+ ### Changed
19
+
20
+ - Changed the interactive working loader and slash-command progress bars to use the active theme's accent shimmer instead of static muted text.
21
+
22
+ ### Fixed
23
+
24
+ - Fixed false-positive hashline `~ TEXT` separator-padding warning firing on YAML, JSON, Python, Markdown, TOML, and other indent-sensitive file edits. The padding check is now skipped entirely for indentation-sensitive extensions (`.py`, `.yml`/`.yaml`, `.md`/`.mdx`, `.json`/`.jsonc`/`.json5`, `.toml`, `.rst`, `.tf`, `.nix`, `.coffee`, `.haml`/`.slim`/`.pug`, `.sass`/`.styl`, `.nim`, `.cr`, `.elm`, `.fs`, …), and tightened on every other extension to flag only the `~ beta` typo shape (exactly one leading space before non-space content) rather than any leading-space payload.
25
+
26
+ ### Fixed
27
+
28
+ - Fixed goal state machine: `get` now returns paused goals (was returning null for `enabled=false`), `complete` now works on paused goals after interrupts, `create` is allowed after a previous goal reaches `complete` status, and the goal tool is re-added to the active tool set on session reload when a paused goal is persisted. Added `resume` and `drop` ops to the goal tool so the agent can re-engage or discard a paused goal without requiring user slash commands. ([#1249](https://github.com/can1357/oh-my-pi/issues/1249))
29
+
30
+ ## [15.1.9] - 2026-05-21
31
+
32
+ ### Fixed
33
+
34
+ - Fixed `disabledProviders` still probing local discovery endpoints for Ollama, llama.cpp, and LM Studio during background model refresh. Disabled providers are now excluded before implicit and built-in discovery managers are created. ([#1232](https://github.com/can1357/oh-my-pi/issues/1232))
35
+
36
+ ### Fixed
37
+
38
+ - Fixed `omp acp` auto-discovering host `.mcp.json` servers in parallel with the ACP client's `session/new.mcpServers`, which shadowed client-supplied MCP tools in `search_tool_bm25` and the session tool registry. The ACP session factory now forces `enableMCP: false`, so MCP ownership stays with `AcpAgent#configureMcpServers`. Non-ACP modes keep on-disk discovery. ([#1234](https://github.com/can1357/oh-my-pi/issues/1234))
39
+
40
+ ### Fixed
41
+
42
+ - Fixed binary `omp update` rollbacks so a downloaded replacement that fails post-install version verification no longer remains installed over the previous working binary. ([#1240](https://github.com/can1357/oh-my-pi/issues/1240))
43
+
44
+ ### Fixed
45
+
46
+ - Fixed `/force <tool>` rejecting Ollama/local models before the requested tool could run; Ollama now receives a named forced choice that the provider transport narrows to the selected tool. ([#1236](https://github.com/can1357/oh-my-pi/issues/1236))
47
+
48
+ ### Fixed
49
+
50
+ - Fixed `web_search` freezing the session when an upstream provider stalled. Bun's WinHTTP backend on Windows can silently drop `AbortSignal` once a TCP/TLS connection hangs (oven-sh/bun#15275, oven-sh/bun#18536), so Esc never reached the in-flight fetch and the only recovery was Ctrl+C + `omp --resume`. Every web-search provider's outbound `fetch` (anthropic, brave, codex, exa, gemini, jina, kagi, kimi, parallel, perplexity, searxng, synthetic, tavily, z.ai) now composes the caller signal with a 60s hard timeout via a shared `withHardTimeout` helper, guaranteeing the request settles within a minute even when Bun's abort fails to propagate. Independently, `executeSearch`'s provider-fallback loop was masking real cancellations as ordinary provider errors and returning "All web search providers failed"; it now re-throws as `ToolAbortError` the moment the caller's signal aborts, so the session sees a clean cancel on every platform. ([#1221](https://github.com/can1357/oh-my-pi/issues/1221))
51
+
52
+ ### Added
53
+
54
+ - Added OSC 8 terminal hyperlink support for file paths in tool output. When the terminal supports hyperlinks (kitty, Ghostty, WezTerm, iTerm2, Alacritty, VS Code) and the new `tui.hyperlinks` setting is `auto` (default) or `always`, OMP wraps file paths emitted by `read`, `find`, `search`, `edit`, `ast_grep`, and `ast_edit` renderers in `file:///abs/path` hyperlinks. `local://` and other fs-backed internal URLs resolve to their backing path. Set `tui.hyperlinks: off` to disable. ([#1244](https://github.com/can1357/oh-my-pi/issues/1244))
55
+
5
56
  ## [15.1.8] - 2026-05-20
6
57
 
7
58
  ### Fixed
@@ -8454,4 +8505,4 @@ Initial public release.
8454
8505
  - Git branch display in footer
8455
8506
  - Message queueing during streaming responses
8456
8507
  - OAuth integration for Gmail and Google Calendar access
8457
- - HTML export with syntax highlighting and collapsible sections
8508
+ - HTML export with syntax highlighting and collapsible sections
@@ -1,3 +1,17 @@
1
+ /** Result from running the installed binary and parsing its reported version. */
2
+ export interface InstalledVersionVerification {
3
+ ok: boolean;
4
+ actual?: string;
5
+ path?: string;
6
+ }
7
+ /** Paths and verifier used while replacing a downloaded binary update. */
8
+ export interface BinaryReplacementOptions {
9
+ targetPath: string;
10
+ tempPath: string;
11
+ backupPath: string;
12
+ expectedVersion: string;
13
+ verifyInstalledVersion: (expectedVersion: string) => Promise<InstalledVersionVerification>;
14
+ }
1
15
  /**
2
16
  * Parse update subcommand arguments.
3
17
  * Returns undefined if not an update command.
@@ -7,6 +21,10 @@ export declare function parseUpdateArgs(args: string[]): {
7
21
  check: boolean;
8
22
  } | undefined;
9
23
  export declare function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary";
24
+ /**
25
+ * Atomically replace the installed binary and roll back if version verification fails.
26
+ */
27
+ export declare function replaceBinaryForUpdate(options: BinaryReplacementOptions): Promise<InstalledVersionVerification>;
10
28
  /**
11
29
  * Run the update command.
12
30
  */
@@ -634,6 +634,16 @@ export declare const SETTINGS_SCHEMA: {
634
634
  readonly default: 20;
635
635
  readonly description: "Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).";
636
636
  };
637
+ readonly "tui.hyperlinks": {
638
+ readonly type: "enum";
639
+ readonly values: readonly ["off", "auto", "always"];
640
+ readonly default: "auto";
641
+ readonly ui: {
642
+ readonly tab: "appearance";
643
+ readonly label: "Terminal Hyperlinks";
644
+ readonly description: "Wrap file paths in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)";
645
+ };
646
+ };
637
647
  readonly "display.tabWidth": {
638
648
  readonly type: "number";
639
649
  readonly default: 3;
@@ -21,6 +21,12 @@ export interface KernelExecuteResult {
21
21
  cancelled: boolean;
22
22
  timedOut: boolean;
23
23
  stdinRequested: boolean;
24
+ /**
25
+ * True when the kernel subprocess was killed as part of settling this
26
+ * execution (e.g. SIGINT was ignored and we escalated to shutdown, or the
27
+ * kernel died unexpectedly). When false, the kernel remains reusable.
28
+ */
29
+ kernelKilled?: boolean;
24
30
  }
25
31
  export interface KernelShutdownResult {
26
32
  confirmed: boolean;
@@ -17,7 +17,7 @@ export interface GoalModeState {
17
17
  goal: Goal;
18
18
  }
19
19
  export interface GoalToolDetails {
20
- op: "create" | "get" | "complete";
20
+ op: "create" | "get" | "complete" | "resume" | "drop";
21
21
  goal?: Goal | null;
22
22
  remainingTokens?: number | null;
23
23
  completionBudgetReport?: string | null;
@@ -9,7 +9,9 @@ declare const goalSchema: z.ZodObject<{
9
9
  op: z.ZodEnum<{
10
10
  complete: "complete";
11
11
  create: "create";
12
+ drop: "drop";
12
13
  get: "get";
14
+ resume: "resume";
13
15
  }>;
14
16
  objective: z.ZodOptional<z.ZodString>;
15
17
  token_budget: z.ZodOptional<z.ZodNumber>;
@@ -32,7 +34,9 @@ export declare class GoalTool implements AgentTool<typeof goalSchema, GoalToolDe
32
34
  op: z.ZodEnum<{
33
35
  complete: "complete";
34
36
  create: "create";
37
+ drop: "drop";
35
38
  get: "get";
39
+ resume: "resume";
36
40
  }>;
37
41
  objective: z.ZodOptional<z.ZodString>;
38
42
  token_budget: z.ZodOptional<z.ZodNumber>;
@@ -1,7 +1,11 @@
1
1
  import type { HashlineCursor, HashlineEdit } from "./types";
2
2
  export declare function cloneCursor(cursor: HashlineCursor): HashlineCursor;
3
- export declare function parseHashline(diff: string): HashlineEdit[];
4
- export declare function parseHashlineWithWarnings(diff: string): {
3
+ export declare function parseHashline(diff: string, opts?: ParseHashlineOptions): HashlineEdit[];
4
+ export interface ParseHashlineOptions {
5
+ /** File path the diff targets. Used to suppress indent-sensitive false-positive warnings. */
6
+ path?: string;
7
+ }
8
+ export declare function parseHashlineWithWarnings(diff: string, opts?: ParseHashlineOptions): {
5
9
  edits: HashlineEdit[];
6
10
  warnings: string[];
7
11
  };
@@ -1,4 +1,10 @@
1
1
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
2
+ /**
3
+ * Snapshot of memory roots for every registered session, deduped.
4
+ * Each session has its own cwd (possibly a worktree), so subagents and main
5
+ * may see different roots.
6
+ */
7
+ export declare function memoryRootsFromRegistry(): string[];
2
8
  /**
3
9
  * Resolve a memory:// URL to an absolute filesystem path under memory root.
4
10
  */
@@ -5,16 +5,40 @@
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
7
  import type { Args } from "./cli/args";
8
+ import { ModelRegistry } from "./config/model-registry";
8
9
  import { Settings } from "./config/settings";
9
10
  import { InteractiveMode, runAcpMode } from "./modes";
10
11
  import type { SubmittedUserInput } from "./modes/types";
11
- import { createAgentSession, discoverAuthStorage } from "./sdk";
12
+ import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession, discoverAuthStorage } from "./sdk";
12
13
  import type { AgentSession } from "./session/agent-session";
14
+ import type { AuthStorage } from "./session/auth-storage";
13
15
  export interface InteractiveModeNotify {
14
16
  kind: "warn" | "error" | "info";
15
17
  message: string;
16
18
  }
17
19
  export declare function submitInteractiveInput(mode: Pick<InteractiveMode, "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError" | "checkShutdownRequested">, session: Pick<AgentSession, "prompt" | "promptCustomMessage">, input: SubmittedUserInput): Promise<void>;
20
+ type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
21
+ export interface AcpSessionFactoryOptions {
22
+ baseOptions: CreateAgentSessionOptions;
23
+ settings: Settings;
24
+ sessionDir?: string;
25
+ authStorage: AuthStorage;
26
+ modelRegistry: ModelRegistry;
27
+ parsedArgs: Pick<Args, "apiKey">;
28
+ rawArgs: string[];
29
+ createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
30
+ }
31
+ /**
32
+ * Build the per-`session/new` factory used by ACP mode.
33
+ *
34
+ * MCP servers in ACP sessions are owned exclusively by the ACP client, which
35
+ * supplies them through `session/new.mcpServers` and re-applies them via
36
+ * {@link AcpAgent#configureMcpServers}. We therefore force `enableMCP: false`
37
+ * on every session created here so {@link createAgentSession} skips the on-disk
38
+ * `.mcp.json` discovery path — otherwise host MCP tools land in the session's
39
+ * tool registry and shadow the client-supplied servers (issue #1234).
40
+ */
41
+ export declare function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSessionFactory;
18
42
  interface RunRootCommandDependencies {
19
43
  createAgentSession?: typeof createAgentSession;
20
44
  discoverAuthStorage?: typeof discoverAuthStorage;
@@ -0,0 +1,27 @@
1
+ import type { Theme, ThemeColor } from "./theme";
2
+ type ShimmerTheme = Pick<Theme, "bold" | "fg">;
3
+ /** Three-tier color stack a shimmer character cycles through as the band sweeps. */
4
+ export interface ShimmerPalette {
5
+ /** Color for chars outside / at the edge of the band (intensity < 0.2). */
6
+ low: ThemeColor;
7
+ /** Color for chars approaching the crest (0.2 <= intensity < 0.6). */
8
+ mid: ThemeColor;
9
+ /** Color at the band's crest (intensity >= 0.6). */
10
+ high: ThemeColor;
11
+ /** Whether to bold the crest tier. Default `false`. */
12
+ bold?: boolean;
13
+ }
14
+ /** One run of text that shares a palette inside a larger shimmer sweep. */
15
+ export interface ShimmerSegment {
16
+ text: string;
17
+ palette?: ShimmerPalette;
18
+ }
19
+ export declare const DEFAULT_SHIMMER_PALETTE: ShimmerPalette;
20
+ /**
21
+ * Apply a shimmer sweep across one or more segments, treating them as a single
22
+ * continuous string for band positioning. Each segment can supply its own
23
+ * palette so the gradient stays in lockstep while the colors differ.
24
+ */
25
+ export declare function shimmerSegments(segments: readonly ShimmerSegment[], theme: ShimmerTheme): string;
26
+ export declare function shimmerText(text: string, theme: ShimmerTheme, palette?: ShimmerPalette): string;
27
+ export {};
@@ -1,7 +1,10 @@
1
+ import { type Theme } from "../../modes/theme/theme";
1
2
  /** Format a millisecond duration as a coarse-grained human label. */
2
3
  export declare function formatDuration(ms: number): string;
4
+ type ProgressBarTheme = Pick<Theme, "bold" | "fg">;
3
5
  /**
4
6
  * Render an ASCII progress bar with a trailing percent label.
5
7
  * `fraction` is clamped to `[0, 1]`. `undefined` renders a dotted placeholder.
6
8
  */
7
- export declare function renderAsciiBar(fraction: number | undefined, width?: number): string;
9
+ export declare function renderAsciiBar(fraction: number | undefined, width?: number, uiTheme?: ProgressBarTheme): string;
10
+ export {};
@@ -31,6 +31,9 @@ export interface AstEditToolDetails {
31
31
  /** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
32
32
  * a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
33
33
  displayContent?: string;
34
+ /** Absolute base directory used during the edit. Used by the renderer to resolve
35
+ * display-relative paths to absolute paths for OSC 8 hyperlinks. */
36
+ searchPath?: string;
34
37
  }
35
38
  export declare class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
36
39
  private readonly session;
@@ -28,6 +28,9 @@ export interface AstGrepToolDetails {
28
28
  /** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
29
29
  * a `│` gutter and `*` to mark match lines. The TUI uses this directly so it never parses model-facing text. */
30
30
  displayContent?: string;
31
+ /** Absolute base directory used during search. Used by the renderer to resolve
32
+ * display-relative paths to absolute paths for OSC 8 hyperlinks. */
33
+ searchPath?: string;
31
34
  }
32
35
  export declare class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
33
36
  private readonly session;
@@ -23,6 +23,9 @@ export interface FindToolDetails {
23
23
  files?: string[];
24
24
  truncated?: boolean;
25
25
  error?: string;
26
+ /** Working directory at search time. Used by the renderer to resolve relative
27
+ * file paths to absolute paths for OSC 8 hyperlinks. */
28
+ cwd?: string;
26
29
  /** User-supplied paths whose base directory was missing on disk. The tool
27
30
  * skipped these and continued with the surviving entries; surfaced as a
28
31
  * non-fatal warning in the renderer and in the model-facing text. */
@@ -43,6 +43,9 @@ export interface SearchToolDetails {
43
43
  * `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs space for
44
44
  * context). The TUI uses this directly so it never parses model-facing hashline anchors. */
45
45
  displayContent?: string;
46
+ /** Absolute base directory used during search. Used by the renderer to resolve
47
+ * display-relative paths to absolute paths for OSC 8 hyperlinks. */
48
+ searchPath?: string;
46
49
  /** User-supplied paths whose base directory was missing on disk. The tool
47
50
  * skipped these and continued with the surviving entries; surfaced as a
48
51
  * non-fatal warning in the renderer and in the model-facing text. */
@@ -4,6 +4,9 @@
4
4
  import type { Theme } from "../modes/theme/theme";
5
5
  export interface FileEntry {
6
6
  path: string;
7
+ /** Absolute filesystem path. When provided together with {@link FileListOptions.hyperlinkFn}, the
8
+ * rendered path text is wrapped in an OSC 8 hyperlink. */
9
+ absPath?: string;
7
10
  isDirectory?: boolean;
8
11
  meta?: string;
9
12
  }
@@ -12,5 +15,8 @@ export interface FileListOptions {
12
15
  expanded?: boolean;
13
16
  maxCollapsed?: number;
14
17
  showIcons?: boolean;
18
+ /** When provided, called with the entry's absolute path and the ANSI-styled display string to
19
+ * optionally wrap the path in an OSC 8 hyperlink. Only invoked when {@link FileEntry.absPath} is set. */
20
+ hyperlinkFn?: (absPath: string, displayText: string) => string;
15
21
  }
16
22
  export declare function renderFileList(options: FileListOptions, theme: Theme): string[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Returns true when OSC 8 hyperlinks should be emitted.
3
+ *
4
+ * Respects `tui.hyperlinks` setting:
5
+ * - `"off"`: never
6
+ * - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support
7
+ * - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
8
+ */
9
+ export declare function isHyperlinkEnabled(): boolean;
10
+ /**
11
+ * Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
12
+ *
13
+ * Returns `displayText` unchanged when hyperlinks are disabled or when
14
+ * the text already contains an OSC 8 sequence (prevents double-wrapping).
15
+ *
16
+ * The caller is responsible for passing an absolute path. Relative paths
17
+ * produce invalid `file://` URIs and are accepted silently to avoid runtime
18
+ * errors in renderer hot paths.
19
+ *
20
+ * @param absPath - Absolute filesystem path
21
+ * @param displayText - Text to render as the hyperlink anchor (may contain ANSI codes)
22
+ * @param opts - Optional line/col position appended as `?line=N&col=M` query params
23
+ */
24
+ export declare function fileHyperlink(absPath: string, displayText: string, opts?: {
25
+ line?: number;
26
+ col?: number;
27
+ }): string;
28
+ /**
29
+ * Synchronously resolve a filesystem-backed internal URL (e.g. `local://foo.md`,
30
+ * `memory://root/notes.md`) to its absolute filesystem path. Returns `undefined`
31
+ * for inputs that aren't fs-backed, aren't resolvable in the current session
32
+ * registry, or fail to parse.
33
+ *
34
+ * Used by renderers to wrap fs-backed internal URLs in OSC 8 hyperlinks even
35
+ * when the resolved path isn't yet available from tool result details (e.g.
36
+ * during the call/streaming phase before a result lands).
37
+ *
38
+ * Async-resolved schemes (`artifact://`, `agent://`, `skill://`, `rule://`,
39
+ * `omp://`) are not handled here — those rely on `details.resolvedPath` set
40
+ * by the read tool's router resolution.
41
+ */
42
+ export declare function tryResolveInternalUrlSync(input: string): string | undefined;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export * from "./code-cell";
5
5
  export * from "./file-list";
6
+ export * from "./hyperlink";
6
7
  export * from "./output-block";
7
8
  export * from "./status-line";
8
9
  export * from "./tree-list";
@@ -1,6 +1,7 @@
1
1
  import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
2
2
  /**
3
3
  * Build a provider-aware tool choice that targets one specific tool when supported.
4
- * Some providers only support "any tool" forcing, not a named tool.
4
+ * Providers that only expose required/any forcing may still honor named choices by
5
+ * narrowing their request tool list before transport.
5
6
  */
6
7
  export declare function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined;
@@ -1,4 +1,4 @@
1
- import type { SearchSource } from "../../../web/search/types";
1
+ import { SearchProviderError, type SearchProviderId, type SearchSource } from "../../../web/search/types";
2
2
  /**
3
3
  * Search for an API credential by checking an env-derived key first,
4
4
  * then falling back to agent.db stored credentials for the given providers.
@@ -12,6 +12,31 @@ export declare function findCredential(envKey: string | null | undefined, ...sto
12
12
  * Swallows lookup errors and reports unavailability.
13
13
  */
14
14
  export declare function isApiKeyAvailable(findApiKey: () => string | null | Promise<string | null>): Promise<boolean>;
15
+ /**
16
+ * Default hard ceiling for a single web-search round-trip. 60s tolerates
17
+ * legitimate slow LLM-mediated responses (anthropic web_search_20250305,
18
+ * perplexity, gemini, codex) while still guaranteeing the session unfreezes
19
+ * within a minute if Bun's `AbortSignal` fails to propagate on Windows.
20
+ *
21
+ * Pure search APIs (brave, exa, jina, tavily, searxng, synthetic, zai)
22
+ * settle far faster in practice; reusing the same ceiling keeps the wiring
23
+ * uniform without compromising correctness.
24
+ */
25
+ export declare const SEARCH_HARD_TIMEOUT_MS = 60000;
26
+ /**
27
+ * Compose a caller-supplied {@link AbortSignal} with a hard timeout so an
28
+ * outbound `fetch()` is guaranteed to settle within `ms` even when the
29
+ * runtime fails to propagate cancellation to the underlying transport.
30
+ *
31
+ * Bun's WinHTTP backend on Windows is known to ignore `AbortSignal` once a
32
+ * TCP/TLS connection stalls (oven-sh/bun#15275, oven-sh/bun#18536); without
33
+ * this safety net a stalled web-search request freezes the entire session
34
+ * because the user's Esc is never delivered to the native layer.
35
+ *
36
+ * @param signal - Caller cancellation signal, if any.
37
+ * @param ms - Hard timeout in milliseconds. Defaults to {@link SEARCH_HARD_TIMEOUT_MS}.
38
+ */
39
+ export declare function withHardTimeout(signal: AbortSignal | undefined, ms?: number): AbortSignal;
15
40
  /**
16
41
  * Map a provider's raw source list to the unified SearchSource shape,
17
42
  * clamped to the requested result count and annotated with ageSeconds.
@@ -22,3 +47,4 @@ export declare function toSearchSources(sources: ReadonlyArray<{
22
47
  snippet?: string;
23
48
  publishedDate?: string;
24
49
  }>, numResults: number): SearchSource[];
50
+ export declare function classifyProviderHttpError(provider: SearchProviderId, status: number, body: string): SearchProviderError | null;
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": "15.1.8",
4
+ "version": "15.2.1",
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,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "15.1.8",
51
- "@oh-my-pi/pi-agent-core": "15.1.8",
52
- "@oh-my-pi/pi-ai": "15.1.8",
53
- "@oh-my-pi/pi-natives": "15.1.8",
54
- "@oh-my-pi/pi-tui": "15.1.8",
55
- "@oh-my-pi/pi-utils": "15.1.8",
50
+ "@oh-my-pi/omp-stats": "15.2.1",
51
+ "@oh-my-pi/pi-agent-core": "15.2.1",
52
+ "@oh-my-pi/pi-ai": "15.2.1",
53
+ "@oh-my-pi/pi-natives": "15.2.1",
54
+ "@oh-my-pi/pi-tui": "15.2.1",
55
+ "@oh-my-pi/pi-utils": "15.2.1",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@types/turndown": "5.0.6",
58
58
  "@xterm/headless": "^6.0.0",
@@ -20,6 +20,22 @@ interface ReleaseInfo {
20
20
  version: string;
21
21
  }
22
22
 
23
+ /** Result from running the installed binary and parsing its reported version. */
24
+ export interface InstalledVersionVerification {
25
+ ok: boolean;
26
+ actual?: string;
27
+ path?: string;
28
+ }
29
+
30
+ /** Paths and verifier used while replacing a downloaded binary update. */
31
+ export interface BinaryReplacementOptions {
32
+ targetPath: string;
33
+ tempPath: string;
34
+ backupPath: string;
35
+ expectedVersion: string;
36
+ verifyInstalledVersion: (expectedVersion: string) => Promise<InstalledVersionVerification>;
37
+ }
38
+
23
39
  /**
24
40
  * Parse update subcommand arguments.
25
41
  * Returns undefined if not an update command.
@@ -197,9 +213,7 @@ function resolveOmpPath(): string | undefined {
197
213
  /**
198
214
  * Run the resolved omp binary and check if it reports the expected version.
199
215
  */
200
- async function verifyInstalledVersion(
201
- expectedVersion: string,
202
- ): Promise<{ ok: boolean; actual?: string; path?: string }> {
216
+ async function verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification> {
203
217
  const ompPath = resolveOmpPath();
204
218
  if (!ompPath) return { ok: false };
205
219
  try {
@@ -215,29 +229,69 @@ async function verifyInstalledVersion(
215
229
  }
216
230
  }
217
231
 
232
+ function printVerifiedVersion(expectedVersion: string): void {
233
+ console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
234
+ }
235
+
236
+ function formatVerificationFailure(result: InstalledVersionVerification, expectedVersion: string): string {
237
+ if (result.actual) {
238
+ return `${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`;
239
+ }
240
+ return `could not verify updated version${result.path ? ` at ${result.path}` : ""}`;
241
+ }
242
+
218
243
  /**
219
244
  * Print post-update verification result.
220
245
  */
221
246
  async function printVerification(expectedVersion: string): Promise<void> {
222
247
  const result = await verifyInstalledVersion(expectedVersion);
223
248
  if (result.ok) {
224
- console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
249
+ printVerifiedVersion(expectedVersion);
225
250
  return;
226
251
  }
227
- if (result.actual) {
228
- console.log(
229
- chalk.yellow(
230
- `\nWarning: ${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`,
231
- ),
232
- );
233
- } else {
234
- console.log(
235
- chalk.yellow(`\nWarning: could not verify updated version${result.path ? ` at ${result.path}` : ""}`),
236
- );
237
- }
252
+ console.log(chalk.yellow(`\nWarning: ${formatVerificationFailure(result, expectedVersion)}`));
238
253
  console.log(chalk.yellow(`You may need to reinstall: curl -fsSL https://omp.sh/install | sh`));
239
254
  }
240
255
 
256
+ async function unlinkIfExists(filePath: string): Promise<void> {
257
+ try {
258
+ await fs.promises.unlink(filePath);
259
+ } catch (err) {
260
+ if (!isEnoent(err)) throw err;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Atomically replace the installed binary and roll back if version verification fails.
266
+ */
267
+ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions): Promise<InstalledVersionVerification> {
268
+ let backupReady = false;
269
+ try {
270
+ await unlinkIfExists(options.backupPath);
271
+ await fs.promises.rename(options.targetPath, options.backupPath);
272
+ backupReady = true;
273
+ await fs.promises.rename(options.tempPath, options.targetPath);
274
+
275
+ const verification = await options.verifyInstalledVersion(options.expectedVersion);
276
+ if (!verification.ok) {
277
+ throw new Error(
278
+ `${formatVerificationFailure(verification, options.expectedVersion)}; restored previous ${APP_NAME} binary`,
279
+ );
280
+ }
281
+
282
+ backupReady = false;
283
+ await unlinkIfExists(options.backupPath);
284
+ return verification;
285
+ } catch (err) {
286
+ if (backupReady) {
287
+ await unlinkIfExists(options.targetPath);
288
+ await fs.promises.rename(options.backupPath, options.targetPath);
289
+ }
290
+ await unlinkIfExists(options.tempPath);
291
+ throw err;
292
+ }
293
+ }
294
+
241
295
  /**
242
296
  * Update via bun package manager.
243
297
  */
@@ -271,27 +325,15 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
271
325
  await pipeline(response.body, fileStream);
272
326
 
273
327
  console.log(chalk.dim("Installing update..."));
274
- try {
275
- try {
276
- await fs.promises.unlink(backupPath);
277
- } catch (err) {
278
- if (!isEnoent(err)) throw err;
279
- }
280
- await fs.promises.rename(targetPath, backupPath);
281
- await fs.promises.rename(tempPath, targetPath);
282
- await fs.promises.unlink(backupPath);
283
-
284
- await printVerification(expectedVersion);
285
- console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
286
- } catch (err) {
287
- if (fs.existsSync(backupPath) && !fs.existsSync(targetPath)) {
288
- await fs.promises.rename(backupPath, targetPath);
289
- }
290
- if (fs.existsSync(tempPath)) {
291
- await fs.promises.unlink(tempPath);
292
- }
293
- throw err;
294
- }
328
+ await replaceBinaryForUpdate({
329
+ targetPath,
330
+ tempPath,
331
+ backupPath,
332
+ expectedVersion,
333
+ verifyInstalledVersion,
334
+ });
335
+ printVerifiedVersion(expectedVersion);
336
+ console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
295
337
  }
296
338
 
297
339
  /**