@oh-my-pi/pi-coding-agent 10.5.0 → 10.6.1

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,54 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [10.6.1] - 2026-02-04
6
+
7
+ ### Added
8
+
9
+ - Added `commit` model role for dedicated commit message generation
10
+ - Exported `resolveModelOverride` function from model resolver for external use
11
+
12
+ ### Changed
13
+
14
+ - Updated model role resolution to accept optional `roleOrder` parameter for custom role priority
15
+ - Made `tag` and `color` properties optional in `ModelRoleInfo` interface
16
+ - Updated model selector to safely handle roles without tag or color definitions
17
+ - Refactored role label display to use centralized `MODEL_ROLES` registry instead of hardcoded strings
18
+ - Refactored model role system to use centralized `MODEL_ROLES` registry with consistent tag, name, and color definitions
19
+ - Simplified model role resolution to use `MODEL_ROLE_IDS` array instead of hardcoded role checks
20
+ - Updated model selector to dynamically generate menu actions from `MODEL_ROLES` registry
21
+
22
+ ### Removed
23
+
24
+ - Removed support for `omp/` model role prefix; use `pi/` prefix instead
25
+
26
+ ## [10.6.0] - 2026-02-04
27
+ ### Breaking Changes
28
+
29
+ - Removed `output_mode` parameter from grep tool—results now always use content mode with formatted match output
30
+ - Renamed grep context parameters from `context_pre`/`context_post` to `pre`/`post`
31
+ - Removed `n` (show line numbers) parameter—line numbers are now always displayed in grep results
32
+
33
+ ### Added
34
+
35
+ - Added Jina as a web search provider option alongside Exa, Perplexity, and Anthropic
36
+ - Added support for Jina Reader API integration with automatic provider detection when JINA_API_KEY is configured
37
+
38
+ ### Changed
39
+
40
+ - Reformatted grep output to display matches grouped by file with numbered match headers and aligned context lines
41
+ - Updated grep output to use `>>` prefix for match lines and aligned spacing for context lines for improved readability
42
+ - Changed multiline matching to automatically enable when pattern contains literal newlines (`
43
+ `)
44
+ - Split grep context parameter into separate `context_pre` and `context_post` options for independent control of lines before and after matches
45
+ - Updated grep tool to use configurable default context settings from `grep.contextBefore` and `grep.contextAfter` configuration
46
+ - Added configurable grep context defaults and reduced the default to 1 line before, 3 lines after
47
+ - Enabled the browser tool by default
48
+
49
+ ### Removed
50
+
51
+ - Removed `filesWithMatches` and `count` output modes from grep tool
52
+
5
53
  ## [10.5.0] - 2026-02-04
6
54
 
7
55
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "10.5.0",
3
+ "version": "10.6.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -80,12 +80,12 @@
80
80
  },
