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

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 (142) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/dist/types/cli/classify-install-target.d.ts +5 -1
  3. package/dist/types/config/keybindings.d.ts +4 -1
  4. package/dist/types/config/settings-schema.d.ts +24 -5
  5. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  6. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
  7. package/dist/types/eval/backend.d.ts +6 -6
  8. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  9. package/dist/types/eval/idle-timeout.d.ts +16 -14
  10. package/dist/types/eval/js/executor.d.ts +3 -3
  11. package/dist/types/eval/py/executor.d.ts +2 -2
  12. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  13. package/dist/types/modes/components/assistant-message.d.ts +16 -0
  14. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  16. package/dist/types/modes/components/error-banner.d.ts +11 -0
  17. package/dist/types/modes/components/model-selector.d.ts +1 -0
  18. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  19. package/dist/types/modes/components/transcript-container.d.ts +1 -0
  20. package/dist/types/modes/components/user-message.d.ts +1 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  23. package/dist/types/modes/image-references.d.ts +17 -0
  24. package/dist/types/modes/interactive-mode.d.ts +8 -1
  25. package/dist/types/modes/types.d.ts +8 -1
  26. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  27. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  28. package/dist/types/session/blob-store.d.ts +12 -11
  29. package/dist/types/session/session-manager.d.ts +5 -3
  30. package/dist/types/system-prompt.d.ts +2 -0
  31. package/dist/types/tiny/title-client.d.ts +16 -1
  32. package/dist/types/tool-discovery/mode.d.ts +8 -0
  33. package/dist/types/tools/archive-reader.d.ts +5 -1
  34. package/dist/types/tools/eval-render.d.ts +8 -0
  35. package/dist/types/tools/render-utils.d.ts +25 -0
  36. package/dist/types/tui/code-cell.d.ts +6 -0
  37. package/dist/types/tui/hyperlink.d.ts +12 -0
  38. package/dist/types/tui/output-block.d.ts +11 -0
  39. package/dist/types/web/search/render.d.ts +1 -2
  40. package/package.json +9 -9
  41. package/src/autoresearch/dashboard.ts +11 -21
  42. package/src/cli/classify-install-target.ts +31 -5
  43. package/src/cli/claude-trace-cli.ts +13 -1
  44. package/src/cli/plugin-cli.ts +45 -0
  45. package/src/cli/web-search-cli.ts +0 -1
  46. package/src/config/keybindings.ts +58 -1
  47. package/src/config/model-registry.ts +54 -4
  48. package/src/config/settings-schema.ts +25 -5
  49. package/src/debug/raw-sse.ts +18 -4
  50. package/src/edit/file-snapshot-store.ts +1 -1
  51. package/src/edit/index.ts +1 -1
  52. package/src/edit/renderer.ts +7 -7
  53. package/src/edit/streaming.ts +1 -1
  54. package/src/eval/__tests__/agent-bridge.test.ts +100 -27
  55. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  56. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  57. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  58. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  59. package/src/eval/__tests__/shared-executors.test.ts +2 -2
  60. package/src/eval/agent-bridge.ts +4 -5
  61. package/src/eval/backend.ts +6 -6
  62. package/src/eval/bridge-timeout.ts +44 -0
  63. package/src/eval/idle-timeout.ts +33 -15
  64. package/src/eval/js/executor.ts +10 -10
  65. package/src/eval/llm-bridge.ts +4 -5
  66. package/src/eval/py/executor.ts +6 -6
  67. package/src/eval/py/kernel.ts +11 -1
  68. package/src/eval/py/spawn-options.ts +126 -0
  69. package/src/eval/py/tool-bridge.ts +43 -5
  70. package/src/export/ttsr.ts +9 -0
  71. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
  72. package/src/extensibility/extensions/runner.ts +2 -0
  73. package/src/internal-urls/docs-index.generated.ts +9 -8
  74. package/src/lsp/client.ts +80 -2
  75. package/src/lsp/index.ts +38 -4
  76. package/src/lsp/render.ts +3 -3
  77. package/src/main.ts +8 -2
  78. package/src/modes/components/agent-dashboard.ts +13 -4
  79. package/src/modes/components/assistant-message.ts +44 -1
  80. package/src/modes/components/copy-selector.ts +249 -0
  81. package/src/modes/components/custom-editor.ts +14 -2
  82. package/src/modes/components/error-banner.ts +33 -0
  83. package/src/modes/components/extensions/extension-list.ts +17 -8
  84. package/src/modes/components/history-search.ts +19 -11
  85. package/src/modes/components/model-selector.ts +125 -29
  86. package/src/modes/components/oauth-selector.ts +28 -12
  87. package/src/modes/components/session-observer-overlay.ts +13 -15
  88. package/src/modes/components/session-selector.ts +24 -13
  89. package/src/modes/components/tool-execution.ts +71 -13
  90. package/src/modes/components/transcript-container.ts +93 -32
  91. package/src/modes/components/tree-selector.ts +19 -7
  92. package/src/modes/components/user-message-selector.ts +25 -14
  93. package/src/modes/components/user-message.ts +9 -2
  94. package/src/modes/controllers/command-controller.ts +0 -116
  95. package/src/modes/controllers/event-controller.ts +67 -12
  96. package/src/modes/controllers/input-controller.ts +33 -1
  97. package/src/modes/controllers/selector-controller.ts +38 -1
  98. package/src/modes/image-references.ts +111 -0
  99. package/src/modes/interactive-mode.ts +52 -17
  100. package/src/modes/theme/theme.ts +46 -10
  101. package/src/modes/types.ts +11 -2
  102. package/src/modes/utils/copy-targets.ts +254 -0
  103. package/src/modes/utils/ui-helpers.ts +23 -2
  104. package/src/prompts/ci-green-request.md +5 -3
  105. package/src/prompts/system/project-prompt.md +1 -0
  106. package/src/prompts/tools/ast-edit.md +1 -1
  107. package/src/prompts/tools/ast-grep.md +1 -1
  108. package/src/prompts/tools/read.md +1 -1
  109. package/src/prompts/tools/search.md +1 -1
  110. package/src/sdk.ts +17 -9
  111. package/src/session/agent-session.ts +43 -14
  112. package/src/session/blob-store.ts +96 -9
  113. package/src/session/session-manager.ts +19 -10
  114. package/src/slash-commands/builtin-registry.ts +3 -11
  115. package/src/system-prompt.ts +4 -0
  116. package/src/task/render.ts +38 -11
  117. package/src/tiny/title-client.ts +7 -1
  118. package/src/tool-discovery/mode.ts +24 -0
  119. package/src/tools/archive-reader.ts +339 -31
  120. package/src/tools/bash.ts +18 -8
  121. package/src/tools/browser/render.ts +5 -4
  122. package/src/tools/debug.ts +3 -3
  123. package/src/tools/eval-render.ts +24 -9
  124. package/src/tools/eval.ts +14 -19
  125. package/src/tools/fetch.ts +34 -14
  126. package/src/tools/gh.ts +65 -11
  127. package/src/tools/index.ts +6 -8
  128. package/src/tools/read.ts +65 -19
  129. package/src/tools/render-utils.ts +46 -0
  130. package/src/tools/search-tool-bm25.ts +4 -6
  131. package/src/tools/search.ts +60 -11
  132. package/src/tools/ssh.ts +21 -8
  133. package/src/tools/write.ts +17 -8
  134. package/src/tui/code-cell.ts +19 -4
  135. package/src/tui/hyperlink.ts +42 -7
  136. package/src/tui/output-block.ts +14 -0
  137. package/src/web/search/index.ts +2 -2
  138. package/src/web/search/render.ts +23 -55
  139. package/dist/types/eval/heartbeat.d.ts +0 -45
  140. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  141. package/src/eval/heartbeat.ts +0 -74
  142. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
