@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
@@ -5,19 +5,90 @@ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
5
5
 
6
6
  const BLOB_PREFIX = "blob:sha256:";
7
7
 
8
+ export interface BlobPutOptions {
9
+ /** Optional file extension for a sidecar hardlink/copy that OS openers can type-detect. */
10
+ extension?: string;
11
+ }
12
+
8
13
  export interface BlobPutResult {
9
14
  hash: string;
15
+ /** Canonical content-addressed path, always `<dir>/<sha256-hex>`. */
10
16
  path: string;
17
+ /** Path with the requested extension when supplied, otherwise the canonical path. */
18
+ displayPath: string;
11
19
  get ref(): string;
12
20
  }
13
21
 
14
22
  /**
15
23
  * Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
16
24
  *
17
- * Files are stored at `<dir>/<sha256-hex>` with no extension. The SHA-256 hash is computed
18
- * over the raw binary data (not base64). Content-addressing makes writes idempotent and
19
- * provides automatic deduplication across sessions.
25
+ * Files are stored canonically at `<dir>/<sha256-hex>`. Callers may also request
26
+ * a typed sidecar path (`<dir>/<sha256-hex>.<ext>`) for `file://` links and OS
27
+ * image viewers; blob refs and reads still address the extensionless hash path.
28
+ * The SHA-256 hash is computed over the raw binary data (not base64).
29
+ * Content-addressing makes writes idempotent and provides automatic deduplication
30
+ * across sessions.
20
31
  */
