@oh-my-pi/pi-coding-agent 16.1.5 → 16.1.7

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.
@@ -44,6 +44,7 @@ export declare const OpenAICompatSchema: import("arktype/internal/variants/objec
44
44
  supportsReasoningParams?: boolean | undefined;
45
45
  alwaysSendMaxTokens?: boolean | undefined;
46
46
  strictResponsesPairing?: boolean | undefined;
47
+ supportsImageDetailOriginal?: boolean | undefined;
47
48
  requiresToolResultId?: boolean | undefined;
48
49
  replayUnsignedThinking?: boolean | undefined;
49
50
  whenThinking?: {
@@ -92,6 +93,7 @@ export declare const OpenAICompatSchema: import("arktype/internal/variants/objec
92
93
  supportsReasoningParams?: boolean | undefined;
93
94
  alwaysSendMaxTokens?: boolean | undefined;
94
95
  strictResponsesPairing?: boolean | undefined;
96
+ supportsImageDetailOriginal?: boolean | undefined;
95
97
  requiresToolResultId?: boolean | undefined;
96
98
  replayUnsignedThinking?: boolean | undefined;
97
99
  } | undefined;
@@ -182,6 +184,7 @@ export declare const ModelOverrideSchema: import("arktype/internal/variants/obje
182
184
  supportsReasoningParams?: boolean | undefined;
183
185
  alwaysSendMaxTokens?: boolean | undefined;
184
186
  strictResponsesPairing?: boolean | undefined;
187
+ supportsImageDetailOriginal?: boolean | undefined;
185
188
  requiresToolResultId?: boolean | undefined;
186
189
  replayUnsignedThinking?: boolean | undefined;
187
190
  whenThinking?: {
@@ -230,6 +233,7 @@ export declare const ModelOverrideSchema: import("arktype/internal/variants/obje
230
233
  supportsReasoningParams?: boolean | undefined;
231
234
  alwaysSendMaxTokens?: boolean | undefined;
232
235
  strictResponsesPairing?: boolean | undefined;
236
+ supportsImageDetailOriginal?: boolean | undefined;
233
237
  requiresToolResultId?: boolean | undefined;
234
238
  replayUnsignedThinking?: boolean | undefined;
235
239
  } | undefined;
@@ -298,6 +302,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
298
302
  supportsReasoningParams?: boolean | undefined;
299
303
  alwaysSendMaxTokens?: boolean | undefined;
300
304
  strictResponsesPairing?: boolean | undefined;
305
+ supportsImageDetailOriginal?: boolean | undefined;
301
306
  requiresToolResultId?: boolean | undefined;
302
307
  replayUnsignedThinking?: boolean | undefined;
303
308
  whenThinking?: {
@@ -346,6 +351,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
346
351
  supportsReasoningParams?: boolean | undefined;
347
352
  alwaysSendMaxTokens?: boolean | undefined;
348
353
  strictResponsesPairing?: boolean | undefined;
354
+ supportsImageDetailOriginal?: boolean | undefined;
349
355
  requiresToolResultId?: boolean | undefined;
350
356
  replayUnsignedThinking?: boolean | undefined;
351
357
  } | undefined;
@@ -444,6 +450,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
444
450
  supportsReasoningParams?: boolean | undefined;
445
451
  alwaysSendMaxTokens?: boolean | undefined;
446
452
  strictResponsesPairing?: boolean | undefined;
453
+ supportsImageDetailOriginal?: boolean | undefined;
447
454
  requiresToolResultId?: boolean | undefined;
448
455
  replayUnsignedThinking?: boolean | undefined;
449
456
  whenThinking?: {
@@ -492,6 +499,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
492
499
  supportsReasoningParams?: boolean | undefined;
493
500
  alwaysSendMaxTokens?: boolean | undefined;
494
501
  strictResponsesPairing?: boolean | undefined;
502
+ supportsImageDetailOriginal?: boolean | undefined;
495
503
  requiresToolResultId?: boolean | undefined;
496
504
  replayUnsignedThinking?: boolean | undefined;
497
505
  } | undefined;
@@ -585,6 +593,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
585
593
  supportsReasoningParams?: boolean | undefined;
586
594
  alwaysSendMaxTokens?: boolean | undefined;
587
595
  strictResponsesPairing?: boolean | undefined;
596
+ supportsImageDetailOriginal?: boolean | undefined;
588
597
  requiresToolResultId?: boolean | undefined;
589
598
  replayUnsignedThinking?: boolean | undefined;
590
599
  whenThinking?: {
@@ -633,6 +642,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
633
642
  supportsReasoningParams?: boolean | undefined;
634
643
  alwaysSendMaxTokens?: boolean | undefined;
635
644
  strictResponsesPairing?: boolean | undefined;
645
+ supportsImageDetailOriginal?: boolean | undefined;
636
646
  requiresToolResultId?: boolean | undefined;
637
647
  replayUnsignedThinking?: boolean | undefined;
638
648
  } | undefined;
@@ -81,6 +81,7 @@ export declare const ModelsConfigFile: ConfigFile<{
81
81
  supportsReasoningParams?: boolean | undefined;
82
82
  alwaysSendMaxTokens?: boolean | undefined;
83
83
  strictResponsesPairing?: boolean | undefined;
84
+ supportsImageDetailOriginal?: boolean | undefined;
84
85
  requiresToolResultId?: boolean | undefined;
85
86
  replayUnsignedThinking?: boolean | undefined;
86
87
  whenThinking?: {
@@ -129,6 +130,7 @@ export declare const ModelsConfigFile: ConfigFile<{
129
130
  supportsReasoningParams?: boolean | undefined;
130
131
  alwaysSendMaxTokens?: boolean | undefined;
131
132
  strictResponsesPairing?: boolean | undefined;
133
+ supportsImageDetailOriginal?: boolean | undefined;
132
134
  requiresToolResultId?: boolean | undefined;
133
135
  replayUnsignedThinking?: boolean | undefined;
134
136
  } | undefined;
@@ -212,6 +214,7 @@ export declare const ModelsConfigFile: ConfigFile<{
212
214
  supportsReasoningParams?: boolean | undefined;
213
215
  alwaysSendMaxTokens?: boolean | undefined;
214
216
  strictResponsesPairing?: boolean | undefined;
217
+ supportsImageDetailOriginal?: boolean | undefined;
215
218
  requiresToolResultId?: boolean | undefined;
216
219
  replayUnsignedThinking?: boolean | undefined;
217
220
  whenThinking?: {
@@ -260,6 +263,7 @@ export declare const ModelsConfigFile: ConfigFile<{
260
263
  supportsReasoningParams?: boolean | undefined;
261
264
  alwaysSendMaxTokens?: boolean | undefined;
262
265
  strictResponsesPairing?: boolean | undefined;
266
+ supportsImageDetailOriginal?: boolean | undefined;
263
267
  requiresToolResultId?: boolean | undefined;
264
268
  replayUnsignedThinking?: boolean | undefined;
265
269
  } | undefined;
@@ -338,6 +342,7 @@ export declare const ModelsConfigFile: ConfigFile<{
338
342
  supportsReasoningParams?: boolean | undefined;
339
343
  alwaysSendMaxTokens?: boolean | undefined;
340
344
  strictResponsesPairing?: boolean | undefined;
345
+ supportsImageDetailOriginal?: boolean | undefined;
341
346
  requiresToolResultId?: boolean | undefined;
342
347
  replayUnsignedThinking?: boolean | undefined;
343
348
  whenThinking?: {
@@ -386,6 +391,7 @@ export declare const ModelsConfigFile: ConfigFile<{
386
391
  supportsReasoningParams?: boolean | undefined;
387
392
  alwaysSendMaxTokens?: boolean | undefined;
388
393
  strictResponsesPairing?: boolean | undefined;
394
+ supportsImageDetailOriginal?: boolean | undefined;
389
395
  requiresToolResultId?: boolean | undefined;
390
396
  replayUnsignedThinking?: boolean | undefined;
391
397
  } | undefined;
@@ -177,7 +177,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
177
177
  * user submits becomes the new loop prompt and resumes iteration.
178
178
  */
179
179
  pauseLoop(): void;
180
- handleLoopCommand(args?: string): Promise<void>;
180
+ handleLoopCommand(args?: string): Promise<string | undefined>;
181
181
  recordLocalSubmission(text: string, imageCount?: number): () => void;
182
182
  withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: {
183
183
  imageCount?: number;
@@ -14,7 +14,20 @@ export type LoopLimitRuntime = {
14
14
  durationMs: number;
15
15
  deadlineMs: number;
16
16
  };
17
- export declare function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string;
17
+ export interface ParsedLoopArgs {
18
+ /** Iteration/duration budget, when the user supplied a leading limit token. */
19
+ limit?: LoopLimitConfig;
20
+ /** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
21
+ prompt?: string;
22
+ }
23
+ /**
24
+ * Parse `/loop` arguments into an optional leading limit plus an optional inline
25
+ * prompt. A token that *looks* like a limit (starts with a digit or sign) but
26
+ * fails to parse is a hard error; anything else is treated as prompt text, so
27
+ * plain prose after `/loop` keeps starting an unbounded loop instead of erroring
28
+ * (the pre-arg-parsing behavior). Returns the error message string on failure.
29
+ */
30
+ export declare function parseLoopLimitArgs(args: string): ParsedLoopArgs | string;
18
31
  export declare function createLoopLimitRuntime(config: LoopLimitConfig | undefined, nowMs?: number): LoopLimitRuntime | undefined;
19
32
  export declare function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
20
33
  export declare function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
@@ -339,7 +339,7 @@ export interface InteractiveModeContext {
339
339
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
340
340
  handleGoalModeCommand(rest?: string): Promise<void>;
341
341
  handleGuidedGoalCommand(rest?: string): Promise<void>;
342
- handleLoopCommand(args?: string): Promise<void>;
342
+ handleLoopCommand(args?: string): Promise<string | undefined>;
343
343
  disableLoopMode(): void;
344
344
  pauseLoop(): void;
345
345
  handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
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": "16.1.5",
4
+ "version": "16.1.7",
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",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.5",
52
- "@oh-my-pi/omp-stats": "16.1.5",
53
- "@oh-my-pi/pi-agent-core": "16.1.5",
54
- "@oh-my-pi/pi-ai": "16.1.5",
55
- "@oh-my-pi/pi-catalog": "16.1.5",
56
- "@oh-my-pi/pi-mnemopi": "16.1.5",
57
- "@oh-my-pi/pi-natives": "16.1.5",
58
- "@oh-my-pi/pi-tui": "16.1.5",
59
- "@oh-my-pi/pi-utils": "16.1.5",
60
- "@oh-my-pi/pi-wire": "16.1.5",
61
- "@oh-my-pi/snapcompact": "16.1.5",
51
+ "@oh-my-pi/hashline": "16.1.7",
52
+ "@oh-my-pi/omp-stats": "16.1.7",
53
+ "@oh-my-pi/pi-agent-core": "16.1.7",
54
+ "@oh-my-pi/pi-ai": "16.1.7",
55
+ "@oh-my-pi/pi-catalog": "16.1.7",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.7",
57
+ "@oh-my-pi/pi-natives": "16.1.7",
58
+ "@oh-my-pi/pi-tui": "16.1.7",
59
+ "@oh-my-pi/pi-utils": "16.1.7",
60
+ "@oh-my-pi/pi-wire": "16.1.7",
61
+ "@oh-my-pi/snapcompact": "16.1.7",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -57,6 +57,7 @@ const OpenAICompatFields = {
57
57
  "supportsReasoningParams?": "boolean",
58
58
  "alwaysSendMaxTokens?": "boolean",
59
59
  "strictResponsesPairing?": "boolean",
60
+ "supportsImageDetailOriginal?": "boolean",
60
61
  // anthropic-messages compat flags (same `compat` slot, per-api interpretation)
61
62
  "requiresToolResultId?": "boolean",
62
63
  "replayUnsignedThinking?": "boolean",
@@ -1128,27 +1128,33 @@ export class InteractiveMode implements InteractiveModeContext {
1128
1128
  this.#cancelLoopAutoSubmit();
1129
1129
  }
1130
1130
 
1131
- async handleLoopCommand(args = ""): Promise<void> {
1131
+ async handleLoopCommand(args = ""): Promise<string | undefined> {
1132
1132
  if (this.loopModeEnabled) {
1133
1133
  this.disableLoopMode();
1134
- return;
1134
+ return undefined;
1135
1135
  }
1136
- const parsedLimit = parseLoopLimitArgs(args);
1137
- if (typeof parsedLimit === "string") {
1138
- this.showError(parsedLimit);
1139
- return;
1136
+ const parsed = parseLoopLimitArgs(args);
1137
+ if (typeof parsed === "string") {
1138
+ this.showError(parsed);
1139
+ return undefined;
1140
1140
  }
1141
1141
  this.loopModeEnabled = true;
1142
1142
  this.loopPrompt = undefined;
1143
- this.loopLimit = createLoopLimitRuntime(parsedLimit);
1143
+ this.loopLimit = createLoopLimitRuntime(parsed.limit);
1144
1144
  this.statusLine.setLoopModeStatus({ enabled: true });
1145
1145
  this.updateEditorTopBorder();
1146
1146
  this.ui.requestRender();
1147
- const limitSuffix = parsedLimit ? ` Limited to ${describeLoopLimit(parsedLimit)}.` : "";
1147
+ const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
1148
1148
  const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
1149
+ const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
1149
1150
  this.showStatus(
1150
- `Loop mode enabled.${limitSuffix}${remainingSuffix} Your next prompt will repeat after each turn. Esc cancels the current iteration; /loop again to disable.`,
1151
+ `Loop mode enabled.${limitSuffix}${remainingSuffix} ${tail} Esc cancels the current iteration; /loop again to disable.`,
1151
1152
  );
1153
+ // Hand any inline prompt back to the dispatcher so the normal submit flow
1154
+ // runs the first iteration — it records the text as the loop prompt and
1155
+ // auto-resubmits it after each yield, identical to typing the prompt right
1156
+ // after enabling loop mode.
1157
+ return parsed.prompt;
1152
1158
  }
1153
1159
 
1154
1160
  recordLocalSubmission(text: string, imageCount = 0): () => void {
@@ -38,53 +38,105 @@ const TIME_UNITS_MS = new Map<string, number>([
38
38
  ["hours", 3_600_000],
39
39
  ]);
40
40
 
41
- export function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string {
42
- const trimmed = args.trim().toLowerCase();
43
- if (!trimmed) return undefined;
41
+ const LOOP_USAGE = "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
44
42
 
45
- const parts = trimmed.split(/\s+/);
46
- if (parts.length > 2) {
47
- return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
48
- }
43
+ export interface ParsedLoopArgs {
44
+ /** Iteration/duration budget, when the user supplied a leading limit token. */
45
+ limit?: LoopLimitConfig;
46
+ /** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
47
+ prompt?: string;
48
+ }
49
49
 
50
- if (parts.length === 2) {
51
- return parseDurationParts(parts[0], parts[1]);
50
+ /**
51
+ * Parse `/loop` arguments into an optional leading limit plus an optional inline
52
+ * prompt. A token that *looks* like a limit (starts with a digit or sign) but
53
+ * fails to parse is a hard error; anything else is treated as prompt text, so
54
+ * plain prose after `/loop` keeps starting an unbounded loop instead of erroring
55
+ * (the pre-arg-parsing behavior). Returns the error message string on failure.
56
+ */
57
+ export function parseLoopLimitArgs(args: string): ParsedLoopArgs | string {
58
+ const trimmed = args.trim();
59
+ if (!trimmed) return {};
60
+
61
+ const firstSpace = trimmed.search(/\s/);
62
+ const firstToken = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
63
+ const rest = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1).trim();
64
+ const token = firstToken.toLowerCase();
65
+
66
+ // Not a limit attempt (prose like "keep going") → unbounded loop, prompt = full args.
67
+ if (!/^[+-]?\d/.test(token)) {
68
+ return { prompt: trimmed };
52
69
  }
53
70
 
54
- const token = parts[0];
55
- const iterationMatch = /^(\d+)$/.exec(token);
56
- if (iterationMatch) {
57
- const iterations = Number(iterationMatch[1]);
58
- if (!Number.isSafeInteger(iterations) || iterations <= 0) {
59
- return "Loop count must be a positive integer.";
71
+ // Bare integer: iteration count, unless the next token is a time unit ("10 minutes").
72
+ if (/^\d+$/.test(token)) {
73
+ if (rest) {
74
+ const restTokens = rest.split(/\s+/);
75
+ const unitMs = TIME_UNITS_MS.get(restTokens[0].toLowerCase());
76
+ if (unitMs !== undefined) {
77
+ const limit = makeDuration(token, unitMs);
78
+ if (typeof limit === "string") return limit;
79
+ return { limit, prompt: restTokens.slice(1).join(" ").trim() || undefined };
80
+ }
60
81
  }
61
- return { kind: "iterations", iterations };
82
+ const limit = makeIterations(token);
83
+ if (typeof limit === "string") return limit;
84
+ return { limit, prompt: rest || undefined };
62
85
  }
63
86
 
64
- const durationMatch = /^(\d+)([a-z]+)$/.exec(token);
65
- if (durationMatch) {
66
- return parseDurationParts(durationMatch[1], durationMatch[2]);
87
+ // Compact / compound duration: "10m", "90s", "1h30m".
88
+ const duration = parseCompoundDuration(token);
89
+ if (duration !== undefined) {
90
+ if (typeof duration === "string") return duration;
91
+ return { limit: duration, prompt: rest || undefined };
67
92
  }
68
93
 
69
- return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
94
+ // Limit-shaped but unparseable ("-1", "1.5h", "10x10").
95
+ return LOOP_USAGE;
70
96
  }
71
97
 
72
- function parseDurationParts(amountText: string, unitText: string): LoopLimitConfig | string {
73
- if (!/^\d+$/.test(amountText)) {
74
- return "Loop duration must use a positive integer amount.";
98
+ function makeIterations(amountText: string): LoopLimitConfig | string {
99
+ const amount = Number(amountText);
100
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
101
+ return "Loop count must be a positive integer.";
75
102
  }
103
+ return { kind: "iterations", iterations: amount };
104
+ }
76
105
 
106
+ function makeDuration(amountText: string, unitMs: number): LoopLimitConfig | string {
77
107
  const amount = Number(amountText);
78
108
  if (!Number.isSafeInteger(amount) || amount <= 0) {
79
109
  return "Loop duration must be positive.";
80
110
  }
111
+ return { kind: "duration", durationMs: amount * unitMs };
112
+ }
81
113
 
82
- const unitMs = TIME_UNITS_MS.get(unitText);
83
- if (unitMs === undefined) {
84
- return "Loop duration unit must be seconds, minutes, or hours.";
114
+ /**
115
+ * Parse a compact duration token such as `10m`, or a compound one like `1h30m`.
116
+ * Returns `undefined` when the token is not duration-shaped, or an error string
117
+ * when it is shaped like a duration but uses an unknown unit / non-positive
118
+ * amount.
119
+ */
120
+ function parseCompoundDuration(token: string): LoopLimitConfig | string | undefined {
121
+ if (!/^(?:\d+[a-z]+)+$/.test(token)) return undefined;
122
+ const segments = token.match(/\d+[a-z]+/g);
123
+ if (!segments) return undefined;
124
+ let totalMs = 0;
125
+ for (const segment of segments) {
126
+ const match = /^(\d+)([a-z]+)$/.exec(segment);
127
+ if (!match) return LOOP_USAGE;
128
+ const unitMs = TIME_UNITS_MS.get(match[2]);
129
+ if (unitMs === undefined) {
130
+ return "Loop duration unit must be seconds, minutes, or hours.";
131
+ }
132
+ const amount = Number(match[1]);
133
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
134
+ return "Loop duration must be positive.";
135
+ }
136
+ totalMs += amount * unitMs;
85
137
  }
86
-
87
- return { kind: "duration", durationMs: amount * unitMs };
138
+ if (totalMs <= 0) return "Loop duration must be positive.";
139
+ return { kind: "duration", durationMs: totalMs };
88
140
  }
89
141
 
90
142
  export function createLoopLimitRuntime(
@@ -362,7 +362,7 @@ export interface InteractiveModeContext {
362
362
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
363
363
  handleGoalModeCommand(rest?: string): Promise<void>;
364
364
  handleGuidedGoalCommand(rest?: string): Promise<void>;
365
- handleLoopCommand(args?: string): Promise<void>;
365
+ handleLoopCommand(args?: string): Promise<string | undefined>;
366
366
  disableLoopMode(): void;
367
367
  pauseLoop(): void;
368
368
  handlePlanApproval(details: PlanApprovalDetails): Promise<void>;