@@ -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;
@@ -190,7 +197,6 @@ export interface InteractiveModeContext {
190
197
  toggleTodoExpansion(): void;
191
198
  handleExportCommand(text: string): Promise<void>;
192
199
  handleShareCommand(): Promise<void>;
193
- handleCopyCommand(sub?: string): void;
194
200
  handleTodoCommand(args: string): Promise<void>;
195
201
  handleSessionCommand(): Promise<void>;
196
202
  handleJobsCommand(): Promise<void>;
@@ -228,6 +234,7 @@ export interface InteractiveModeContext {
228
234
  }): void;
229
235
  showPluginSelector(mode?: "install" | "uninstall"): void;
230
236
  showUserMessageSelector(): void;
237
+ showCopySelector(): void;
231
238
  showTreeSelector(): void;
232
239
  showSessionSelector(): void;
233
240
  handleResumeSession(sessionPath: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ /** A fenced code block extracted from assistant markdown. */
3
+ export interface CodeBlock {
4
+ /** Info string after the opening fence (language id), trimmed. */
5
+ lang: string;
6
+ /** Block body with the trailing newline stripped. */
7
+ code: string;
8
+ }
9
+ /** A runnable command found in the transcript. */
10
+ export interface LastCommand {
11
+ kind: "bash" | "eval";
12
+ code: string;
13
+ /** Highlight language: "bash" for bash, "python"/"javascript" for eval. */
14
+ language: string;
15
+ }
16
+ /**
17
+ * A node in the `/copy` picker tree. Leaves carry `content` (placed on the
18
+ * clipboard) plus `copyMessage` (the status shown afterwards); groups carry
19
+ * `children` to drill into.
20
+ */
21
+ export interface CopyTarget {
22
+ /** Stable identifier (e.g. "msg:1", "msg:1:code:0", "msg:1:all", "cmd:1"). */
23
+ id: string;
24
+ label: string;
25
+ /** Dim annotation: line/block counts, language, or tool name. */
26
+ hint?: string;
27
+ /** Full text rendered in the preview pane. */
28
+ preview: string;
29
+ /** Highlight language for code/command previews (undefined = plain/markdown). */
30
+ language?: string;
31
+ /** Leaf: text copied to the clipboard. */
32
+ content?: string;
33
+ /** Leaf: status message shown after copying. */
34
+ copyMessage?: string;
35
+ /** Group: nested targets to drill into. */
36
+ children?: CopyTarget[];
37
+ }
38
+ /** Minimal session surface needed to assemble copy targets (eases testing). */
39
+ export interface CopySource {
40
+ readonly messages: readonly AgentMessage[];
41
+ getLastVisibleHandoffText(): string | undefined;
42
+ }
43
+ /** Extract fenced code blocks from assistant markdown, in document order. */
44
+ export declare function extractCodeBlocks(text: string): CodeBlock[];
45
+ /** Walk the transcript backwards for the most recent bash command or eval code. */
46
+ export declare function extractLastCommand(messages: readonly AgentMessage[]): LastCommand | undefined;
47
+ /**
48
+ * Assemble the unified `/copy` target tree: recent assistant messages
49
+ * (most recent first, each drillable into its code blocks), runnable command
50
+ * targets interleaved after the assistant message that issued them, and a
51
+ * fresh-handoff fallback when no assistant message exists yet.
52
+ */
53
+ export declare function buildCopyTargets(source: CopySource): CopyTarget[];
@@ -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 {
@@ -13,6 +13,14 @@ import type { EvalStatusEvent, EvalToolDetails } from "../eval/types";
13
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
14
  import { type Theme } from "../modes/theme/theme";
15
15
  export declare const EVAL_DEFAULT_PREVIEW_LINES = 10;
16
+ /**
17
+ * Rows of source kept in the *pending* eval preview. The window follows the
18
+ * streaming edge (newest lines pinned to the bottom) so you can watch the code
19
+ * being written, while staying bounded — a volatile tool block taller than the
20
+ * viewport would otherwise strand its scrolled-off head out of native scrollback
21
+ * on ED3-risk terminals. Matches the streaming windows used by edit/write.
22
+ */
23
+ export declare const EVAL_STREAMING_PREVIEW_LINES = 12;
16
24
  interface EvalRenderCellArg {
17
25
  language?: string;
18
26
  code?: string;
@@ -76,6 +76,31 @@ export declare function formatBadge(label: string, color: ToolUIColor, theme: Th
76
76
  * Uses consistent wording pattern.
77
77
  */
78
78
  export declare function formatMoreItems(remaining: number, itemType: string): string;
79
+ /**
80
+ * Maximum rows a tool's streaming/pending *call* preview may render before it is
81
+ * capped. This is intentionally conservative: the preview still sits inside a
82
+ * transcript that already consumed some viewport rows, and tool blocks carry
83
+ * extra chrome (status/header/border/"more lines"), so a "reasonable" raw code
84
+ * or command preview like 10-12 lines can still overflow and strand its top
85
+ * while the block is volatile. Keeping the live call window short avoids that
86
+ * across terminals without turning the transcript into an interactive scroller.
87
+ */
88
+ export declare const CALL_PREVIEW_MAX_LINES = 6;
89
+ /**
90
+ * Cap a pre-rendered pending/call preview to a bounded window. When truncated,
91
+ * show both the head and the live tail so the user can still see what the tool
92
+ * is currently writing while the volatile block stays short enough not to strand
93
+ * its top above the viewport. `Ctrl+O` widens the bounded window, but does not
94
+ * fully uncap live tool previews for the same reason.
95
+ *
96
+ * `prefix` (raw, e.g. a dim tree gutter) is prepended to the summary line so
97
+ * nested previews stay aligned.
98
+ */
99
+ export declare function capPreviewLines(lines: string[], theme: Theme, options?: {
100
+ max?: number;
101
+ expanded?: boolean;
102
+ prefix?: string;
103
+ }): string[];
79
104
  export declare function formatMeta(meta: string[], theme: Theme): string;
80
105
  export declare function formatErrorMessage(message: string | undefined, theme: Theme): string;
81
106
  export declare function formatEmptyMessage(message: string, theme: Theme): string;
@@ -11,6 +11,12 @@ export interface CodeCellOptions {
11
11
  output?: string;
12
12
  outputMaxLines?: number;
13
13
  codeMaxLines?: number;
14
+ /**
15
+ * Show the LAST `codeMaxLines` rows (the live streaming edge) instead of the
16
+ * first, with a "… N earlier lines" marker on top. Lets a pending preview
17
+ * follow code as it is written while staying bounded. Ignored when `expanded`.
18
+ */
19
+ codeTail?: boolean;
14
20
  expanded?: boolean;
15
21
  /** Animate the cell border with a sweeping segment while pending/running. */
16
22
  animate?: boolean;
@@ -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
  *
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Bordered output container with optional header and sections.
3
+ */
4
+ import type { Component } from "@oh-my-pi/pi-tui";
1
5
  import type { Theme } from "../modes/theme/theme";
2
6
  import type { State } from "./types";
3
7
  export interface OutputBlockOptions {
@@ -13,6 +17,12 @@ export interface OutputBlockOptions {
13
17
  /** Animate the border with a sweeping dark segment (pending/running state). */
14
18
  animate?: boolean;
15
19
  }
20
+ declare const FRAMED_BLOCK_COMPONENT: unique symbol;
21
+ export type FramedBlockComponent = Component & {
22
+ [FRAMED_BLOCK_COMPONENT]?: true;
23
+ };
24
+ export declare function markFramedBlockComponent<T extends Component>(component: T): T & FramedBlockComponent;
25
+ export declare function isFramedBlockComponent(component: Component): boolean;
16
26
  /**
17
27
  * Monotonic frame counter for animated borders, quantized to the TUI's ~16ms
18
28
  * render cap so the cache key advances once per ~60fps frame — fine enough for a
@@ -44,3 +54,4 @@ export declare class CachedOutputBlock {
44
54
  /** Invalidate the cache, forcing a rebuild on next render. */
45
55
  invalidate(): void;
46
56
  }
57
+ export {};
@@ -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.67",
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.67",
51
+ "@oh-my-pi/omp-stats": "15.9.67",
52
+ "@oh-my-pi/pi-agent-core": "15.9.67",
53
+ "@oh-my-pi/pi-ai": "15.9.67",
54
+ "@oh-my-pi/pi-mnemopi": "15.9.67",
55
+ "@oh-my-pi/pi-natives": "15.9.67",
56
+ "@oh-my-pi/pi-tui": "15.9.67",
57
+ "@oh-my-pi/pi-utils": "15.9.67",
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,4 +1,4 @@
1
- import { matchesKey, replaceTabs, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { matchesKey, replaceTabs, ScrollView, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import type { Theme } from "../modes/theme/theme";
3
3
  import { formatElapsed, formatNum, isBetter } from "./helpers";
4
4
  import { currentResults, findBaselineMetric, findBaselineRunNumber, findBaselineSecondary } from "./state";
@@ -76,14 +76,14 @@ export function createDashboardController(): DashboardController {
76
76
  const viewportRows = Math.max(4, terminalRows - 4);
77
77
  const maxScroll = Math.max(0, body.length - viewportRows);
78
78
  if (scrollOffset > maxScroll) scrollOffset = maxScroll;
79
- const visible = body.slice(scrollOffset, scrollOffset + viewportRows);
80
- const footer = renderOverlayFooter(width, scrollOffset, viewportRows, body.length, theme);
81
- return [
82
- header,
83
- ...visible,
84
- ...Array.from({ length: Math.max(0, viewportRows - visible.length) }, () => ""),
85
- footer,
86
- ];
79
+ const sv = new ScrollView(body.slice(scrollOffset, scrollOffset + viewportRows), {
80
+ height: viewportRows,
81
+ scrollbar: "auto",
82
+ totalRows: body.length,
83
+ theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
84
+ });
85
+ sv.setScrollOffset(scrollOffset);
86
+ return [header, ...sv.render(width), renderOverlayFooter(width, theme)];
87
87
  },
88
88
  handleInput(data: string): void {
89
89
  const totalRows =
@@ -406,18 +406,8 @@ function renderOverlayRunningLine(
406
406
  );
407
407
  }
408
408
 
409
- function renderOverlayFooter(
410
- width: number,
411
- scrollOffset: number,
412
- viewportRows: number,
413
- totalRows: number,
414
- theme: Theme,
415
- ): string {
416
- const position =
417
- totalRows > viewportRows
418
- ? ` ${scrollOffset + 1}-${Math.min(totalRows, scrollOffset + viewportRows)}/${totalRows}`
419
- : "";
420
- const hint = theme.fg("dim", ` up/down j/k pageup pagedown g G esc${position} `);
409
+ function renderOverlayFooter(width: number, theme: Theme): string {
410
+ const hint = theme.fg("dim", " up/down j/k pageup pagedown g G esc ");
421
411
  const fill = Math.max(0, width - visibleWidth(hint));
422
412
  return theme.fg("borderMuted", "-".repeat(fill)) + hint;
423
413
  }
@@ -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.
@@ -380,6 +380,18 @@ function isMessagesRequest(message: ParsedHttpMessage): boolean {
380
380
  return pathNameFromRequestTarget(message.path ?? "") === "/v1/messages";
381
381
  }
382
382
 
383
+ // Claude Code fires a background warmup/classification call on its small fast
384
+ // model (a haiku variant, ANTHROPIC_SMALL_FAST_MODEL) before sending the user's
385
+ // real message. Skip it so the capture lands on the actual prompt.
386
+ function isBackgroundModelRequest(message: ParsedHttpMessage): boolean {
387
+ try {
388
+ const parsed = JSON.parse(decodeBody(message.headers, message.body)) as { model?: unknown };
389
+ return typeof parsed.model === "string" && parsed.model.toLowerCase().includes("haiku");
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+
383
395
  function decodeBody(headers: readonly HeaderEntry[], body: Buffer): string {
384
396
  const encoding = headerValue(headers, "content-encoding")?.toLowerCase().trim();
385
397
  try {
@@ -636,7 +648,7 @@ export class ClaudeMessagesProxy {
636
648
  upstreamTls.write(data);
637
649
  const messages = requestParser.push(data);
638
650
  for (const message of messages) {
639
- if (!isMessagesRequest(message)) {
651
+ if (!isMessagesRequest(message) || isBackgroundModelRequest(message)) {
640
652
  responseQueue.push(null);
641
653
  continue;
642
654
  }
@@ -354,6 +354,7 @@ async function handleInstall(
354
354
  console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`));
355
355
  console.error(chalk.dim(` ${APP_NAME} plugin install github:user/repo`));
356
356
  console.error(chalk.dim(` ${APP_NAME} plugin install https://github.com/user/repo#v1.0`));
357
+ console.error(chalk.dim(` ${APP_NAME} plugin install ./path/to/local/plugin`));
357
358
  process.exit(1);
358
359
  }
359
360
 
@@ -382,6 +383,49 @@ async function handleInstall(
382
383
  continue;
383
384
  }
384
385
 
386
+ if (target.type === "local") {
387
+ // Local paths route to link(): symlink the directory into the plugins
388
+ // node_modules tree so source edits show up without a reinstall. Matches
389
+ // `omp plugin link <path>` so users can use either verb interchangeably.
390
+ if (flags.scope) {
391
+ console.error(
392
+ chalk.yellow(
393
+ `Warning: --scope is only supported for marketplace installs (name@marketplace). Ignoring for ${spec}.`,
394
+ ),
395
+ );
396
+ }
397
+ if (flags.force) {
398
+ console.error(
399
+ chalk.yellow(
400
+ `Warning: --force has no effect for local path installs (link is already idempotent). Ignoring for ${spec}.`,
401
+ ),
402
+ );
403
+ }
404
+ if (flags.dryRun) {
405
+ if (flags.json) {
406
+ console.log(JSON.stringify({ dryRun: true, action: "link", path: target.path }, null, 2));
407
+ } else {
408
+ console.log(chalk.dim(`[dry-run] Would link ${spec}`));
409
+ }
410
+ continue;
411
+ }
412
+ try {
413
+ const result = await manager.link(target.path);
414
+ if (flags.json) {
415
+ console.log(JSON.stringify(result, null, 2));
416
+ } else {
417
+ console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${spec}`));
418
+ if (result.manifest.description) {
419
+ console.log(chalk.dim(` ${result.manifest.description}`));
420
+ }
421
+ }
422
+ } catch (err) {
423
+ console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
424
+ process.exit(1);
425
+ }
426
+ continue;
427
+ }
428
+
385
429
  // --scope only applies to marketplace installs; warn when it would be silently no-op'd for npm.
386
430
  if (flags.scope) {
387
431
  console.error(
@@ -923,6 +967,7 @@ ${chalk.bold("Sources:")}
923
967
  github:user/repo[#ref] GitHub shorthand (also gitlab:, bitbucket:, codeberg:, sourcehut:)
924
968
  https://github.com/user/repo Full git URL (https, ssh, or git protocol)
925
969
  name@marketplace Marketplace plugin (see marketplace command)
970
+ ./path, ../path, /abs, ~/path Local plugin directory (symlinked, same as plugin link)
926
971
 
927
972
  ${chalk.bold("Config Subcommands:")}
928
973
  config list <pkg> List all settings
@@ -97,7 +97,6 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
97
97
  const result = await runSearchQuery(params);
98
98
  const component = renderSearchResult(result, { expanded: cmd.expanded, isPartial: false }, theme, {
99
99
  query: cmd.query,
100
- allowLongAnswer: true,
101
100
  maxAnswerLines: cmd.expanded ? undefined : 6,
102
101
  });
103
102