@oh-my-pi/pi-coding-agent 14.0.2 → 14.0.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.0.3] - 2026-04-09
6
+
7
+ ### Fixed
8
+
9
+ - Fixed cached Ollama discovery rows so upgraded installs switch to the OpenAI Responses transport instead of staying on the old completions transport
10
+
5
11
  ## [14.0.2] - 2026-04-09
6
12
  ### Added
7
13
 
@@ -258,6 +264,10 @@
258
264
  - `/autoresearch` toggles like `/plan` when empty; slash completion no longer suggests `off`/`clear` on an empty prefix after the command
259
265
  - Chunk-mode read/edit edge cases (zero-width gap replaces, stale batch diagnostics, grouped Go receivers, line-count headers, parse error locations)
260
266
 
267
+ ### Added
268
+
269
+ - `/review` command now accepts inline args as custom instructions appended to the generated prompt for all structured review modes (PR-style, uncommitted, specific commit). When inline args are provided, option 4 (editor) is suppressed from the menu. The no-UI (Task tool) path forwards args as a focus hint.
270
+
261
271
  ## [13.19.0] - 2026-04-05
262
272
 
263
273
  ### Added
@@ -6890,4 +6900,4 @@ Initial public release.
6890
6900
  - Git branch display in footer
6891
6901
  - Message queueing during streaming responses
6892
6902
  - OAuth integration for Gmail and Google Calendar access
6893
- - HTML export with syntax highlighting and collapsible sections
6903
+ - HTML export with syntax highlighting and collapsible sections
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": "14.0.2",
4
+ "version": "14.0.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.16.1",
48
48
  "@mozilla/readability": "^0.6",
49
- "@oh-my-pi/omp-stats": "14.0.2",
50
- "@oh-my-pi/pi-agent-core": "14.0.2",
51
- "@oh-my-pi/pi-ai": "14.0.2",
52
- "@oh-my-pi/pi-natives": "14.0.2",
53
- "@oh-my-pi/pi-tui": "14.0.2",
54
- "@oh-my-pi/pi-utils": "14.0.2",
49
+ "@oh-my-pi/omp-stats": "14.0.3",
50
+ "@oh-my-pi/pi-agent-core": "14.0.3",
51
+ "@oh-my-pi/pi-ai": "14.0.3",
52
+ "@oh-my-pi/pi-natives": "14.0.3",
53
+ "@oh-my-pi/pi-tui": "14.0.3",
54
+ "@oh-my-pi/pi-utils": "14.0.3",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
@@ -947,7 +947,10 @@ export class ModelRegistry {
947
947
  }
948
948
  const models = this.#applyProviderModelOverrides(
949
949
  providerConfig.provider,
950
- this.#applyProviderCompat(providerConfig.compat, cache.models),
950
+ this.#normalizeDiscoverableModels(
951
+ providerConfig,
952
+ this.#applyProviderCompat(providerConfig.compat, cache.models),
953
+ ),
951
954
  );
952
955
  cachedModels.push(...models);
953
956
  this.#providerDiscoveryStates.set(providerConfig.provider, {
@@ -967,11 +970,19 @@ export class ModelRegistry {
967
970
  return models.map(model => ({ ...model, compat: mergeCompat(model.compat, compat) }));
968
971
  }
969
972
 
973
+ #normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
974
+ if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
975
+ return models;
976
+ }
977
+
978
+ return models.map(model => (model.api === "openai-completions" ? { ...model, api: "openai-responses" } : model));
979
+ }
980
+
970
981
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
971
982
  if (!configuredProviders.has("ollama")) {
972
983
  this.#discoverableProviders.push({
973
984
  provider: "ollama",
974
- api: "openai-completions",
985
+ api: "openai-responses",
975
986
  baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
976
987
  discovery: { type: "ollama" },
977
988
  optional: true,
@@ -1203,7 +1214,10 @@ export class ModelRegistry {
1203
1214
  }
1204
1215
  return this.#applyProviderModelOverrides(
1205
1216
  providerId,
1206
- this.#applyProviderCompat(providerConfig.compat, result.models),
1217
+ this.#normalizeDiscoverableModels(
1218
+ providerConfig,
1219
+ this.#applyProviderCompat(providerConfig.compat, result.models),
1220
+ ),
1207
1221
  );
1208
1222
  }
