@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.5

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 (63) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/dist/types/cli/classify-install-target.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +13 -4
  4. package/dist/types/modes/components/assistant-message.d.ts +11 -0
  5. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  6. package/dist/types/modes/components/error-banner.d.ts +11 -0
  7. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  8. package/dist/types/modes/components/transcript-container.d.ts +1 -0
  9. package/dist/types/modes/components/user-message.d.ts +1 -1
  10. package/dist/types/modes/image-references.d.ts +17 -0
  11. package/dist/types/modes/interactive-mode.d.ts +7 -0
  12. package/dist/types/modes/types.d.ts +7 -0
  13. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  14. package/dist/types/session/blob-store.d.ts +12 -11
  15. package/dist/types/session/session-manager.d.ts +5 -3
  16. package/dist/types/system-prompt.d.ts +2 -0
  17. package/dist/types/tiny/title-client.d.ts +16 -1
  18. package/dist/types/tool-discovery/mode.d.ts +8 -0
  19. package/dist/types/tools/archive-reader.d.ts +5 -1
  20. package/dist/types/tui/hyperlink.d.ts +12 -0
  21. package/dist/types/web/search/render.d.ts +1 -2
  22. package/package.json +9 -9
  23. package/src/cli/classify-install-target.ts +31 -5
  24. package/src/cli/plugin-cli.ts +45 -0
  25. package/src/cli/web-search-cli.ts +0 -1
  26. package/src/config/model-registry.ts +54 -4
  27. package/src/config/settings-schema.ts +14 -4
  28. package/src/eval/__tests__/agent-bridge.test.ts +72 -0
  29. package/src/eval/py/tool-bridge.ts +43 -5
  30. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
  31. package/src/internal-urls/docs-index.generated.ts +3 -3
  32. package/src/main.ts +7 -1
  33. package/src/modes/components/assistant-message.ts +22 -0
  34. package/src/modes/components/custom-editor.ts +14 -2
  35. package/src/modes/components/error-banner.ts +33 -0
  36. package/src/modes/components/tool-execution.ts +44 -0
  37. package/src/modes/components/transcript-container.ts +93 -32
  38. package/src/modes/components/user-message.ts +9 -2
  39. package/src/modes/controllers/event-controller.ts +42 -3
  40. package/src/modes/controllers/input-controller.ts +33 -1
  41. package/src/modes/image-references.ts +111 -0
  42. package/src/modes/interactive-mode.ts +48 -13
  43. package/src/modes/types.ts +10 -1
  44. package/src/modes/utils/ui-helpers.ts +23 -2
  45. package/src/prompts/ci-green-request.md +5 -3
  46. package/src/prompts/system/project-prompt.md +1 -0
  47. package/src/sdk.ts +17 -9
  48. package/src/session/agent-session.ts +37 -12
  49. package/src/session/blob-store.ts +96 -9
  50. package/src/session/session-manager.ts +19 -10
  51. package/src/system-prompt.ts +4 -0
  52. package/src/tiny/title-client.ts +7 -1
  53. package/src/tool-discovery/mode.ts +24 -0
  54. package/src/tools/archive-reader.ts +339 -31
  55. package/src/tools/fetch.ts +29 -9
  56. package/src/tools/gh.ts +65 -11
  57. package/src/tools/index.ts +6 -8
  58. package/src/tools/read.ts +58 -12
  59. package/src/tools/search-tool-bm25.ts +4 -6
  60. package/src/tools/search.ts +60 -11
  61. package/src/tui/hyperlink.ts +42 -7
  62. package/src/web/search/index.ts +2 -2
  63. package/src/web/search/render.ts +20 -52
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.9.5] - 2026-06-05
6
+ ### Added
7
+
8
+ - Added a persistent error banner pinned above the editor when an assistant turn ends on a provider error (e.g. Anthropic's "Output blocked by content filtering policy"). The transcript `Error: …` line scrolls away as the conversation grows, so terminal turns that ended on a stream error could pass unnoticed; the banner stays in the fixed region above the input and is cleared when the next turn starts.
9
+
10
+ - Added bold, underlined, clickable `[Image #N]` placeholders in the draft editor and sent user-message bubbles, backed by extension-bearing blob-store sidecar files so terminal `file://` links open in image viewers.
11
+ - Added the active model identifier (`provider/id`) to the system prompt's `<workstation>` block so the agent knows which model it is running as. Gated by the new `includeModelInPrompt` setting (default on); the base prompt is rebuilt on a mid-session model switch so the surfaced identifier stays current.
12
+ - Added `OLLAMA_HOST` support for implicit local Ollama discovery when `OLLAMA_BASE_URL` is unset, so OMP picks up the same host setting used by Ollama.
13
+ - Added `OLLAMA_CONTEXT_LENGTH` as a positive-integer context-window override for implicit local Ollama discovery, so users can correct OMP context budgeting without writing per-model overrides.
14
+
15
+ ### Changed
16
+
17
+ - Changed `tools.discoveryMode` to default to `auto`, which keeps discovery off for small tool sets and automatically switches to MCP-only tool discovery when more than 40 tools are registered.
18
+
19
+ ### Fixed
20
+
21
+ - Fixed user-message rendering to materialize image links from embedded image blocks when rebuilding chat output, so image placeholders remain clickable after replayed or restored messages
22
+ - Fixed queued/steering user messages carrying a pasted image rendering out of order — sometimes dropping the user bubble *below* the very tool output it was sent to steer. `EventController.#handleMessageStart` awaited async image-link materialization between the user `message_start` and `addMessageToChat`; since `AgentSession.#emit` dispatches TUI listeners fire-and-forget, that mid-handler yield let the next synchronously-handled events (assistant `message_start`, tool execution start/end) append their components first, scrambling transcript order and live-region block boundaries. The bubble is now appended synchronously, with clickable image links still materialized via the synchronous blob-store fallback.
23
+ - Fixed tool execution cards to finalize promptly when a turn is abandoned or completed so stale streaming previews and frozen spinner frames no longer keep transcript rows in the live region
24
+ - Fixed `read` and `search` TUI rendering to emit OSC 8 hyperlinks for HTTP URLs, `local://` resources backed by files, and filesystem search targets, including line-specific links for search match rows.
25
+ - Fixed aborted streaming assistant messages staying frozen before their red "Operation aborted" label when status rows were appended underneath on ED3-risk terminals.
26
+ - Fixed `omp` / `omp -c` stacking a fresh welcome screen and transcript on top of the previous run's leftover terminal scrollback. The cold-launch transcript render was the only session-load path that did not pass `clearTerminalHistory`, so the TUI's scrollback-preserving initial paint left the prior run's welcome + conversation above the new one; the cold launch now clears native scrollback before painting, matching every in-process session switch.
27
+ - Fixed a long streamed assistant reply dropping its earlier lines on ED3-risk terminals (Ghostty/kitty/iTerm2) once it grew past the viewport — the head scrolled off the top and never reached scrollback, so the reply rendered as a ~viewport-tall circular buffer of only its latest lines. `AssistantMessageComponent` now reports itself as an append-only transcript block and `TranscriptContainer` surfaces the resulting commit-safe boundary, so the renderer commits the scrolled-off head to native scrollback instead of discarding it (volatile tool previews stay deferred as before).
28
+
29
+ ### Security
30
+
31
+ - Blocked OSC 8 hyperlink wrapping for URI targets containing terminal control bytes to avoid rendering malformed control-sequence links
32
+
33
+ ## [15.9.4] - 2026-06-05
34
+ ### Fixed
35
+
36
+ - Fixed chat transcript updates after submitting input so frozen scrollback is only thawed when native scrollback replay succeeds, preventing misplaced or duplicated rows when the viewport is not at the tail
37
+ - Fixed `read` of `.zip` archives to list the central directory without inflating every member, so large or corrupt zip payloads no longer freeze directory reads; member contents are inflated only when a specific entry is read.
38
+ - Fixed the Python `eval` kernel being hard-killed (and its persistent session state lost) when a cell blocked in `parallel()` / `agent()` was interrupted. Each `agent()`/`tool.*` call blocks a kernel worker thread in a synchronous `urllib` request to the host bridge, and `parallel()`'s `ThreadPoolExecutor` exit joins those threads — so the kernel cannot unwind a `KeyboardInterrupt` until every in-flight bridge call returns. A wide subagent fan-out's teardown routinely outlasted the kernel's 5s SIGINT-escalation window, so the kernel was force-killed (surfacing `[kernel] Python kernel shutdown`) while the subagents were still winding down. The host bridge now resolves an in-flight call the instant the cell's signal aborts, so the kernel unwinds cleanly and keeps its state; the already-signaled subagent continues tearing down in the background.
39
+ - Fixed `github` tool `run_watch` op ignoring the explicit `repo` argument and silently watching the cwd-inferred repository in nested/umbrella workspaces. `executeRunWatch` passed `undefined` for the user-supplied `repo` to `resolveGitHubRepo`, so a call like `{op: "run_watch", repo: "owner/cxf", branch: "main"}` fell back to `gh repo view` in cwd and streamed `watching <sha> on <cwd-repo>` against the wrong repository. The explicit `repo` now takes precedence over the cwd inference, and the no-`branch`/no-`run` path refuses to derive the watched commit from `git HEAD` unless the cwd actually points at the resolved repo — otherwise it raises a `ToolError` telling the caller to pass `branch` or `run` instead of silently rebinding to an unrelated commit ([#1949](https://github.com/can1357/oh-my-pi/issues/1949)).
40
+ - Fixed `omp plugin install <local-path>` failing with `Invalid package name: .` (and similar) for cwd-relative (`.`, `./pkg`), absolute (`/abs`, `C:\…`, `\\unc`), and tilde-prefixed (`~/pkg`) specs. `classifyInstallTarget` now returns a `local` arm in addition to `marketplace`/`npm`, and `plugin install` routes those specs to `PluginManager.link()` — the same code path as `omp plugin link`. ([#1945](https://github.com/can1357/oh-my-pi/issues/1945))
41
+ - Fixed the `web_search` result renderer capping the synthesized answer at 12 lines even when expanded, while the Sources list expanded in full — so a long answer stayed truncated ("… N more lines") after `Ctrl+O`, making the expand toggle look like a no-op and dwarfing the answer next to its sources. The answer now renders the full text (markdown-formatted) when expanded and a short markdown preview when collapsed, matching the sources' collapse/expand behavior. The answer is also rendered through the Markdown component instead of dimmed raw lines, so headings, bold, lists, and code in the answer display formatted.
42
+
5
43
  ## [15.9.3] - 2026-06-05
6
44
 
7
45
  ### Fixed
@@ -9361,4 +9399,4 @@ Initial public release.
9361
9399
  - Git branch display in footer
9362
9400
  - Message queueing during streaming responses
9363
9401
  - OAuth integration for Gmail and Google Calendar access
9364
- - HTML export with syntax highlighting and collapsible sections
9402
+ - HTML export with syntax highlighting and collapsible sections
@@ -1,4 +1,7 @@
1
- export declare function classifyInstallTarget(spec: string, knownMarketplaces: Set<string>): {
1
+ export type ClassifiedInstallTarget = {
2
+ type: "local";
3
+ path: string;
4
+ } | {
2
5
  type: "marketplace";
3
6
  name: string;
4
7
  marketplace: string;
@@ -6,3 +9,4 @@ export declare function classifyInstallTarget(spec: string, knownMarketplaces: S
6
9
  type: "npm";
7
10
  spec: string;
8
11
  };
12
+ export declare function classifyInstallTarget(spec: string, knownMarketplaces: Set<string>): ClassifiedInstallTarget;
@@ -659,7 +659,7 @@ export declare const SETTINGS_SCHEMA: {
659
659
  readonly ui: {
660
660
  readonly tab: "appearance";
661
661
  readonly label: "Terminal Hyperlinks";
662
- readonly description: "Wrap file paths in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)";
662
+ readonly description: "Wrap paths and URLs in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)";
663
663
  };
664
664
  };
665
665
  readonly "display.tabWidth": {
@@ -745,6 +745,15 @@ export declare const SETTINGS_SCHEMA: {
745
745
  readonly description: "Render full tool descriptions in the system prompt instead of a tool name list";
746
746
  };
747
747
  };
748
+ readonly includeModelInPrompt: {
749
+ readonly type: "boolean";
750
+ readonly default: true;
751
+ readonly ui: {
752
+ readonly tab: "model";
753
+ readonly label: "Include Model In Prompt";
754
+ readonly description: "Surface the active model identifier in the system prompt so the agent knows which model it is";
755
+ };
756
+ };
748
757
  readonly temperature: {
749
758
  readonly type: "number";
750
759
  readonly default: -1;
@@ -2828,12 +2837,12 @@ export declare const SETTINGS_SCHEMA: {
2828
2837
  };
2829
2838
  readonly "tools.discoveryMode": {
2830
2839
  readonly type: "enum";
2831
- readonly values: readonly ["off", "mcp-only", "all"];
2832
- readonly default: "off";
2840
+ readonly values: readonly ["auto", "off", "mcp-only", "all"];
2841
+ readonly default: "auto";
2833
2842
  readonly ui: {
2834
2843
  readonly tab: "tools";
2835
2844
  readonly label: "Tool Discovery";
2836
- readonly description: "Hide tools behind a search tool to save tokens. 'mcp-only' hides MCP tools; 'all' hides all non-essential built-ins too.";
2845
+ readonly description: "Hide tools behind a search tool to save tokens. 'auto' hides MCP tools once the tool set has more than 40 tools; 'mcp-only' always hides MCP tools; 'all' hides all non-essential built-ins too.";
2837
2846
  };
2838
2847
  };
2839
2848
  readonly "tools.essentialOverride": {
@@ -13,6 +13,17 @@ export declare class AssistantMessageComponent extends Container {
13
13
  constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, thinkingRenderers?: readonly AssistantThinkingRenderer[], imageBudget?: ImageBudget | undefined);
14
14
  invalidate(): void;
15
15
  setHideThinkingBlock(hide: boolean): void;
16
+ isTranscriptBlockFinalized(): boolean;
17
+ /**
18
+ * Assistant text/thinking streams in append-only: earlier rendered rows never
19
+ * re-layout, new content only grows the block at the bottom. The transcript
20
+ * reports this so the renderer may commit scrolled-off head rows of a long
21
+ * streamed reply to native scrollback instead of dropping them (see
22
+ * `NativeScrollbackLiveRegion#getNativeScrollbackCommitSafeEnd`). Volatile
23
+ * blocks (tool previews that collapse) intentionally do not implement this.
24
+ */
25
+ isTranscriptBlockAppendOnly(): boolean;
26
+ markTranscriptBlockFinalized(): void;
16
27
  setToolResultImages(toolCallId: string, images: ImageContent[]): void;
17
28
  setUsageInfo(usage: Usage): void;
18
29
  updateContent(message: AssistantMessage): void;
@@ -6,8 +6,10 @@ type ConfigurableEditorAction = Extract<AppKeybinding, "app.interrupt" | "app.cl
6
6
  */
7
7
  export declare class CustomEditor extends Editor {
8
8
  #private;
9
+ imageLinks?: readonly (string | undefined)[];
9
10
  /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflow" keywords as the user types
10
- * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. */
11
+ * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
12
+ * pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
11
13
  decorateText: (text: string) => string;
12
14
  onEscape?: () => void;
13
15
  onClear?: () => void;
@@ -0,0 +1,11 @@
1
+ import { Container } from "@oh-my-pi/pi-tui";
2
+ /**
3
+ * A persistent error banner pinned above the editor. Unlike the transcript
4
+ * "Error: …" line (which scrolls away as the conversation grows), this stays in
5
+ * the fixed region directly above the input so a turn that ended on a provider
6
+ * error — e.g. Anthropic's "Output blocked by content filtering policy" — cannot
7
+ * be missed. It is cleared when the next turn starts.
8
+ */
9
+ export declare class ErrorBannerComponent extends Container {
10
+ constructor(message: string);
11
+ }
@@ -52,6 +52,21 @@ export declare class ToolExecutionComponent extends Container {
52
52
  details?: any;
53
53
  isError?: boolean;
54
54
  }, isPartial?: boolean, _toolCallId?: string): void;
55
+ /**
56
+ * Whether this block has reached a terminal state for transcript freezing.
57
+ * Reports `false` while it can still visually change so the
58
+ * {@link TranscriptContainer} keeps it inside the repaintable live region:
59
+ * a foreground tool awaiting its result, or one streaming partial output.
60
+ * A final (non-partial) result, a background-async tool the agent has moved
61
+ * past, or an explicit {@link seal} flips it to `true`.
62
+ */
63
+ isTranscriptBlockFinalized(): boolean;
64
+ /**
65
+ * Mark the tool terminal even though no result arrived (the turn aborted or
66
+ * abandoned it) and stop animating, so it can freeze and stops pinning the
67
+ * transcript live region.
68
+ */
69
+ seal(): void;
55
70
  /**
56
71
  * Stop spinner animation and cleanup resources.
57
72
  */
@@ -26,6 +26,7 @@ export declare class TranscriptContainer extends Container implements NativeScro
26
26
  invalidate(): void;
27
27
  clear(): void;
28
28
  getNativeScrollbackLiveRegionStart(): number | undefined;
29
+ getNativeScrollbackCommitSafeEnd(): number | undefined;
29
30
  /**
30
31
  * Retire all frozen snapshots so the next render reflects each block's current
31
32
  * state. Call at reconciliation checkpoints (prompt submit) where the whole
@@ -3,6 +3,6 @@ import { Container } from "@oh-my-pi/pi-tui";
3
3
  * Component that renders a user message
4
4
  */
5
5
  export declare class UserMessageComponent extends Container {
6
- constructor(text: string, synthetic?: boolean);
6
+ constructor(text: string, synthetic?: boolean, imageLinks?: readonly (string | undefined)[]);
7
7
  render(width: number): string[];
8
8
  }
@@ -0,0 +1,17 @@
1
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
+ import { type BlobPutResult } from "../session/blob-store";
3
+ type ImageBlobWriter = (data: Buffer, options?: {
4
+ extension?: string;
5
+ }) => Promise<BlobPutResult>;
6
+ type ImageBlobWriterSync = (data: Buffer, options?: {
7
+ extension?: string;
8
+ }) => BlobPutResult;
9
+ export interface ImageReferenceRenderers {
10
+ renderText: (text: string) => string;
11
+ renderReference: (label: string, index: number) => string;
12
+ }
13
+ export declare function renderImageReferences(text: string, renderers: ImageReferenceRenderers): string;
14
+ export declare function imageReferenceHyperlink(label: string, index: number, imageLinks: readonly (string | undefined)[] | undefined, renderLabel: (text: string) => string): string;
15
+ export declare function materializeImageReferenceLinks(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriter): Promise<(string | undefined)[] | undefined>;
16
+ export declare function materializeImageReferenceLinksSync(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriterSync): (string | undefined)[] | undefined;
17
+ export {};
@@ -56,6 +56,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
56
56
  todoContainer: Container;
57
57
  btwContainer: Container;
58
58
  omfgContainer: Container;
59
+ errorBannerContainer: Container;
59
60
  editor: CustomEditor;
60
61
  editorContainer: Container;
61
62
  hookWidgetContainerAbove: Container;
@@ -77,6 +78,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
77
78
  todoPhases: TodoPhase[];
78
79
  hideThinkingBlock: boolean;
79
80
  pendingImages: ImageContent[];
81
+ pendingImageLinks: (string | undefined)[];
80
82
  compactionQueuedMessages: CompactionQueuedMessage[];
81
83
  pendingTools: Map<string, ToolExecutionHandle>;
82
84
  pendingBashComponents: BashExecutionComponent[];
@@ -136,6 +138,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
136
138
  startPendingSubmission(input: {
137
139
  text: string;
138
140
  images?: ImageContent[];
141
+ imageLinks?: (string | undefined)[];
139
142
  customType?: string;
140
143
  display?: boolean;
141
144
  }): SubmittedUserInput;
@@ -162,6 +165,8 @@ export declare class InteractiveMode implements InteractiveModeContext {
162
165
  dim?: boolean;
163
166
  }): void;
164
167
  showError(message: string): void;
168
+ showPinnedError(message: string): void;
169
+ clearPinnedError(): void;
165
170
  showWarning(message: string): void;
166
171
  ensureLoadingAnimation(): void;
167
172
  setWorkingMessage(message?: string): void;
@@ -177,6 +182,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
177
182
  isKnownSlashCommand(text: string): boolean;
178
183
  addMessageToChat(message: AgentMessage, options?: {
179
184
  populateHistory?: boolean;
185
+ imageLinks?: readonly (string | undefined)[];
180
186
  }): Component[];
181
187
  renderSessionContext(sessionContext: SessionContext, options?: {
182
188
  updateFooter?: boolean;
@@ -184,6 +190,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
184
190
  }): void;
185
191
  renderInitialMessages(prebuiltContext?: SessionContext, options?: {
186
192
  preserveExistingChat?: boolean;
193
+ clearTerminalHistory?: boolean;
187
194
  }): void;
188
195
  getUserMessageText(message: Message): string;
189
196
  findLastAssistantMessage(): AssistantMessage | undefined;
@@ -32,6 +32,7 @@ export type CompactionQueuedMessage = {
32
32
  export type SubmittedUserInput = {
33
33
  text: string;
34
34
  images?: ImageContent[];
35
+ imageLinks?: (string | undefined)[];
35
36
  customType?: string;
36
37
  display?: boolean;
37
38
  cancelled: boolean;
@@ -60,6 +61,7 @@ export interface InteractiveModeContext {
60
61
  todoContainer: Container;
61
62
  btwContainer: Container;
62
63
  omfgContainer: Container;
64
+ errorBannerContainer: Container;
63
65
  editor: CustomEditor;
64
66
  editorContainer: Container;
65
67
  hookWidgetContainerAbove: Container;
@@ -87,6 +89,7 @@ export interface InteractiveModeContext {
87
89
  planModePlanFilePath?: string;
88
90
  hideThinkingBlock: boolean;
89
91
  pendingImages: ImageContent[];
92
+ pendingImageLinks: (string | undefined)[];
90
93
  compactionQueuedMessages: CompactionQueuedMessage[];
91
94
  pendingTools: Map<string, ToolExecutionHandle>;
92
95
  pendingBashComponents: BashExecutionComponent[];
@@ -130,6 +133,8 @@ export interface InteractiveModeContext {
130
133
  dim?: boolean;
131
134
  }): void;
132
135
  showError(message: string): void;
136
+ showPinnedError(message: string): void;
137
+ clearPinnedError(): void;
133
138
  showWarning(message: string): void;
134
139
  showNewVersionNotification(newVersion: string): void;
135
140
  clearEditor(): void;
@@ -146,6 +151,7 @@ export interface InteractiveModeContext {
146
151
  startPendingSubmission(input: {
147
152
  text: string;
148
153
  images?: ImageContent[];
154
+ imageLinks?: (string | undefined)[];
149
155
  customType?: string;
150
156
  display?: boolean;
151
157
  }): SubmittedUserInput;
@@ -170,6 +176,7 @@ export interface InteractiveModeContext {
170
176
  isKnownSlashCommand(text: string): boolean;
171
177
  addMessageToChat(message: AgentMessage, options?: {
172
178
  populateHistory?: boolean;
179
+ imageLinks?: readonly (string | undefined)[];
173
180
  }): Component[];
174
181
  renderSessionContext(sessionContext: SessionContext, options?: {
175
182
  updateFooter?: boolean;
@@ -24,6 +24,7 @@ export declare class UiHelpers {
24
24
  }): void;
25
25
  addMessageToChat(message: AgentMessage, options?: {
26
26
  populateHistory?: boolean;
27
+ imageLinks?: readonly (string | undefined)[];
27
28
  }): Component[];
28
29
  /**
29
30
  * Render session context to chat. Used for initial load and rebuild after compaction.
@@ -1,15 +1,16 @@
1
+ export interface BlobPutOptions {
2
+ /** Optional file extension for a sidecar hardlink/copy that OS openers can type-detect. */
3
+ extension?: string;
4
+ }
1
5
  export interface BlobPutResult {
2
6
  hash: string;
7
+ /** Canonical content-addressed path, always `<dir>/<sha256-hex>`. */
3
8
  path: string;
9
+ /** Path with the requested extension when supplied, otherwise the canonical path. */
10
+ displayPath: string;
4
11
  get ref(): string;
5
12
  }
6
- /**
7
- * Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
8
- *
9
- * Files are stored at `<dir>/<sha256-hex>` with no extension. The SHA-256 hash is computed
10
- * over the raw binary data (not base64). Content-addressing makes writes idempotent and
11
- * provides automatic deduplication across sessions.
12
- */
13
+ export declare function blobExtensionForImageMimeType(mimeType: string | undefined): string | undefined;
13
14
  export declare class BlobStore {
14
15
  readonly dir: string;
15
16
  constructor(dir: string);
@@ -17,13 +18,13 @@ export declare class BlobStore {
17
18
  * Write binary data to the blob store.
18
19
  * @returns SHA-256 hex hash of the data
19
20
  */
20
- put(data: Buffer): Promise<BlobPutResult>;
21
+ put(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult>;
21
22
  /**
22
23
  * Synchronous variant of {@link put}. Use on persistence hot paths where the caller
23
24
  * cannot afford the microtask hops of the async version (e.g. OOM-safe session writes).
24
25
  * Returns once the bytes are in the kernel page cache.
25
26
  */
26
- putSync(data: Buffer): BlobPutResult;
27
+ putSync(data: Buffer, options?: BlobPutOptions): BlobPutResult;
27
28
  /** Read blob by hash, returns Buffer or null if not found. */
28
29
  get(hash: string): Promise<Buffer | null>;
29
30
  /** Check if a blob exists. */
@@ -46,9 +47,9 @@ export declare function externalizeImageDataUrlSync(blobStore: BlobStore, dataUr
46
47
  * Externalize an image's base64 data to the blob store, returning a blob reference.
47
48
  * If the data is already a blob reference, returns it unchanged.
48
49
  */
49
- export declare function externalizeImageData(blobStore: BlobStore, base64Data: string): Promise<string>;
50
+ export declare function externalizeImageData(blobStore: BlobStore, base64Data: string, mimeType?: string): Promise<string>;
50
51
  /** Synchronous variant of {@link externalizeImageData}. */
51
- export declare function externalizeImageDataSync(blobStore: BlobStore, base64Data: string): string;
52
+ export declare function externalizeImageDataSync(blobStore: BlobStore, base64Data: string, mimeType?: string): string;
52
53
  /**
53
54
  * Resolve an externalized provider image data URL back to its original string.
54
55
  * If the data is not a blob reference, returns it unchanged.
@@ -1,7 +1,7 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ImageContent, Message, MessageAttribution, ServiceTier, TextContent } from "@oh-my-pi/pi-ai";
3
3
  import { ArtifactManager } from "./artifacts";
4
- import { type BlobPutResult } from "./blob-store";
4
+ import { type BlobPutOptions, type BlobPutResult } from "./blob-store";
5
5
  import { type BashExecutionMessage, type CustomMessage, type FileMentionMessage, type HookMessage, type PythonExecutionMessage } from "./messages";
6
6
  import type { SessionStorage } from "./session-storage";
7
7
  export declare const CURRENT_SESSION_VERSION = 3;
@@ -208,7 +208,7 @@ export interface SessionInfo {
208
208
  */
209
209
  status?: SessionStatus;
210
210
  }
211
- export type ReadonlySessionManager = Pick<SessionManager, "getCwd" | "getSessionDir" | "getSessionId" | "getSessionFile" | "getSessionName" | "getArtifactsDir" | "getArtifactManager" | "allocateArtifactPath" | "saveArtifact" | "getArtifactPath" | "getLeafId" | "getLeafEntry" | "getEntry" | "getLabel" | "getBranch" | "getHeader" | "getEntries" | "getTree" | "getUsageStatistics" | "putBlob">;
211
+ export type ReadonlySessionManager = Pick<SessionManager, "getCwd" | "getSessionDir" | "getSessionId" | "getSessionFile" | "getSessionName" | "getArtifactsDir" | "getArtifactManager" | "allocateArtifactPath" | "saveArtifact" | "getArtifactPath" | "getLeafId" | "getLeafEntry" | "getEntry" | "getLabel" | "getBranch" | "getHeader" | "getEntries" | "getTree" | "getUsageStatistics" | "putBlob" | "putBlobSync">;
212
212
  /** Exported for testing */
213
213
  export declare function migrateSessionEntries(entries: FileEntry[]): void;
214
214
  /** Exported for compaction.test.ts */
@@ -294,7 +294,9 @@ export declare class SessionManager {
294
294
  private readonly storage;
295
295
  private constructor();
296
296
  /** Puts a binary blob into the blob store and returns the blob reference */
297
- putBlob(data: Buffer): Promise<BlobPutResult>;
297
+ putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult>;
298
+ /** Synchronous variant of {@link putBlob} for rebuild-only render paths. */
299
+ putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult;
298
300
  captureState(): SessionManagerStateSnapshot;
299
301
  restoreState(snapshot: SessionManagerStateSnapshot): void;
300
302
  /** Switch to a different session file (used for resume and branching) */
@@ -84,6 +84,8 @@ export interface BuildSystemPromptOptions {
84
84
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
85
85
  /** Whether the local memory://root summary is active. */
86
86
  memoryRootEnabled?: boolean;
87
+ /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
88
+ model?: string;
87
89
  }
88
90
  /** Result of building provider-facing system prompt messages. */
89
91
  export interface BuildSystemPromptResult {
@@ -1,5 +1,19 @@
1
1
  import type { Subprocess } from "bun";
2
- import type { TinyTitleProgressEvent, TinyTitleWorkerOutbound } from "./title-protocol";
2
+ import type { TinyTitleProgressEvent, TinyTitleWorkerInbound, TinyTitleWorkerOutbound } from "./title-protocol";
3
+ /**
4
+ * Abstraction over the tiny-model subprocess. Modelled as a worker interface
5
+ * so existing callers (titles, memory completions, downloads) compose the
6
+ * same way; the runtime implementation is a Bun child process so
7
+ * `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
8
+ * address space — that destructor segfaults Bun on Windows during shutdown
9
+ * (issue #1606).
10
+ */
11
+ interface WorkerHandle {
12
+ send(message: TinyTitleWorkerInbound): void;
13
+ onMessage(handler: (message: TinyTitleWorkerOutbound) => void): () => void;
14
+ onError(handler: (error: Error) => void): () => void;
15
+ terminate(): Promise<void>;
16
+ }
3
17
  export interface TinyTitleDownloadOptions {
4
18
  signal?: AbortSignal;
5
19
  onProgress?: (event: TinyTitleProgressEvent) => void;
@@ -39,6 +53,7 @@ interface SpawnedSubprocess {
39
53
  export declare function createTinyTitleSubprocess(): SpawnedSubprocess;
40
54
  export declare class TinyTitleClient {
41
55
  #private;
56
+ constructor(spawnWorker?: () => WorkerHandle);
42
57
  onProgress(listener: (event: TinyTitleProgressEvent) => void): () => void;
43
58
  generate(modelKey: string, message: string, signal?: AbortSignal): Promise<string | null>;
44
59
  complete(modelKey: string, prompt: string, options?: {
@@ -0,0 +1,8 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type { SettingValue } from "../config/settings-schema";
3
+ export declare const TOOL_DISCOVERY_AUTO_THRESHOLD = 40;
4
+ export declare const TOOL_DISCOVERY_SEARCH_TOOL_NAME = "search_tool_bm25";
5
+ export type ToolDiscoveryModeSetting = SettingValue<"tools.discoveryMode">;
6
+ export type EffectiveToolDiscoveryMode = Exclude<ToolDiscoveryModeSetting, "auto">;
7
+ export declare function countToolsForAutoDiscovery(toolNames: Iterable<string>): number;
8
+ export declare function resolveEffectiveToolDiscoveryMode(settings: Settings, toolCount: number): EffectiveToolDiscoveryMode;
@@ -21,7 +21,11 @@ interface TarStorage {
21
21
  }
22
22
  interface ZipStorage {
23
23
  type: "zip";
24
- bytes: Uint8Array;
24
+ archivePath: string;
25
+ compressedSize: number;
26
+ compression: number;
27
+ flags: number;
28
+ localHeaderOffset: number;
25
29
  }
26
30
  type EntryStorage = TarStorage | ZipStorage;
27
31
  interface ArchiveIndexEntry extends ArchiveNode {
@@ -7,6 +7,18 @@
7
7
  * - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
8
8
  */
9
9
  export declare function isHyperlinkEnabled(): boolean;
10
+ /**
11
+ * Wrap `displayText` in an OSC 8 hyperlink pointing at `uri`.
12
+ *
13
+ * Returns `displayText` unchanged when hyperlinks are disabled, `uri` contains
14
+ * terminal control bytes, or `displayText` already contains an OSC 8 sequence.
15
+ */
16
+ export declare function uriHyperlink(uri: string, displayText: string): string;
17
+ /**
18
+ * Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL.
19
+ * `www.example.com` inputs are linked as `https://www.example.com`.
20
+ */
21
+ export declare function urlHyperlink(url: string, displayText: string): string;
10
22
  /**
11
23
  * Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
12
24
  *
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
8
- import type { Theme } from "../../modes/theme/theme";
8
+ import { type Theme } from "../../modes/theme/theme";
9
9
  import type { SearchResponse } from "./types";
10
10
  export interface SearchRenderDetails {
11
11
  response: SearchResponse;
@@ -20,7 +20,6 @@ export declare function renderSearchResult(result: {
20
20
  details?: SearchRenderDetails;
21
21
  }, options: RenderResultOptions, theme: Theme, args?: {
22
22
  query?: string;
23
- allowLongAnswer?: boolean;
24
23
  maxAnswerLines?: number;
25
24
  }): Component;
26
25
  /** Render web search call (query preview) */
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.9.3",
4
+ "version": "15.9.5",
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,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.9.3",
51
- "@oh-my-pi/omp-stats": "15.9.3",
52
- "@oh-my-pi/pi-agent-core": "15.9.3",
53
- "@oh-my-pi/pi-ai": "15.9.3",
54
- "@oh-my-pi/pi-mnemopi": "15.9.3",
55
- "@oh-my-pi/pi-natives": "15.9.3",
56
- "@oh-my-pi/pi-tui": "15.9.3",
57
- "@oh-my-pi/pi-utils": "15.9.3",
50
+ "@oh-my-pi/hashline": "15.9.5",
51
+ "@oh-my-pi/omp-stats": "15.9.5",
52
+ "@oh-my-pi/pi-agent-core": "15.9.5",
53
+ "@oh-my-pi/pi-ai": "15.9.5",
54
+ "@oh-my-pi/pi-mnemopi": "15.9.5",
55
+ "@oh-my-pi/pi-natives": "15.9.5",
56
+ "@oh-my-pi/pi-tui": "15.9.5",
57
+ "@oh-my-pi/pi-utils": "15.9.5",
58
58
  "@opentelemetry/api": "^1.9.1",
59
59
  "@opentelemetry/context-async-hooks": "^2.7.1",
60
60
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Classify an install spec as a marketplace plugin reference or a plain npm package.
2
+ * Classify an install spec as a local path, marketplace plugin reference, or
3
+ * plain npm package.
3
4
  *
4
5
  * Rules (applied in order):
6
+ * 0. Looks like a filesystem path (`.`, `..`, `./…`, `..\…`, `/…`, `~/…`,
7
+ * `C:\…`, `\\unc`) -> local. Routed through `PluginManager.link()` so the
8
+ * `omp plugin install <path>` and `omp plugin link <path>` flows agree.
5
9
  * 1. Starts with `@` (scoped npm) -> always npm.
6
10
  * 2. Contains `@` after the first character -> split on the LAST `@`.
7
11
  * If the right-hand side is a known marketplace name, it's a marketplace ref.
@@ -25,10 +29,32 @@ const NPM_DIST_TAGS = new Set([
25
29
  // Semver-like: starts with digit, or contains version range prefixes
26
30
  const LOOKS_LIKE_VERSION = /^[\d~^>=<]/;
27
31
 
28
- export function classifyInstallTarget(
29
- spec: string,
30
- knownMarketplaces: Set<string>,
31
- ): { type: "marketplace"; name: string; marketplace: string } | { type: "npm"; spec: string } {
32
+ /**
33
+ * Detect specs that name a filesystem path rather than a package: bare `.` /
34
+ * `..`, cwd-relative (`./`, `../`, `.\`, `..\`), absolute (`/`, `C:\`, `C:/`,
35
+ * UNC `\\`), and tilde-prefixed (`~`, `~/`, `~\`). Tilde paths still rely on
36
+ * the shell or the caller for expansion — we only classify them so they reach
37
+ * the link path instead of npm-name validation.
38
+ */
39
+ function isLocalPathSpec(spec: string): boolean {
40
+ if (spec === "." || spec === ".." || spec === "~") return true;
41
+ if (spec.startsWith("./") || spec.startsWith("../")) return true;
42
+ if (spec.startsWith(".\\") || spec.startsWith("..\\")) return true;
43
+ if (spec.startsWith("~/") || spec.startsWith("~\\")) return true;
44
+ if (spec.startsWith("/")) return true;
45
+ if (spec.startsWith("\\\\")) return true;
46
+ if (/^[A-Za-z]:[\\/]/.test(spec)) return true;
47
+ return false;
48
+ }
49
+
50
+ export type ClassifiedInstallTarget =
51
+ | { type: "local"; path: string }
52
+ | { type: "marketplace"; name: string; marketplace: string }
53
+ | { type: "npm"; spec: string };
54
+
55
+ export function classifyInstallTarget(spec: string, knownMarketplaces: Set<string>): ClassifiedInstallTarget {
56
+ // Rule 0: filesystem path — bypass npm/marketplace validation entirely.
57
+ if (isLocalPathSpec(spec)) return { type: "local", path: spec };
32
58
  // Rule 1: scoped npm package — @ at position 0 is never a marketplace separator.
33
59
  if (spec.startsWith("@")) return { type: "npm", spec };
34
60
  // Rule 2: @ somewhere after the first character.