32
+
33
+ const IMAGE_EXTENSION_BY_MIME: Record<string, string> = {
34
+ "image/png": "png",
35
+ "image/jpeg": "jpg",
36
+ "image/jpg": "jpg",
37
+ "image/gif": "gif",
38
+ "image/webp": "webp",
39
+ "image/svg+xml": "svg",
40
+ };
41
+
42
+ function normalizeBlobExtension(extension: string | undefined): string | undefined {
43
+ if (!extension) return undefined;
44
+ const normalized = extension.startsWith(".") ? extension.slice(1) : extension;
45
+ if (normalized.length === 0 || normalized.length > 32) return undefined;
46
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(normalized)) return undefined;
47
+ return normalized.toLowerCase();
48
+ }
49
+
50
+ async function ensureDisplayPath(blobPath: string, displayPath: string, data: Buffer): Promise<void> {
51
+ if (displayPath === blobPath) return;
52
+ try {
53
+ await fsp.link(blobPath, displayPath);
54
+ return;
55
+ } catch (err) {
56
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") return;
57
+ logger.debug("Blob display hardlink failed; falling back to copy", {
58
+ blobPath,
59
+ displayPath,
60
+ error: err instanceof Error ? err.message : String(err),
61
+ });
62
+ }
63
+ await Bun.write(displayPath, data);
64
+ }
65
+
66
+ function ensureDisplayPathSync(blobPath: string, displayPath: string, data: Buffer): void {
67
+ if (displayPath === blobPath) return;
68
+ try {
69
+ fs.linkSync(blobPath, displayPath);
70
+ return;
71
+ } catch (err) {
72
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") return;
73
+ logger.debug("Blob display hardlink failed; falling back to copy", {
74
+ blobPath,
75
+ displayPath,
76
+ error: err instanceof Error ? err.message : String(err),
77
+ });
78
+ }
79
+ fs.writeFileSync(displayPath, data);
80
+ }
81
+
82
+ export function blobExtensionForImageMimeType(mimeType: string | undefined): string | undefined {
83
+ if (!mimeType) return undefined;
84
+ const lower = mimeType.toLowerCase();
85
+ const known = IMAGE_EXTENSION_BY_MIME[lower];
86
+ if (known) return known;
87
+ if (!lower.startsWith("image/")) return undefined;
88
+ const subtype = lower.slice("image/".length).split(";")[0]?.split("+")[0];
89
+ return normalizeBlobExtension(subtype);
90
+ }
91
+
21
92
  export class BlobStore {
22
93
  constructor(readonly dir: string) {}
23
94
 
@@ -25,18 +96,22 @@ export class BlobStore {
25
96
  * Write binary data to the blob store.
26
97
  * @returns SHA-256 hex hash of the data
27
98
  */
28
- async put(data: Buffer): Promise<BlobPutResult> {
99
+ async put(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
29
100
  const hash = new Bun.SHA256().update(data).digest("hex");
30
101
  const blobPath = path.join(this.dir, hash);
102
+ const extension = normalizeBlobExtension(options?.extension);
103
+ const displayPath = extension ? `${blobPath}.${extension}` : blobPath;
31
104
  const result = {
32
105
  hash,
33
106
  path: blobPath,
107
+ displayPath,
34
108
  get ref() {
35
109
  return `${BLOB_PREFIX}${hash}`;
36
110
  },
37
111
  };
38
112
 
39
113
  await Bun.write(blobPath, data);
114
+ await ensureDisplayPath(blobPath, displayPath, data);
40
115
  return result;
41
116
  }
42
117
 
@@ -45,18 +120,22 @@ export class BlobStore {
45
120
  * cannot afford the microtask hops of the async version (e.g. OOM-safe session writes).
46
121
  * Returns once the bytes are in the kernel page cache.
47
122
  */
48
- putSync(data: Buffer): BlobPutResult {
123
+ putSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
49
124
  const hash = new Bun.SHA256().update(data).digest("hex");
50
125
  const blobPath = path.join(this.dir, hash);
126
+ const extension = normalizeBlobExtension(options?.extension);
127
+ const displayPath = extension ? `${blobPath}.${extension}` : blobPath;
51
128
  const result = {
52
129
  hash,
53
130
  path: blobPath,
131
+ displayPath,
54
132
  get ref() {
55
133
  return `${BLOB_PREFIX}${hash}`;
56
134
  },
57
135
  };
58
136
  fs.mkdirSync(this.dir, { recursive: true });
59
137
  fs.writeFileSync(blobPath, data);
138
+ ensureDisplayPathSync(blobPath, displayPath, data);
60
139
  return result;
61
140
  }
62
141
 
@@ -120,17 +199,25 @@ export function externalizeImageDataUrlSync(blobStore: BlobStore, dataUrl: strin
120
199
  * Externalize an image's base64 data to the blob store, returning a blob reference.
121
200
  * If the data is already a blob reference, returns it unchanged.
122
201
  */
123
- export async function externalizeImageData(blobStore: BlobStore, base64Data: string): Promise<string> {
202
+ export async function externalizeImageData(
203
+ blobStore: BlobStore,
204
+ base64Data: string,
205
+ mimeType?: string,
206
+ ): Promise<string> {
124
207
  if (isBlobRef(base64Data)) return base64Data;
125
208
  const buffer = Buffer.from(base64Data, "base64");
126
- const { ref } = await blobStore.put(buffer);
209
+ const { ref } = await blobStore.put(buffer, {
210
+ extension: blobExtensionForImageMimeType(mimeType),
211
+ });
127
212
  return ref;
128
213
  }
129
214
 
130
215
  /** Synchronous variant of {@link externalizeImageData}. */
131
- export function externalizeImageDataSync(blobStore: BlobStore, base64Data: string): string {
216
+ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: string, mimeType?: string): string {
132
217
  if (isBlobRef(base64Data)) return base64Data;
133
- return blobStore.putSync(Buffer.from(base64Data, "base64")).ref;
218
+ return blobStore.putSync(Buffer.from(base64Data, "base64"), {
219
+ extension: blobExtensionForImageMimeType(mimeType),
220
+ }).ref;
134
221
  }
135
222
 
136
223
  /**
@@ -29,6 +29,7 @@ import {
29
29
  } from "@oh-my-pi/pi-utils";
30
30
  import { ArtifactManager } from "./artifacts";
31
31
  import {
32
+ type BlobPutOptions,
32
33
  type BlobPutResult,
33
34
  BlobStore,
34
35
  externalizeImageData,
@@ -336,6 +337,7 @@ export type ReadonlySessionManager = Pick<
336
337
  | "getTree"
337
338
  | "getUsageStatistics"
338
339
  | "putBlob"
340
+ | "putBlobSync"
339
341
  >;
340
342
 
341
343
  function createSessionId(): string {
@@ -1219,7 +1221,7 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
1219
1221
  if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
1220
1222
  if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
1221
1223
  changed = true;
1222
- const blobRef = await externalizeImageData(blobStore, item.data);
1224
+ const blobRef = await externalizeImageData(blobStore, item.data, item.mimeType);
1223
1225
  return { ...item, data: blobRef };
1224
1226
  }
1225
1227
  }
@@ -1313,13 +1315,15 @@ function truncateForPersistenceSync(obj: unknown, blobStore: BlobStore, key?: st
1313
1315
  const result: unknown[] = new Array(obj.length);
1314
1316
  for (let i = 0; i < obj.length; i++) {
1315
1317
  const item = obj[i];
1316
- if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
1317
- if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
1318
- changed = true;
1319
- const blobRef = externalizeImageDataSync(blobStore, item.data);
1320
- result[i] = { ...item, data: blobRef };
1321
- continue;
1322
- }
1318
+ if (
1319
+ key === TEXT_CONTENT_KEY &&
1320
+ isImageBlock(item) &&
1321
+ !isBlobRef(item.data) &&
1322
+ item.data.length >= BLOB_EXTERNALIZE_THRESHOLD
1323
+ ) {
1324
+ changed = true;
1325
+ result[i] = { ...item, data: externalizeImageDataSync(blobStore, item.data, item.mimeType) };
1326
+ continue;
1323
1327
  }
1324
1328
  const newItem = truncateForPersistenceSync(item, blobStore, key);
1325
1329
  if (newItem !== item) changed = true;
@@ -1978,8 +1982,13 @@ export class SessionManager {
1978
1982
  }
1979
1983
 
1980
1984
  /** Puts a binary blob into the blob store and returns the blob reference */
1981
- async putBlob(data: Buffer): Promise<BlobPutResult> {
1982
- return this.#blobStore.put(data);
1985
+ async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
1986
+ return this.#blobStore.put(data, options);
1987
+ }
1988
+
1989
+ /** Synchronous variant of {@link putBlob} for rebuild-only render paths. */
1990
+ putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
1991
+ return this.#blobStore.putSync(data, options);
1983
1992
  }
1984
1993
 
1985
1994
  captureState(): SessionManagerStateSnapshot {
@@ -363,6 +363,8 @@ export interface BuildSystemPromptOptions {
363
363
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
364
364
  /** Whether the local memory://root summary is active. */
365
365
  memoryRootEnabled?: boolean;
366
+ /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
367
+ model?: string;
366
368
  }
367
369
 
368
370
  /** Result of building provider-facing system prompt messages. */
@@ -396,6 +398,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
396
398
  secretsEnabled = false,
397
399
  workspaceTree: providedWorkspaceTree,
398
400
  memoryRootEnabled = false,
401
+ model,
399
402
  } = options;
400
403
  const resolvedCwd = cwd ?? getProjectDir();
401
404
 
@@ -566,6 +569,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
566
569
  date,
567
570
  dateTime,
568
571
  cwd: promptCwd,
572
+ model: model ?? "",
569
573
  intentTracing: !!intentField,
570
574
  intentField: intentField ?? "",
571
575
  mcpDiscoveryMode,
@@ -261,6 +261,11 @@ export class TinyTitleClient {
261
261
  #pending = new Map<string, PendingRequest>();
262
262
  #progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
263
263
  #nextRequestId = 0;
264
+ #spawnWorker: () => WorkerHandle;
265
+
266
+ constructor(spawnWorker: () => WorkerHandle = spawnTinyTitleWorker) {
267
+ this.#spawnWorker = spawnWorker;
268
+ }
264
269
 
265
270
  onProgress(listener: (event: TinyTitleProgressEvent) => void): () => void {
266
271
  this.#progressListeners.add(listener);
@@ -392,7 +397,7 @@ export class TinyTitleClient {
392
397
 
393
398
  #ensureWorker(): WorkerHandle {
394
399
  if (this.#worker) return this.#worker;
395
- const worker = spawnTinyTitleWorker();
400
+ const worker = this.#spawnWorker();
396
401
  this.#worker = worker;
397
402
  this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
398
403
  this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
@@ -429,6 +434,7 @@ export class TinyTitleClient {
429
434
  this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
430
435
  if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
431
436
  else pending.resolve(false);
437
+ void this.terminate();
432
438
  }
433
439
 
434
440
  #emitProgress(event: TinyTitleProgressEvent): void {
@@ -0,0 +1,24 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type { SettingValue } from "../config/settings-schema";
3
+
4
+ export const TOOL_DISCOVERY_AUTO_THRESHOLD = 40;
5
+ export const TOOL_DISCOVERY_SEARCH_TOOL_NAME = "search_tool_bm25";
6
+
7
+ export type ToolDiscoveryModeSetting = SettingValue<"tools.discoveryMode">;
8
+ export type EffectiveToolDiscoveryMode = Exclude<ToolDiscoveryModeSetting, "auto">;
9
+
10
+ export function countToolsForAutoDiscovery(toolNames: Iterable<string>): number {
11
+ let count = 0;
12
+ for (const name of toolNames) {
13
+ if (name !== TOOL_DISCOVERY_SEARCH_TOOL_NAME) count++;
14
+ }
15
+ return count;
16
+ }
17
+
18
+ export function resolveEffectiveToolDiscoveryMode(settings: Settings, toolCount: number): EffectiveToolDiscoveryMode {
19
+ const configuredMode = settings.get("tools.discoveryMode");
20
+ if (configuredMode === "all" || configuredMode === "mcp-only") return configuredMode;
21
+ if (settings.get("mcp.discoveryMode")) return "mcp-only";
22
+ if (configuredMode === "auto" && toolCount > TOOL_DISCOVERY_AUTO_THRESHOLD) return "mcp-only";
23
+ return "off";
24
+ }