1209
1223
 
@@ -312,7 +312,7 @@ export const chunkToolEditSchema = Type.Object({
312
312
  "Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
313
313
  }),
314
314
  content: Type.String({
315
- description: "New content. Use one leading space per indent level; do not include the chunk's base padding.",
315
+ description: "New content. Use \\t for indentation. Do NOT include the chunk's base padding.",
316
316
  }),
317
317
  });
318
318
  export const chunkEditParamsSchema = Type.Object(
@@ -197,7 +197,7 @@ const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than t
197
197
  /**
198
198
  * Build the full review prompt with diff stats and distribution guidance.
199
199
  */
200
- function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): string {
200
+ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, additionalInstructions?: string): string {
201
201
  const agentCount = getRecommendedAgentCount(stats);
202
202
  const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
203
203
  const totalLines = stats.totalAdded + stats.totalRemoved;
@@ -221,6 +221,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
221
221
  skipDiff,
222
222
  rawDiff: rawDiff.trim(),
223
223
  linesPerFile,
224
+ additionalInstructions,
224
225
  });
225
226
  }
226
227
 
@@ -230,17 +231,30 @@ export class ReviewCommand implements CustomCommand {
230
231
 
231
232
  constructor(private api: CustomCommandAPI) {}
232
233
 
233
- async execute(_args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
+ async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
235
  if (!ctx.hasUI) {
235
- return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
236
+ const base = "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
237
+ return args.length > 0 ? `${base} Focus: ${args.join(" ")}` : base;
236
238
  }
237
239
 
238
- const mode = await ctx.ui.select("Review Mode", [
239
- "1. Review against a base branch (PR Style)",
240
- "2. Review uncommitted changes",
241
- "3. Review a specific commit",
242
- "4. Custom review instructions",
243
- ]);
240
+ // Inline args act as additional instructions appended to the generated prompt.
241
+ // When present, skip option 4 (editor) — the args already provide the instructions.
242
+ const extraInstructions = args.length > 0 ? args.join(" ") : undefined;
243
+
244
+ const menuItems = extraInstructions
245
+ ? [
246
+ "1. Review against a base branch (PR Style)",
247
+ "2. Review uncommitted changes",
248
+ "3. Review a specific commit",
249
+ ]
250
+ : [
251
+ "1. Review against a base branch (PR Style)",
252
+ "2. Review uncommitted changes",
253
+ "3. Review a specific commit",
254
+ "4. Custom review instructions",
255
+ ];
256
+
257
+ const mode = await ctx.ui.select("Review Mode", menuItems);
244
258
 
245
259
  if (!mode) return undefined;
246
260
 
@@ -282,6 +296,7 @@ export class ReviewCommand implements CustomCommand {
282
296
  `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
283
297
  stats,
284
298
  diffText,
299
+ extraInstructions,
285
300
  );
286
301
  }
287
302
 
@@ -318,7 +333,12 @@ export class ReviewCommand implements CustomCommand {
318
333
  return undefined;
319
334
  }
320
335
 
321
- return buildReviewPrompt("Reviewing uncommitted changes (staged + unstaged)", stats, combinedDiff);
336
+ return buildReviewPrompt(
337
+ "Reviewing uncommitted changes (staged + unstaged)",
338
+ stats,
339
+ combinedDiff,
340
+ extraInstructions,
341
+ );
322
342
  }
323
343
 
324
344
  case 3: {
@@ -354,7 +374,7 @@ export class ReviewCommand implements CustomCommand {
354
374
  return undefined;
355
375
  }
356
376
 
357
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText);
377
+ return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, extraInstructions);
358
378
  }
359
379
 
360
380
  case 4: {
@@ -374,11 +394,12 @@ export class ReviewCommand implements CustomCommand {
374
394
  if (reviewDiff) {
375
395
  const stats = parseDiff(reviewDiff);
376
396
  // Even if all files filtered, include the custom instructions
377
- return `${buildReviewPrompt(
397
+ return buildReviewPrompt(
378
398
  `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
379
399
  stats,
380
400
  reviewDiff,
381
- )}\n\n### Additional Instructions\n\n${instructions}`;
401
+ instructions,
402
+ );
382
403
  }
383
404
 
384
405
  // No diff available, just pass instructions
package/src/lsp/config.ts CHANGED
@@ -197,6 +197,21 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
197
197
  { markers: ["go.mod", "go.sum"], binDir: "bin" },
198
198
  ];
199
199
 
200
+ const WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"] as const;
201
+
202
+ function resolveLocalCommand(basePath: string): string | null {
203
+ if (fs.existsSync(basePath)) return basePath;
204
+ if (process.platform !== "win32") return null;
205
+
206
+ // Package managers write Windows launchers with executable suffixes in node_modules/.bin.
207
+ for (const extension of WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS) {
208
+ const candidate = `${basePath}${extension}`;
209
+ if (fs.existsSync(candidate)) return candidate;
210
+ }
211
+
212
+ return null;
213
+ }
214
+
200
215
  /**
201
216
  * Resolve a command to an executable path.
202
217
  * Checks project-local bin directories first, then falls back to $PATH.
@@ -210,8 +225,9 @@ export function resolveCommand(command: string, cwd: string): string | null {
210
225
  for (const { markers, binDir } of LOCAL_BIN_PATHS) {
211
226
  if (hasRootMarkers(cwd, markers)) {
212
227
  const localPath = path.join(cwd, binDir, command);
213
- if (fs.existsSync(localPath)) {
214
- return localPath;
228
+ const resolvedLocalPath = resolveLocalCommand(localPath);
229
+ if (resolvedLocalPath) {
230
+ return resolvedLocalPath;
215
231
  }
216
232
  }
217
233
  }
@@ -43,6 +43,8 @@ export class SessionObserverOverlayComponent extends Container {
43
43
  #observeKeys: KeyId[];
44
44
  /** Cached parsed transcript per session file to avoid reparsing on every refresh */
45
45
  #transcriptCache?: { path: string; bytesRead: number; entries: SessionMessageEntry[] };
46
+ /** Live stats text component, placed after transcript to avoid above-viewport diffs */
47
+ #statsText?: Text;
46
48
 
47
49
  constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
48
50
  super();
@@ -87,11 +89,13 @@ export class SessionObserverOverlayComponent extends Container {
87
89
  this.#mode = "viewer";
88
90
  this.children = [];
89
91
  this.#viewerContainer = new Container();
92
+ this.#statsText = new Text("", 1, 0);
90
93
  this.#refreshViewer();
91
94
 
92
95
  this.addChild(new DynamicBorder());
93
96
  this.addChild(this.#viewerContainer);
94
97
  this.addChild(new Spacer(1));
98
+ this.addChild(this.#statsText);
95
99
  this.addChild(new Text(theme.fg("dim", "Esc: back to picker | Ctrl+S: back to picker"), 1, 0));
96
100
  this.addChild(new DynamicBorder());
97
101
  }
@@ -133,16 +137,17 @@ export class SessionObserverOverlayComponent extends Container {
133
137
  const session = sessions.find(s => s.id === this.#selectedSessionId);
134
138
  if (!session) {
135
139
  this.#viewerContainer.addChild(new Text(theme.fg("dim", "Session no longer available."), 1, 0));
140
+ this.#updateStats(undefined);
136
141
  return;
137
142
  }
138
143
 
139
144
  this.#renderSessionHeader(session);
140
145
  this.#renderSessionTranscript(session);
146
+ this.#updateStats(session);
141
147
  }
142
148
 
143
149
  #renderSessionHeader(session: ObservableSession): void {
144
150
  const c = this.#viewerContainer;
145
- const progress = session.progress;
146
151
 
147
152
  // Header: label + status + [agent]
148
153
  const statusColor = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
@@ -154,17 +159,6 @@ export class SessionObserverOverlayComponent extends Container {
154
159
  c.addChild(new Text(theme.fg("muted", session.description), 1, 0));
155
160
  }
156
161
 
157
- // Stats from progress
158
- if (progress) {
159
- const stats: string[] = [];
160
- if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
161
- if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
162
- if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
163
- if (stats.length > 0) {
164
- c.addChild(new Text(theme.fg("dim", stats.join(theme.sep.dot)), 1, 0));
165
- }
166
- }
167
-
168
162
  if (session.sessionFile) {
169
163
  c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(session.sessionFile)}`), 1, 0));
170
164
  }
@@ -172,6 +166,21 @@ export class SessionObserverOverlayComponent extends Container {
172
166
  c.addChild(new DynamicBorder());
173
167
  }
174
168
 
169
+ /** Update live stats in-place (below transcript, within viewport). */
170
+ #updateStats(session: ObservableSession | undefined): void {
171
+ if (!this.#statsText) return;
172
+ const progress = session?.progress;
173
+ if (!progress) {
174
+ this.#statsText.setText("");
175
+ return;
176
+ }
177
+ const stats: string[] = [];
178
+ if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
179
+ if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
180
+ if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
181
+ this.#statsText.setText(stats.length > 0 ? theme.fg("dim", stats.join(theme.sep.dot)) : "");
182
+ }
183
+
175
184
  /** Incrementally read and parse the session JSONL, caching already-parsed entries. */
176
185
  #loadTranscript(sessionFile: string): SessionMessageEntry[] | null {
177
186
  // Invalidate cache if session file changed (e.g. switched to different subagent)
@@ -62,3 +62,9 @@ _Full diff too large ({{len files}} files). Showing first ~{{linesPerFile}} line
62
62
  {{rawDiff}}
63
63
  </diff>
64
64
  {{/if}}
65
+
66
+ {{#if additionalInstructions}}
67
+ ### Additional Instructions
68
+
69
+ {{additionalInstructions}}
70
+ {{/if}}
@@ -7,9 +7,9 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
7
7
  - replacements: `chunk#CRC` or `chunk#CRC@region`
8
8
  - Without a `@region` it defaults to the entire chunk including leading trivia. Valid regions: `head`, `body`, `tail`, `decl`.
9
9
  - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
10
- - Use a single leading space per indent level in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
10
+ - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
11
11
  ```
12
- content: "if (x) {\n return true;\n}"
12
+ content: "if (x) {\n\treturn true;\n}"
13
13
  ```
14
14
  The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
15
15
  - `@region` only works on container chunks (classes, functions, impl blocks, sections). Do **not** use `@head`/`@body`/`@tail` on leaf chunks (enum variants, fields, single statements) — use the whole chunk instead.
@@ -19,6 +19,10 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
19
19
 
20
20
  <critical>
21
21
  You **MUST** use the narrowest region that covers your change. Replacing without a region replaces the **entire chunk including leading comments, decorators, and attributes** — omitting them from `content` deletes them.
22
+
23
+ **`replace` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before replacing `@body` on any chunk, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent.
24
+
25
+ **Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). Replacing `@body` on a group chunk replaces **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not replace the parent group.
22
26
  </critical>
23
27
 
24
28
  <regions>
@@ -108,9 +112,9 @@ Given this `read` output for `example.ts`:
108
112
  ```
109
113
 
110
114
  **Replace a whole chunk** (rename a function):
111
- ```
112
- { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n const c = new Counter();\n c.value = start;\n return c;\n}\n" }
113
- ```
115
+ ~~~json
116
+ { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n\tconst c = new Counter();\n\tc.value = start;\n\treturn c;\n}\n" }
117
+ ~~~
114
118
  Result — the entire chunk is rewritten:
115
119
  ```
116
120
  function makeCounter(start: number): Counter {
@@ -158,9 +162,9 @@ function createCounter(initial: number): Counter {
158
162
  ```
159
163
 
160
164
  **Insert after a chunk** (`after`):
161
- ```
162
- { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
163
- ```
165
+ ~~~json
166
+ { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
167
+ ~~~
164
168
  Result — a new function appears after the enum:
165
169
  ```
166
170
  enum Status {
@@ -189,9 +193,9 @@ class Counter {
189
193
  ```
190
194
 
191
195
  **Append inside a container** (`@body` + `append`):
192
- ```
193
- { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
194
- ```
196
+ ~~~json
197
+ { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
198
+ ~~~
195
199
  Result — a new method is added at the end of the class body, before the closing `}`:
196
200
  ```
197
201
  toString(): string {
@@ -210,10 +214,10 @@ Result — a new method is added at the end of the class body, before the closin
210
214
  ```
211
215
  Result — the method is removed from the class.
212
216
  - Indentation rules (important):
213
- - Use one leading space for each indent level in canonical chunk-edit content. The tool expands those levels to the file's actual style (2-space, 4-space, tabs, etc.).
217
+ - Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
214
218
  - Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
215
219
  - For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
216
220
  - For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
217
- - For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
221
+ - For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
218
222
  - The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
219
223
  </examples>