@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.10

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 (36) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli.js +2822 -2872
  3. package/dist/types/collab/host.d.ts +2 -2
  4. package/dist/types/collab/protocol.d.ts +4 -5
  5. package/dist/types/config/model-resolver.d.ts +11 -2
  6. package/dist/types/config/settings-schema.d.ts +12 -2
  7. package/dist/types/session/agent-session.d.ts +13 -0
  8. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  9. package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
  10. package/dist/types/tools/index.d.ts +9 -1
  11. package/dist/types/utils/image-loading.d.ts +12 -0
  12. package/dist/types/utils/qrcode.d.ts +48 -0
  13. package/package.json +12 -12
  14. package/src/cli/args.ts +7 -1
  15. package/src/collab/host.ts +4 -4
  16. package/src/collab/protocol.ts +48 -15
  17. package/src/config/config-file.ts +1 -1
  18. package/src/config/keybindings.ts +2 -2
  19. package/src/config/model-registry.ts +16 -4
  20. package/src/config/model-resolver.ts +193 -35
  21. package/src/config/settings-schema.ts +14 -2
  22. package/src/config/settings.ts +3 -3
  23. package/src/internal-urls/docs-index.generated.txt +1 -1
  24. package/src/main.ts +2 -2
  25. package/src/modes/components/oauth-selector.ts +31 -2
  26. package/src/prompts/tools/inspect-image.md +1 -1
  27. package/src/sdk.ts +26 -7
  28. package/src/session/agent-session.ts +93 -14
  29. package/src/slash-commands/builtin-registry.ts +29 -11
  30. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  31. package/src/thinking.ts +25 -5
  32. package/src/tools/index.ts +10 -1
  33. package/src/tools/inspect-image.ts +72 -9
  34. package/src/utils/file-mentions.ts +5 -2
  35. package/src/utils/image-loading.ts +58 -0
  36. package/src/utils/qrcode.ts +535 -0
@@ -1,6 +1,6 @@
1
1
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
2
2
  import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
3
- import type { FetchImpl, Model, ToolChoice } from "@oh-my-pi/pi-ai";
3
+ import type { FetchImpl, ImageContent, Model, ToolChoice } from "@oh-my-pi/pi-ai";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
5
  import type { AsyncJobManager } from "../async/job-manager";
6
6
  import type { Rule } from "../capability/rule";
@@ -113,6 +113,13 @@ export type ContextFileEntry = {
113
113
  depth?: number;
114
114
  };
115
115
 
