@oh-my-pi/pi-coding-agent 8.3.0 → 8.4.0

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.4.0] - 2026-01-25
6
+
7
+ ### Added
8
+ - Added extension API to set working/loading messages during streaming
9
+ - Added task worker propagation of context files, skills, and prompt templates
10
+ - Added subagent option to skip Python preflight checks when Python tooling is unused
11
+ - Model field now accepts string arrays for fallback model prioritization
12
+
13
+ ### Changed
14
+ - Merged patch application warnings into edit tool diagnostics output
15
+ - Cached Python prelude docs for subagent workers to avoid repeated warmups
16
+ - Simplified image placeholders inserted on paste to match Claude-style markers
17
+
18
+ ### Fixed
19
+ - Rewrote empty or corrupted session files to restore valid headers
20
+ - Improved patch applicator ambiguity errors with match previews and overlap detection
21
+ - Fixed Task tool agent model resolution to honor comma-separated model lists
5
22
  ## [8.3.0] - 2026-01-25
6
23
 
7
24
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -75,11 +75,11 @@
75
75
  "test": "bun test"
76
76
  },
77
77
  "dependencies": {
78
- "@oh-my-pi/omp-stats": "8.3.0",
79
- "@oh-my-pi/pi-agent-core": "8.3.0",
80
- "@oh-my-pi/pi-ai": "8.3.0",
81
- "@oh-my-pi/pi-tui": "8.3.0",
82
- "@oh-my-pi/pi-utils": "8.3.0",
78
+ "@oh-my-pi/omp-stats": "8.4.0",
79
+ "@oh-my-pi/pi-agent-core": "8.4.0",
80
+ "@oh-my-pi/pi-ai": "8.4.0",
81
+ "@oh-my-pi/pi-tui": "8.4.0",
82
+ "@oh-my-pi/pi-utils": "8.4.0",
83
83
  "@openai/agents": "^0.4.3",
84
84
  "@sinclair/typebox": "^0.34.46",
85
85
  "ajv": "^8.17.1",
@@ -19,12 +19,18 @@ import type { AuthStorage } from "../session/auth-storage";
19
19
 
20
20
  const Ajv = (AjvModule as any).default || AjvModule;
21
21
 
22
+ const OpenRouterRoutingSchema = Type.Object({
23
+ only: Type.Optional(Type.Array(Type.String())),
24
+ order: Type.Optional(Type.Array(Type.String())),
25
+ });
26
+
22
27
  // Schema for OpenAI compatibility settings
23
28
  const OpenAICompatSchema = Type.Object({
24
29
  supportsStore: Type.Optional(Type.Boolean()),
25
30
  supportsDeveloperRole: Type.Optional(Type.Boolean()),
26
31
  supportsReasoningEffort: Type.Optional(Type.Boolean()),
27
32
  maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
33
+ openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
28
34
  });
29
35
 
30
36
  // Schema for custom model definition
@@ -36,6 +42,7 @@ const ModelDefinitionSchema = Type.Object({
36
42
  Type.Literal("openai-completions"),
37
43
  Type.Literal("openai-responses"),
38
44
  Type.Literal("openai-codex-responses"),
45
+ Type.Literal("azure-openai-responses"),
39
46
  Type.Literal("anthropic-messages"),
40
47
  Type.Literal("google-generative-ai"),
41
48
  Type.Literal("google-vertex"),
@@ -63,6 +70,7 @@ const ProviderConfigSchema = Type.Object({
63
70
  Type.Literal("openai-completions"),
64
71
  Type.Literal("openai-responses"),
65
72
  Type.Literal("openai-codex-responses"),
73
+ Type.Literal("azure-openai-responses"),
66
74
  Type.Literal("anthropic-messages"),
67
75
  Type.Literal("google-generative-ai"),
68
76
  Type.Literal("google-vertex"),
@@ -156,13 +156,23 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
156
156
  return undefined;
157
157
  }
158
158
 
159
+ /**
160
+ * Parse model field into a prioritized list.
161
+ */
162
+ export function parseModelList(value: unknown): string[] | undefined {
163
+ const parsed = parseArrayOrCSV(value);
164
+ if (!parsed) return undefined;
165
+ const normalized = parsed.map(entry => entry.trim()).filter(Boolean);
166
+ return normalized.length > 0 ? normalized : undefined;
167
+ }
168
+
159
169
  /** Parsed agent fields from frontmatter (excludes source/filePath/systemPrompt) */
160
170
  export interface ParsedAgentFields {
161
171
  name: string;
162
172
  description: string;
163
173
  tools?: string[];
164
174
  spawns?: string[] | "*";
165
- model?: string;
175
+ model?: string[];
166
176
  output?: unknown;
167
177
  thinkingLevel?: ThinkingLevel;
168
178
  }
@@ -202,7 +212,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
202
212
  }
203
213
 
204
214
  const output = frontmatter.output !== undefined ? frontmatter.output : undefined;
205
- const model = typeof frontmatter.model === "string" ? frontmatter.model : undefined;
215
+ const model = parseModelList(frontmatter.model);
206
216
  const thinkingLevel = parseThinkingLevel(frontmatter);
207
217
 
208
218
  return { name, description, tools, spawns, model, output, thinkingLevel };
@@ -85,6 +85,7 @@ const noOpUIContext: ExtensionUIContext = {
85
85
  input: async (_title, _placeholder, _dialogOptions) => undefined,
86
86
  notify: () => {},
87
87
  setStatus: () => {},
88
+ setWorkingMessage: () => {},
88
89
  setWidget: () => {},
89
90
  setFooter: () => {},
90
91
  setHeader: () => {},
@@ -69,6 +69,9 @@ export interface ExtensionUIContext {
69
69
  /** Set status text in the footer/status bar. Pass undefined to clear. */
70
70
  setStatus(key: string, text: string | undefined): void;
71
71
 
72
+ /** Set the working/loading message shown during streaming. Call with no argument to restore default. */
73
+ setWorkingMessage(message?: string): void;
74
+
72
75
  /** Set a widget to display above the editor. Accepts string array or component factory. */
73
76
  setWidget(key: string, content: string[] | undefined): void;
74
77
  setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
@@ -187,6 +187,10 @@ export function getPreludeDocs(): PreludeHelper[] {
187
187
  return cachedPreludeDocs ?? [];
188
188
  }
189
189
 
190
+ export function setPreludeDocsCache(docs: PreludeHelper[]): void {
191
+ cachedPreludeDocs = docs;
192
+ }
193
+
190
194
  export function resetPreludeDocsCache(): void {
191
195
  cachedPreludeDocs = null;
192
196
  }
@@ -68,6 +68,7 @@ export class EventController {
68
68
  getSymbolTheme().spinnerFrames,
69
69
  );
70
70
  this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
71
+ this.ctx.applyPendingWorkingMessage();
71
72
  this.ctx.ui.requestRender();
72
73
  break;
73
74
 
@@ -30,6 +30,7 @@ export class ExtensionUiController {
30
30
  input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
31
31
  notify: (message, type) => this.showHookNotify(message, type),
32
32
  setStatus: (key, text) => this.setHookStatus(key, text),
33
+ setWorkingMessage: message => this.ctx.setWorkingMessage(message),
33
34
  setWidget: (key, content) => this.setHookWidget(key, content),
34
35
  setTitle: title => setTerminalTitle(title),
35
36
  custom: (factory, _options) => this.showHookCustom(factory),
@@ -389,6 +390,7 @@ export class ExtensionUiController {
389
390
  input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
390
391
  notify: () => {},
391
392
  setStatus: () => {},
393
+ setWorkingMessage: () => {},
392
394
  setWidget: () => {},
393
395
  setTitle: () => {},
394
396
  custom: async () => undefined as never,
@@ -541,9 +541,9 @@ export class InputController {
541
541
  data: imageData.data,
542
542
  mimeType: imageData.mimeType,
543
543
  });
544
- // Insert styled placeholder at cursor like Claude does
544
+ // Insert placeholder at cursor like Claude does
545
545
  const imageNum = this.ctx.pendingImages.length;
546
- const placeholder = theme.bold(theme.underline(`[Image #${imageNum}]`));
546
+ const placeholder = `[Image #${imageNum}]`;
547
547
  this.ctx.editor.insertText(`${placeholder} `);
548
548
  this.ctx.ui.requestRender();
549
549
  return true;
@@ -102,6 +102,8 @@ export class InteractiveMode implements InteractiveModeContext {
102
102
  public loadingAnimation: Loader | undefined = undefined;
103
103
  public autoCompactionLoader: Loader | undefined = undefined;
104
104
  public retryLoader: Loader | undefined = undefined;
105
+ private pendingWorkingMessage: string | undefined;
106
+ private readonly defaultWorkingMessage = `Working${theme.format.ellipsis} (esc to interrupt)`;
105
107
  public autoCompactionEscapeHandler?: () => void;
106
108
  public retryEscapeHandler?: () => void;
107
109
  public unsubscribe?: () => void;
@@ -160,6 +162,7 @@ export class InteractiveMode implements InteractiveModeContext {
160
162
  this.statusContainer = new Container();
161
163
  this.todoContainer = new Container();
162
164
  this.editor = new CustomEditor(getEditorTheme());
165
+ this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
163
166
  this.editor.onAutocompleteCancel = () => {
164
167
  this.ui.requestRender(true);
165
168
  };
@@ -538,6 +541,33 @@ export class InteractiveMode implements InteractiveModeContext {
538
541
  this.uiHelpers.showWarning(message);
539
542
  }
540
543
 
544
+ setWorkingMessage(message?: string): void {
545
+ if (message === undefined) {
546
+ this.pendingWorkingMessage = undefined;
547
+ if (this.loadingAnimation) {
548
+ this.loadingAnimation.setMessage(this.defaultWorkingMessage);
549
+ }
550
+ return;
551
+ }
552
+
553
+ if (this.loadingAnimation) {
554
+ this.loadingAnimation.setMessage(message);
555
+ return;
556
+ }
557
+
558
+ this.pendingWorkingMessage = message;
559
+ }
560
+
561
+ applyPendingWorkingMessage(): void {
562
+ if (this.pendingWorkingMessage === undefined) {
563
+ return;
564
+ }
565
+
566
+ const message = this.pendingWorkingMessage;
567
+ this.pendingWorkingMessage = undefined;
568
+ this.setWorkingMessage(message);
569
+ }
570
+
541
571
  showNewVersionNotification(newVersion: string): void {
542
572
  this.uiHelpers.showNewVersionNotification(newVersion);
543
573
  }
@@ -187,6 +187,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
187
187
  } as RpcExtensionUIRequest);
188
188
  }
189
189
 
190
+ setWorkingMessage(_message?: string): void {
191
+ // Not supported in RPC mode
192
+ }
193
+
190
194
  setWidget(key: string, content: unknown): void {
191
195
  // Only support string arrays in RPC mode - factory functions are ignored
192
196
  if (content === undefined || Array.isArray(content)) {
@@ -1025,109 +1025,20 @@ function detectColorMode(): ColorMode {
1025
1025
  return "truecolor";
1026
1026
  }
1027
1027
 
1028
- function hexToRgb(hex: string): { r: number; g: number; b: number } {
1029
- const cleaned = hex.replace("#", "");
1030
- if (cleaned.length !== 6) {
1031
- throw new Error(`Invalid hex color: ${hex}`);
1032
- }
1033
- const r = parseInt(cleaned.substring(0, 2), 16);
1034
- const g = parseInt(cleaned.substring(2, 4), 16);
1035
- const b = parseInt(cleaned.substring(4, 6), 16);
1036
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
1037
- throw new Error(`Invalid hex color: ${hex}`);
1038
- }
1039
- return { r, g, b };
1040
- }
1041
-
1042
- // The 6x6x6 color cube channel values (indices 0-5)
1043
- const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
1044
-
1045
- // Grayscale ramp values (indices 232-255, 24 grays from 8 to 238)
1046
- const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
1047
-
1048
- function findClosestCubeIndex(value: number): number {
1049
- let minDist = Infinity;
1050
- let minIdx = 0;
1051
- for (let i = 0; i < CUBE_VALUES.length; i++) {
1052
- const dist = Math.abs(value - CUBE_VALUES[i]);
1053
- if (dist < minDist) {
1054
- minDist = dist;
1055
- minIdx = i;
1056
- }
1028
+ function colorToAnsi(color: string, mode: ColorMode): string {
1029
+ const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
1030
+ const ansi = Bun.color(color, format);
1031
+ if (ansi === null) {
1032
+ throw new Error(`Invalid color value: ${color}`);
1057
1033
  }
1058
- return minIdx;
1059
- }
1060
-
1061
- function findClosestGrayIndex(gray: number): number {
1062
- let minDist = Infinity;
1063
- let minIdx = 0;
1064
- for (let i = 0; i < GRAY_VALUES.length; i++) {
1065
- const dist = Math.abs(gray - GRAY_VALUES[i]);
1066
- if (dist < minDist) {
1067
- minDist = dist;
1068
- minIdx = i;
1069
- }
1070
- }
1071
- return minIdx;
1072
- }
1073
-
1074
- function colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
1075
- // Weighted Euclidean distance (human eye is more sensitive to green)
1076
- const dr = r1 - r2;
1077
- const dg = g1 - g2;
1078
- const db = b1 - b2;
1079
- return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
1080
- }
1081
-
1082
- function rgbTo256(r: number, g: number, b: number): number {
1083
- // Find closest color in the 6x6x6 cube
1084
- const rIdx = findClosestCubeIndex(r);
1085
- const gIdx = findClosestCubeIndex(g);
1086
- const bIdx = findClosestCubeIndex(b);
1087
- const cubeR = CUBE_VALUES[rIdx];
1088
- const cubeG = CUBE_VALUES[gIdx];
1089
- const cubeB = CUBE_VALUES[bIdx];
1090
- const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
1091
- const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);
1092
-
1093
- // Find closest grayscale
1094
- const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
1095
- const grayIdx = findClosestGrayIndex(gray);
1096
- const grayValue = GRAY_VALUES[grayIdx];
1097
- const grayIndex = 232 + grayIdx;
1098
- const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);
1099
-
1100
- // Check if color has noticeable saturation (hue matters)
1101
- // If max-min spread is significant, prefer cube to preserve tint
1102
- const maxC = Math.max(r, g, b);
1103
- const minC = Math.min(r, g, b);
1104
- const spread = maxC - minC;
1105
-
1106
- // Only consider grayscale if color is nearly neutral (spread < 10)
1107
- // AND grayscale is actually closer
1108
- if (spread < 10 && grayDist < cubeDist) {
1109
- return grayIndex;
1110
- }
1111
-
1112
- return cubeIndex;
1113
- }
1114
-
1115
- function hexTo256(hex: string): number {
1116
- const { r, g, b } = hexToRgb(hex);
1117
- return rgbTo256(r, g, b);
1034
+ return ansi;
1118
1035
  }
1119
1036
 
1120
1037
  function fgAnsi(color: string | number, mode: ColorMode): string {
1121
1038
  if (color === "") return "\x1b[39m";
1122
1039
  if (typeof color === "number") return `\x1b[38;5;${color}m`;
1123
- if (color.startsWith("#")) {
1124
- if (mode === "truecolor") {
1125
- const { r, g, b } = hexToRgb(color);
1126
- return `\x1b[38;2;${r};${g};${b}m`;
1127
- } else {
1128
- const index = hexTo256(color);
1129
- return `\x1b[38;5;${index}m`;
1130
- }
1040
+ if (typeof color === "string") {
1041
+ return colorToAnsi(color, mode);
1131
1042
  }
1132
1043
  throw new Error(`Invalid color value: ${color}`);
1133
1044
  }
@@ -1135,16 +1046,8 @@ function fgAnsi(color: string | number, mode: ColorMode): string {
1135
1046
  function bgAnsi(color: string | number, mode: ColorMode): string {
1136
1047
  if (color === "") return "\x1b[49m";
1137
1048
  if (typeof color === "number") return `\x1b[48;5;${color}m`;
1138
- if (color.startsWith("#")) {
1139
- if (mode === "truecolor") {
1140
- const { r, g, b } = hexToRgb(color);
1141
- return `\x1b[48;2;${r};${g};${b}m`;
1142
- } else {
1143
- const index = hexTo256(color);
1144
- return `\x1b[48;5;${index}m`;
1145
- }
1146
- }
1147
- throw new Error(`Invalid color value: ${color}`);
1049
+ const ansi = colorToAnsi(color, mode);
1050
+ return ansi.replace("\x1b[38;", "\x1b[48;");
1148
1051
  }
1149
1052
 
1150
1053
  function resolveVarRefs(
@@ -111,6 +111,8 @@ export interface InteractiveModeContext {
111
111
  queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
112
112
  flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void>;
113
113
  flushPendingBashComponents(): void;
114
+ setWorkingMessage(message?: string): void;
115
+ applyPendingWorkingMessage(): void;
114
116
  isKnownSlashCommand(text: string): boolean;
115
117
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
116
118
  renderSessionContext(