81
81
  "dependencies": {
82
82
  "@mozilla/readability": "0.6.0",
83
- "@oh-my-pi/omp-stats": "10.5.0",
84
- "@oh-my-pi/pi-agent-core": "10.5.0",
85
- "@oh-my-pi/pi-ai": "10.5.0",
86
- "@oh-my-pi/pi-natives": "10.5.0",
87
- "@oh-my-pi/pi-tui": "10.5.0",
88
- "@oh-my-pi/pi-utils": "10.5.0",
83
+ "@oh-my-pi/omp-stats": "10.6.1",
84
+ "@oh-my-pi/pi-agent-core": "10.6.1",
85
+ "@oh-my-pi/pi-ai": "10.6.1",
86
+ "@oh-my-pi/pi-natives": "10.6.1",
87
+ "@oh-my-pi/pi-tui": "10.6.1",
88
+ "@oh-my-pi/pi-utils": "10.6.1",
89
89
  "@openai/agents": "^0.4.5",
90
90
  "@sinclair/typebox": "^0.34.48",
91
91
  "ajv": "^8.17.1",
@@ -1,5 +1,12 @@
1
1
  import type { Api, Model } from "@oh-my-pi/pi-ai";
2
- import { parseModelPattern, parseModelString, SMOL_MODEL_PRIORITY } from "../config/model-resolver";
2
+ import { MODEL_ROLE_IDS } from "../config/model-registry";
3
+ import {
4
+ expandRoleAlias,
5
+ parseModelPattern,
6
+ resolveModelFromSettings,
7
+ resolveModelFromString,
8
+ SMOL_MODEL_PRIORITY,
9
+ } from "../config/model-resolver";
3
10
  import type { Settings } from "../config/settings";
4
11
 
5
12
  export async function resolvePrimaryModel(
@@ -12,9 +19,15 @@ export async function resolvePrimaryModel(
12
19
  ): Promise<{ model: Model<Api>; apiKey: string }> {
13
20
  const available = modelRegistry.getAvailable();
14
21
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
22
+ const roleOrder = ["commit", "smol", ...MODEL_ROLE_IDS] as const;
15
23
  const model = override
16
24
  ? resolveModelFromString(expandRoleAlias(override, settings), available, matchPreferences)
17
- : resolveModelFromSettings(settings, available, matchPreferences);
25
+ : resolveModelFromSettings({
26
+ settings,
27
+ availableModels: available,
28
+ matchPreferences,
29
+ roleOrder,
30
+ });
18
31
  if (!model) {
19
32
  throw new Error("No model available for commit generation");
20
33
  }
@@ -37,7 +50,9 @@ export async function resolveSmolModel(
37
50
  const available = modelRegistry.getAvailable();
38
51
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
39
52
  const role = settings.getModelRole("smol");
40
- const roleModel = role ? resolveModelFromString(role, available, matchPreferences) : undefined;
53
+ const roleModel = role
54
+ ? resolveModelFromString(expandRoleAlias(role, settings), available, matchPreferences)
55
+ : undefined;
41
56
  if (roleModel) {
42
57
  const apiKey = await modelRegistry.getApiKey(roleModel);
43
58
  if (apiKey) return { model: roleModel, apiKey };
@@ -52,39 +67,3 @@ export async function resolveSmolModel(
52
67
 
53
68
  return { model: fallbackModel, apiKey: fallbackApiKey };
54
69
  }
55
-
56
- function resolveModelFromSettings(
57
- settings: Settings,
58
- available: Model<Api>[],
59
- matchPreferences: { usageOrder?: string[] },
60
- ): Model<Api> | undefined {
61
- const roles = ["commit", "smol", "default"];
62
- for (const role of roles) {
63
- const configured = settings.getModelRole(role);
64
- if (!configured) continue;
65
- const resolved = resolveModelFromString(expandRoleAlias(configured, settings), available, matchPreferences);
66
- if (resolved) return resolved;
67
- }
68
- return available[0];
69
- }
70
-
71
- function resolveModelFromString(
72
- value: string,
73
- available: Model<Api>[],
74
- matchPreferences: { usageOrder?: string[] },
75
- ): Model<Api> | undefined {
76
- const parsed = parseModelString(value);
77
- if (parsed) {
78
- return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
79
- }
80
- return parseModelPattern(value, available, matchPreferences).model;
81
- }
82
-
83
- function expandRoleAlias(value: string, settings: Settings): string {
84
- const lower = value.toLowerCase();
85
- if (lower.startsWith("pi/") || lower.startsWith("omp/")) {
86
- const role = lower.startsWith("pi/") ? value.slice(3) : value.slice(4);
87
- return settings.getModelRole(role) ?? value;
88
- }
89
- return value;
90
- }
@@ -15,8 +15,27 @@ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
15
15
  import { type Static, Type } from "@sinclair/typebox";
16
16
  import AjvModule from "ajv";
17
17
  import { YAML } from "bun";
18
+ import type { ThemeColor } from "../modes/theme/theme";
18
19
  import type { AuthStorage } from "../session/auth-storage";
19
20
 
21
+ export type ModelRole = "default" | "smol" | "slow" | "plan" | "commit";
22
+
23
+ export interface ModelRoleInfo {
24
+ tag?: string;
25
+ name: string;
26
+ color?: ThemeColor;
27
+ }
28
+
29
+ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
30
+ default: { tag: "DEFAULT", name: "Default", color: "success" },
31
+ smol: { tag: "SMOL", name: "Fast", color: "warning" },
32
+ slow: { tag: "SLOW", name: "Thinking", color: "accent" },
33
+ plan: { tag: "PLAN", name: "Architect", color: "muted" },
34
+ commit: { name: "Commit" },
35
+ };
36
+
37
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "plan", "commit"];
38
+
20
39
  const Ajv = (AjvModule as any).default || AjvModule;
21
40
 
22
41
  const OpenRouterRoutingSchema = Type.Object({
@@ -6,7 +6,8 @@ import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my
6
6
  import chalk from "chalk";
7
7
  import { isValidThinkingLevel } from "../cli/args";
8
8
  import { fuzzyMatch } from "../utils/fuzzy";
9
- import type { ModelRegistry } from "./model-registry";
9
+ import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
10
+ import type { Settings } from "./settings";
10
11
 
11
12
  /** Default model IDs for each known provider */
12
13
  export const defaultModelPerProvider: Record<KnownProvider, string> = {
@@ -310,6 +311,96 @@ export function parseModelPattern(
310
311
  return parseModelPatternWithContext(pattern, availableModels, context);
311
312
  }
312
313
 
314
+ const PREFIX_MODEL_ROLE = "pi/";
315
+ const DEFAULT_MODEL_ROLE = "default";
316
+
317
+ /**
318
+ * Check if a model override value is effectively the default role.
319
+ */
320
+ export function isDefaultModelAlias(value: string | string[] | undefined): boolean {
321
+ if (!value) return true;
322
+ if (Array.isArray(value)) return value.every(entry => isDefaultModelAlias(entry));
323
+ if (value.startsWith(PREFIX_MODEL_ROLE)) {
324
+ value = value.slice(PREFIX_MODEL_ROLE.length);
325
+ }
326
+ return value === DEFAULT_MODEL_ROLE;
327
+ }
328
+
329
+ /**
330
+ * Expand a role alias like "pi/smol" to the configured model string.
331
+ */
332
+ export function expandRoleAlias(value: string, settings?: Settings): string {
333
+ const normalized = value.trim();
334
+ if (normalized === "default") return settings?.getModelRole("default") ?? value;
335
+ if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return value;
336
+ const role = normalized.slice(PREFIX_MODEL_ROLE.length) as ModelRole;
337
+ if (!MODEL_ROLE_IDS.includes(role)) return value;
338
+ return settings?.getModelRole(role) ?? value;
339
+ }
340
+
341
+ /**
342
+ * Resolve a model identifier or pattern to a Model instance.
343
+ */
344
+ export function resolveModelFromString(
345
+ value: string,
346
+ available: Model<Api>[],
347
+ matchPreferences?: ModelMatchPreferences,
348
+ ): Model<Api> | undefined {
349
+ const parsed = parseModelString(value);
350
+ if (parsed) {
351
+ return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
352
+ }
353
+ return parseModelPattern(value, available, matchPreferences).model;
354
+ }
355
+
356
+ /**
357
+ * Resolve a model from configured roles, honoring order and overrides.
358
+ */
359
+ export function resolveModelFromSettings(options: {
360
+ settings: Settings;
361
+ availableModels: Model<Api>[];
362
+ matchPreferences?: ModelMatchPreferences;
363
+ roleOrder?: readonly ModelRole[];
364
+ }): Model<Api> | undefined {
365
+ const { settings, availableModels, matchPreferences, roleOrder } = options;
366
+ const roles = roleOrder ?? MODEL_ROLE_IDS;
367
+ for (const role of roles) {
368
+ const configured = settings.getModelRole(role);
369
+ if (!configured) continue;
370
+ const resolved = resolveModelFromString(expandRoleAlias(configured, settings), availableModels, matchPreferences);
371
+ if (resolved) return resolved;
372
+ }
373
+ return availableModels[0];
374
+ }
375
+
376
+ /**
377
+ * Resolve a list of override patterns to the first matching model.
378
+ */
379
+ export function resolveModelOverride(
380
+ modelPatterns: string[],
381
+ modelRegistry: ModelRegistry,
382
+ settings?: Settings,
383
+ ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
384
+ if (modelPatterns.length === 0) return {};
385
+ const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
386
+ for (const pattern of modelPatterns) {
387
+ const normalized = pattern.trim();
388
+ if (!normalized || isDefaultModelAlias(normalized)) {
389
+ continue;
390
+ }
391
+ const effectivePattern = expandRoleAlias(pattern, settings);
392
+ const { model, thinkingLevel } = parseModelPattern(
393
+ effectivePattern,
394
+ modelRegistry.getAvailable(),
395
+ matchPreferences,
396
+ );
397
+ if (model) {
398
+ return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined };
399
+ }
400
+ }
401
+ return {};
402
+ }
403
+
313
404
  /**
314
405
  * Resolve model patterns to actual Model objects with optional thinking levels
315
406
  * Format: "pattern:level" where :level is optional
@@ -329,6 +329,26 @@ export const SETTINGS_SCHEMA = {
329
329
  default: true,
330
330
  ui: { tab: "tools", label: "Enable Grep", description: "Enable the grep tool for content searching" },
331
331
  },
332
+ "grep.contextBefore": {
333
+ type: "number",
334
+ default: 1,
335
+ ui: {
336
+ tab: "tools",
337
+ label: "Grep context before",
338
+ description: "Lines of context before each grep match",
339
+ submenu: true,
340
+ },
341
+ },
342
+ "grep.contextAfter": {
343
+ type: "number",
344
+ default: 3,
345
+ ui: {
346
+ tab: "tools",
347
+ label: "Grep context after",
348
+ description: "Lines of context after each grep match",
349
+ submenu: true,
350
+ },
351
+ },
332
352
  "notebook.enabled": {
333
353
  type: "boolean",
334
354
  default: true,
@@ -360,7 +380,7 @@ export const SETTINGS_SCHEMA = {
360
380
  },
361
381
  "browser.enabled": {
362
382
  type: "boolean",
363
- default: false,
383
+ default: true,
364
384
  ui: {
365
385
  tab: "tools",
366
386
  label: "Enable Browser",
@@ -485,7 +505,7 @@ export const SETTINGS_SCHEMA = {
485
505
  // ─────────────────────────────────────────────────────────────────────────
486
506
  "providers.webSearch": {
487
507
  type: "enum",
488
- values: ["auto", "exa", "perplexity", "anthropic"] as const,
508
+ values: ["auto", "exa", "jina", "perplexity", "anthropic"] as const,
489
509
  default: "auto",
490
510
  ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
491
511
  },
@@ -13,6 +13,7 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
+ import type { ModelRole } from "@oh-my-pi/pi-coding-agent/config/model-registry";
16
17
  import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
17
18
  import { YAML } from "bun";
18
19
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
@@ -387,7 +388,7 @@ export class Settings {
387
388
  /**
388
389
  * Set a model role (helper for modelRoles record).
389
390
  */
390
- setModelRole(role: string, modelId: string): void {
391
+ setModelRole(role: ModelRole | string, modelId: string): void {
391
392
  const current = this.get("modelRoles");
392
393
  this.set("modelRoles", { ...current, [role]: modelId });
393
394
  }
@@ -395,11 +396,31 @@ export class Settings {
395
396
  /**
396
397
  * Get a model role (helper for modelRoles record).
397
398
  */
398
- getModelRole(role: string): string | undefined {
399
+ getModelRole(role: ModelRole | string): string | undefined {
399
400
  const roles = this.get("modelRoles");
400
401
  return roles[role];
401
402
  }
402
403
 
404
+ /**
405
+ * Get all model roles (helper for modelRoles record).
406
+ */
407
+ getModelRoles(): ReadOnlyDict<string> {
408
+ return this.get("modelRoles");
409
+ }
410
+
411
+ /*
412
+ * Override model roles (helper for modelRoles record).
413
+ */
414
+ overrideModelRoles(roles: ReadOnlyDict<string>): void {
415
+ const prev = this.get("modelRoles");
416
+ for (const [role, modelId] of Object.entries(roles)) {
417
+ if (modelId) {
418
+ prev[role] = modelId;
419
+ }
420
+ }
421
+ this.set("modelRoles", prev);
422
+ }
423
+
403
424
  /**
404
425
  * Set disabled providers (for compatibility with discovery system).
405
426
  */
package/src/cursor.ts CHANGED
@@ -167,7 +167,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
167
167
  pattern: args.pattern,
168
168
  path: args.path || undefined,
169
169
  glob: args.glob || undefined,
170
- output_mode: args.outputMode || undefined,
170
+ mode: args.outputMode || undefined,
171
171
  context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
172
172
  ignore_case: args.caseInsensitive || undefined,
173
173
  type: args.type || undefined,
@@ -51,7 +51,7 @@ export interface CustomToolContext {
51
51
  /** Model registry - use for API key resolution and model retrieval */
52
52
  modelRegistry: ModelRegistry;
53
53
  /** Current model (may be undefined if no model is selected yet) */
54
- model: Model<any> | undefined;
54
+ model: Model | undefined;
55
55
  /** Whether the agent is idle (not streaming) */
56
56
  isIdle(): boolean;
57
57
  /** Whether there are queued messages waiting to be processed */
@@ -206,7 +206,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
206
206
  return this.runtime.setActiveTools(toolNames);
207
207
  }
208
208
 
209
- setModel(model: Model<any>): Promise<boolean> {
209
+ setModel(model: Model): Promise<boolean> {
210
210
  return this.runtime.setModel(model);
211
211
  }
212
212
 
@@ -112,7 +112,7 @@ export class ExtensionRunner {
112
112
  private sessionManager: SessionManager;
113
113
  private modelRegistry: ModelRegistry;
114
114
  private errorListeners: Set<ExtensionErrorListener> = new Set();
115
- private getModel: () => Model<any> | undefined = () => undefined;
115
+ private getModel: () => Model | undefined = () => undefined;
116
116
  private isIdleFn: () => boolean = () => true;
117
117
  private waitForIdleFn: () => Promise<void> = async () => {};
118
118
  private abortFn: () => void = () => {};
@@ -162,7 +162,7 @@ export interface ExtensionContext {
162
162
  /** Model registry for API key resolution */
163
163
  modelRegistry: ModelRegistry;
164
164
  /** Current model (may be undefined) */
165
- model: Model<any> | undefined;
165
+ model: Model | undefined;
166
166
  /** Whether the agent is idle (not streaming) */
167
167
  isIdle(): boolean;
168
168
  /** Abort the current agent operation */
@@ -776,7 +776,7 @@ export interface ExtensionAPI {
776
776
  setActiveTools(toolNames: string[]): Promise<void>;
777
777
 
778
778
  /** Set the current model. Returns false if no API key available. */
779
- setModel(model: Model<any>): Promise<boolean>;
779
+ setModel(model: Model): Promise<boolean>;
780
780
 
781
781
  /** Get current thinking level. */
782
782
  getThinkingLevel(): ThinkingLevel;
@@ -835,7 +835,7 @@ export type GetAllToolsHandler = () => string[];
835
835
 
836
836
  export type SetActiveToolsHandler = (toolNames: string[]) => Promise<void>;
837
837
 
838
- export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
838
+ export type SetModelHandler = (model: Model) => Promise<boolean>;
839
839
 
840
840
  export type GetThinkingLevelHandler = () => ThinkingLevel;
841
841
 
@@ -862,7 +862,7 @@ export interface ExtensionActions {
862
862
 
863
863
  /** Actions for ExtensionContext (ctx.* in event handlers). */
864
864
  export interface ExtensionContextActions {
865
- getModel: () => Model<any> | undefined;
865
+ getModel: () => Model | undefined;
866
866
  isIdle: () => boolean;
867
867
  abort: () => void;
868
868
  hasPendingMessages: () => boolean;
@@ -69,7 +69,7 @@ export class HookRunner {
69
69
  private sessionManager: SessionManager;
70
70
  private modelRegistry: ModelRegistry;
71
71
  private errorListeners: Set<HookErrorListener> = new Set();
72
- private getModel: () => Model<any> | undefined = () => undefined;
72
+ private getModel: () => Model | undefined = () => undefined;
73
73
  private isIdleFn: () => boolean = () => true;
74
74
  private waitForIdleFn: () => Promise<void> = async () => {};
75
75
  private abortFn: () => void = () => {};
@@ -93,7 +93,7 @@ export class HookRunner {
93
93
  */
94
94
  initialize(options: {
95
95
  /** Function to get the current model */
96
- getModel: () => Model<any> | undefined;
96
+ getModel: () => Model | undefined;
97
97
  /** Handler for hooks to send messages */
98
98
  sendMessageHandler: SendMessageHandler;
99
99
  /** Handler for hooks to append entries */
@@ -147,7 +147,7 @@ export interface HookContext {
147
147
  /** Model registry - use for API key resolution and model retrieval */
148
148
  modelRegistry: ModelRegistry;
149
149
  /** Current model (may be undefined if no model is selected yet) */
150
- model: Model<any> | undefined;
150
+ model: Model | undefined;
151
151
  /** Whether the agent is idle (not streaming) */
152
152
  isIdle(): boolean;
153
153
  /** Abort the current agent operation (fire-and-forget, does not wait) */
package/src/main.ts CHANGED
@@ -391,8 +391,7 @@ async function buildSessionOptions(
391
391
  process.exit(1);
392
392
  }
393
393
  options.model = model;
394
- const currentRoles = settings.get("modelRoles") as Record<string, string>;
395
- settings.override("modelRoles", { ...currentRoles, default: `${model.provider}/${model.id}` });
394
+ settings.overrideModelRoles({ default: `${model.provider}/${model.id}` });
396
395
  } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
397
396
  const remembered = settings.getModelRole("default");
398
397
  if (remembered) {