116
+ /** Image attachment handle exposed to tools for user-facing labels such as `Image #1`. */
117
+ export type ImageAttachmentEntry = {
118
+ label: string;
119
+ uri: string;
120
+ image: ImageContent;
121
+ };
122
+
116
123
  export type {
117
124
  DiscoverableTool,
118
125
  DiscoverableToolSearchIndex,
@@ -353,6 +360,8 @@ export interface ToolSession {
353
360
  /** Get the active OpenTelemetry config so subagent dispatch can forward
354
361
  * the parent's tracer/hooks with the subagent's own identity stamped. */
355
362
  getTelemetry?: () => AgentTelemetryConfig | undefined;
363
+ /** Return image attachments visible to tools for resolving labels such as `Image #1`. */
364
+ getImageAttachments?: () => ImageAttachmentEntry[];
356
365
  }
357
366
 
358
367
  export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
3
- import { type Api, completeSimple, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
3
+ import { type Api, completeSimple, type ImageContent, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import { type } from "arktype";
6
6
  import { extractTextContent } from "../commit/utils";
@@ -11,6 +11,7 @@ import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-sys
11
11
  import {
12
12
  ImageInputTooLargeError,
13
13
  type LoadedImageInput,
14
+ loadImageAttachmentInput,
14
15
  loadImageInput,
15
16
  MAX_IMAGE_INPUT_BYTES,
16
17
  webpExclusionForModel,
@@ -19,13 +20,62 @@ import type { ToolSession } from "./index";
19
20
  import { ToolError } from "./tool-errors";
20
21
 
21
22
  const inspectImageSchema = type({
22
- path: type("string").describe("image path"),
23
+ path: type("string").describe("image file path, Image #N label, or attachment://N URI"),
23
24
  question: type("string").describe("question about image"),
24
25
  "+": "reject",
25
26
  });
26
27
 
27
28
  export type InspectImageParams = typeof inspectImageSchema.infer;
28
29
 
30
+ interface ImageAttachmentReference {
31
+ index: number;
32
+ }
33
+
34
+ const IMAGE_ATTACHMENT_REFERENCE_REGEX =
35
+ /^\s*(?:\[?Image #([1-9]\d*)(?:,[^\]\n]*)?\]?|(?:attachment|image):\/\/([1-9]\d*))\s*$/i;
36
+
37
+ function parseImageAttachmentReference(path: string): ImageAttachmentReference | null {
38
+ const match = IMAGE_ATTACHMENT_REFERENCE_REGEX.exec(path);
39
+ if (!match) return null;
40
+ const rawIndex = match[1] ?? match[2];
41
+ if (!rawIndex) return null;
42
+ return { index: Number(rawIndex) };
43
+ }
44
+
45
+ function formatAvailableImageAttachments(attachments: readonly { label: string; uri: string }[]): string {
46
+ if (attachments.length === 0) return "none";
47
+ return attachments.map(attachment => `${attachment.label} -> ${attachment.uri}`).join(", ");
48
+ }
49
+
50
+ async function loadAttachmentReferenceInput(options: {
51
+ path: string;
52
+ reference: ImageAttachmentReference;
53
+ attachments: readonly { label: string; uri: string; image: ImageContent }[];
54
+ autoResize: boolean;
55
+ excludeWebP: boolean | undefined;
56
+ }): Promise<LoadedImageInput | null> {
57
+ const attachment = options.attachments[options.reference.index - 1];
58
+ if (!attachment) {
59
+ const available = formatAvailableImageAttachments(options.attachments);
60
+ if (options.attachments.length === 0) {
61
+ throw new ToolError(
62
+ `No image attachments are available in this turn. path="${options.path}" must be a readable file path or attachment URI.`,
63
+ );
64
+ }
65
+ throw new ToolError(
66
+ `Could not resolve image attachment '${options.path}'. Available image attachments: ${available}. Pass an attachment URI or a readable filesystem path.`,
67
+ );
68
+ }
69
+ return loadImageAttachmentInput({
70
+ image: attachment.image,
71
+ label: attachment.label,
72
+ uri: attachment.uri,
73
+ autoResize: options.autoResize,
74
+ maxBytes: MAX_IMAGE_INPUT_BYTES,
75
+ excludeWebP: options.excludeWebP,
76
+ });
77
+ }
78
+
29
79
  export interface InspectImageToolDetails {
30
80
  model: string;
31
81
  imagePath: string;
@@ -129,14 +179,27 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
129
179
  }
130
180
 
131
181
  let imageInput: LoadedImageInput | null;
182
+ const autoResize = this.session.settings.get("images.autoResize");
183
+ const excludeWebP = webpExclusionForModel(model);
184
+ const attachmentReference = parseImageAttachmentReference(params.path);
132
185
  try {
133
- imageInput = await loadImageInput({
134
- path: params.path,
135
- cwd: this.session.cwd,
136
- autoResize: this.session.settings.get("images.autoResize"),
137
- maxBytes: MAX_IMAGE_INPUT_BYTES,
138
- excludeWebP: webpExclusionForModel(model),
139
- });
186
+ if (attachmentReference) {
187
+ imageInput = await loadAttachmentReferenceInput({
188
+ path: params.path,
189
+ reference: attachmentReference,
190
+ attachments: this.session.getImageAttachments?.() ?? [],
191
+ autoResize,
192
+ excludeWebP,
193
+ });
194
+ } else {
195
+ imageInput = await loadImageInput({
196
+ path: params.path,
197
+ cwd: this.session.cwd,
198
+ autoResize,
199
+ maxBytes: MAX_IMAGE_INPUT_BYTES,
200
+ excludeWebP,
201
+ });
202
+ }
140
203
  } catch (error) {
141
204
  if (error instanceof ImageInputTooLargeError) {
142
205
  throw new ToolError(error.message);
@@ -24,7 +24,7 @@ import { resolveReadPath } from "../tools/path-utils";
24
24
  import { formatDimensionNote, resizeImage } from "./image-resize";
25
25
 
26
26
  /** Regex to match @filepath patterns in text */
27
- const FILE_MENTION_REGEX = /@([^\s@]+)/g;
27
+ const FILE_MENTION_REGEX = /@(?:"([^"]+)"|'([^']+)'|([^\s@]+))/g;
28
28
  const LEADING_PUNCTUATION_REGEX = /^[`"'([{<]+/;
29
29
  const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
30
30
  const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
@@ -168,7 +168,10 @@ export function extractFileMentions(text: string): string[] {
168
168
  const index = match.index ?? 0;
169
169
  if (!isMentionBoundary(text, index)) continue;
170
170
 
171
- const cleaned = sanitizeMentionPath(match[1]);
171
+ const rawPath = match[1] ?? match[2] ?? match[3];
172
+ if (!rawPath) continue;
173
+
174
+ const cleaned = match[1] !== undefined || match[2] !== undefined ? rawPath.trim() : sanitizeMentionPath(rawPath);
172
175
  if (!cleaned) continue;
173
176
 
174
177
  mentions.push(cleaned);
@@ -38,6 +38,17 @@ export interface LoadImageInputOptions {
38
38
  excludeWebP?: boolean;
39
39
  }
40
40
 
41
+ /** Options for loading an in-memory chat image attachment as a vision-model input. */
42
+ export interface LoadImageAttachmentInputOptions {
43
+ image: ImageContent;
44
+ label: string;
45
+ uri: string;
46
+ autoResize: boolean;
47
+ maxBytes?: number;
48
+ /** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
49
+ excludeWebP?: boolean;
50
+ }
51
+
41
52
  export interface LoadedImageInput {
42
53
  resolvedPath: string;
43
54
  mimeType: string;
@@ -161,3 +172,50 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
161
172
  bytes: outputBytes,
162
173
  };
163
174
  }
175
+
176
+ /** Loads a chat attachment image through the same size and encoder policy as file-backed image inputs. */
177
+ export async function loadImageAttachmentInput(
178
+ options: LoadImageAttachmentInputOptions,
179
+ ): Promise<LoadedImageInput | null> {
180
+ const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
181
+ if (!SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(options.image.mimeType)) {
182
+ return null;
183
+ }
184
+
185
+ const inputBytes = Buffer.byteLength(options.image.data, "base64");
186
+ if (inputBytes > maxBytes) {
187
+ throw new ImageInputTooLargeError(inputBytes, maxBytes);
188
+ }
189
+
190
+ let outputData = options.image.data;
191
+ let outputMimeType = options.image.mimeType;
192
+ let outputBytes = inputBytes;
193
+ let dimensionNote: string | undefined;
194
+
195
+ const shouldReencodeWebP = options.excludeWebP === true && options.image.mimeType === "image/webp";
196
+ if (options.autoResize || shouldReencodeWebP) {
197
+ try {
198
+ const resized = await resizeImage(options.image, { excludeWebP: options.excludeWebP });
199
+ outputData = resized.data;
200
+ outputMimeType = resized.mimeType;
201
+ outputBytes = resized.buffer.byteLength;
202
+ dimensionNote = formatDimensionNote(resized);
203
+ } catch {
204
+ // keep original image when resize fails
205
+ }
206
+ }
207
+
208
+ let textNote = `Read image attachment ${options.label} [${outputMimeType}]`;
209
+ if (dimensionNote) {
210
+ textNote += `\n${dimensionNote}`;
211
+ }
212
+
213
+ return {
214
+ resolvedPath: options.uri,
215
+ mimeType: outputMimeType,
216
+ data: outputData,
217
+ textNote,
218
+ dimensionNote,
219
+ bytes: outputBytes,
220
+ };
221
+ }