@oh-my-pi/pi-coding-agent 3.34.0 → 3.35.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,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.35.0] - 2026-01-09
6
+ ### Added
7
+
8
+ - Added retry logic with exponential backoff for auto-compaction failures
9
+ - Added fallback to alternative models when auto-compaction fails with the primary model
10
+ - Added support for `pi/<role>` model aliases in task tool (e.g., `pi/slow`, `pi/default`)
11
+ - Added visual cycle indicator when switching between role models showing available roles
12
+ - Added automatic model inheritance for subtasks when parent uses default model
13
+ - Added `--` separator in grep tool to prevent pattern interpretation as flags
14
+
15
+ ### Changed
16
+
17
+ - Changed role model cycling to remember last selected role instead of matching current model
18
+ - Changed edit tool to merge call and result displays into single block
19
+ - Changed model override behavior to persist in settings when explicitly set via CLI
20
+
21
+ ### Fixed
22
+
23
+ - Fixed retry-after parsing from error messages supporting multiple header formats (retry-after, retry-after-ms, x-ratelimit-reset)
24
+ - Fixed image attachments being dropped when steering/follow-up messages are queued during streaming
25
+ - Fixed image auto-resize not applying to clipboard images before sending
26
+ - Fixed clipboard image attachments being dropped when steering/follow-up messages are queued while streaming
27
+ - Fixed clipboard image attachments ignoring the auto-resize setting before sending
28
+
5
29
  ## [3.34.0] - 2026-01-09
6
30
 
7
31
  ### Added
@@ -1954,4 +1978,4 @@ Initial public release.
1954
1978
  - Git branch display in footer
1955
1979
  - Message queueing during streaming responses
1956
1980
  - OAuth integration for Gmail and Google Calendar access
1957
- - HTML export with syntax highlighting and collapsible sections
1981
+ - HTML export with syntax highlighting and collapsible sections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.34.0",
3
+ "version": "3.35.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,10 +39,10 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "3.34.0",
43
- "@oh-my-pi/pi-agent-core": "3.34.0",
44
- "@oh-my-pi/pi-git-tool": "3.34.0",
45
- "@oh-my-pi/pi-tui": "3.34.0",
42
+ "@oh-my-pi/pi-ai": "3.35.0",
43
+ "@oh-my-pi/pi-agent-core": "3.35.0",
44
+ "@oh-my-pi/pi-git-tool": "3.35.0",
45
+ "@oh-my-pi/pi-tui": "3.35.0",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -459,7 +459,10 @@ export class AgentSession {
459
459
  const content = message.content;
460
460
  if (typeof content === "string") return content;
461
461
  const textBlocks = content.filter((c) => c.type === "text");
462
- return textBlocks.map((c) => (c as TextContent).text).join("");
462
+ const text = textBlocks.map((c) => (c as TextContent).text).join("");
463
+ if (text.length > 0) return text;
464
+ const hasImages = content.some((c) => c.type === "image");
465
+ return hasImages ? "[Image]" : "";
463
466
  }
464
467
 
465
468
  /** Find the last assistant message in agent state (including aborted ones) */
@@ -722,9 +725,9 @@ export class AgentSession {
722
725
  );
723
726
  }
724
727
  if (options.streamingBehavior === "followUp") {
725
- await this._queueFollowUp(expandedText);
728
+ await this._queueFollowUp(expandedText, options?.images);
726
729
  } else {
727
- await this._queueSteer(expandedText);
730
+ await this._queueSteer(expandedText, options?.images);
728
731
  }
729
732
  return;
730
733
  }
@@ -953,11 +956,16 @@ export class AgentSession {
953
956
  /**
954
957
  * Internal: Queue a steering message (already expanded, no extension command check).
955
958
  */
956
- private async _queueSteer(text: string): Promise<void> {
957
- this._steeringMessages.push(text);
959
+ private async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {
960
+ const displayText = text || (images && images.length > 0 ? "[Image]" : "");
961
+ this._steeringMessages.push(displayText);
962
+ const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
963
+ if (images && images.length > 0) {
964
+ content.push(...images);
965
+ }
958
966
  this.agent.steer({
959
967
  role: "user",
960
- content: [{ type: "text", text }],
968
+ content,
961
969
  timestamp: Date.now(),
962
970
  });
963
971
  }
@@ -965,11 +973,16 @@ export class AgentSession {
965
973
  /**
966
974
  * Internal: Queue a follow-up message (already expanded, no extension command check).
967
975
  */
968
- private async _queueFollowUp(text: string): Promise<void> {
969
- this._followUpMessages.push(text);
976
+ private async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
977
+ const displayText = text || (images && images.length > 0 ? "[Image]" : "");
978
+ this._followUpMessages.push(displayText);
979
+ const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
980
+ if (images && images.length > 0) {
981
+ content.push(...images);
982
+ }
970
983
  this.agent.followUp({
971
984
  role: "user",
972
- content: [{ type: "text", text }],
985
+ content,
973
986
  timestamp: Date.now(),
974
987
  });
975
988
  }
@@ -1175,7 +1188,7 @@ export class AgentSession {
1175
1188
 
1176
1189
  /**
1177
1190
  * Cycle through configured role models in a fixed order.
1178
- * Skips missing roles and deduplicates models.
1191
+ * Skips missing roles.
1179
1192
  * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
1180
1193
  * @param options - Optional settings: `temporary` to not persist to settings
1181
1194
  */
@@ -1189,7 +1202,6 @@ export class AgentSession {
1189
1202
  const currentModel = this.model;
1190
1203
  if (!currentModel) return undefined;
1191
1204
  const roleModels: Array<{ role: string; model: Model<any> }> = [];
1192
- const seen = new Set<string>();
1193
1205
 
1194
1206
  for (const role of roleOrder) {
1195
1207
  const roleModelStr =
@@ -1208,15 +1220,15 @@ export class AgentSession {
1208
1220
  }
1209
1221
  if (!match) continue;
1210
1222
 
1211
- const key = `${match.provider}/${match.id}`;
1212
- if (seen.has(key)) continue;
1213
- seen.add(key);
1214
1223
  roleModels.push({ role, model: match });
1215
1224
  }
1216
1225
 
1217
1226
  if (roleModels.length <= 1) return undefined;
1218
1227
 
1219
- let currentIndex = roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
1228
+ const lastRole = this.sessionManager.getLastModelChangeRole();
1229
+ let currentIndex = lastRole
1230
+ ? roleModels.findIndex((entry) => entry.role === lastRole)
1231
+ : roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
1220
1232
  if (currentIndex === -1) currentIndex = 0;
1221
1233
 
1222
1234
  const nextIndex = (currentIndex + 1) % roleModels.length;
@@ -1558,6 +1570,60 @@ export class AgentSession {
1558
1570
  }
1559
1571
  }
1560
1572
 
1573
+ private _getModelKey(model: Model<any>): string {
1574
+ return `${model.provider}/${model.id}`;
1575
+ }
1576
+
1577
+ private _resolveRoleModel(
1578
+ role: string,
1579
+ availableModels: Model<any>[],
1580
+ currentModel: Model<any> | undefined,
1581
+ ): Model<any> | undefined {
1582
+ const roleModelStr =
1583
+ role === "default"
1584
+ ? (this.settingsManager.getModelRole("default") ??
1585
+ (currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
1586
+ : this.settingsManager.getModelRole(role);
1587
+
1588
+ if (!roleModelStr) return undefined;
1589
+
1590
+ const parsed = parseModelString(roleModelStr);
1591
+ if (parsed) {
1592
+ return availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
1593
+ }
1594
+ const roleLower = roleModelStr.toLowerCase();
1595
+ return availableModels.find((m) => m.id.toLowerCase() === roleLower);
1596
+ }
1597
+
1598
+ private _getCompactionModelCandidates(availableModels: Model<any>[]): Model<any>[] {
1599
+ const candidates: Model<any>[] = [];
1600
+ const seen = new Set<string>();
1601
+
1602
+ const addCandidate = (model: Model<any> | undefined): void => {
1603
+ if (!model) return;
1604
+ const key = this._getModelKey(model);
1605
+ if (seen.has(key)) return;
1606
+ seen.add(key);
1607
+ candidates.push(model);
1608
+ };
1609
+
1610
+ const currentModel = this.model;
1611
+ addCandidate(this._resolveRoleModel("default", availableModels, currentModel));
1612
+ addCandidate(this._resolveRoleModel("slow", availableModels, currentModel));
1613
+ addCandidate(this._resolveRoleModel("small", availableModels, currentModel));
1614
+ addCandidate(this._resolveRoleModel("smol", availableModels, currentModel));
1615
+
1616
+ const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
1617
+ for (const model of sortedByContext) {
1618
+ if (!seen.has(this._getModelKey(model))) {
1619
+ addCandidate(model);
1620
+ break;
1621
+ }
1622
+ }
1623
+
1624
+ return candidates;
1625
+ }
1626
+
1561
1627
  /**
1562
1628
  * Internal: Run auto-compaction with events.
1563
1629
  */
@@ -1577,8 +1643,8 @@ export class AgentSession {
1577
1643
  return;
1578
1644
  }
1579
1645
 
1580
- const apiKey = await this._modelRegistry.getApiKey(this.model);
1581
- if (!apiKey) {
1646
+ const availableModels = this._modelRegistry.getAvailable();
1647
+ if (availableModels.length === 0) {
1582
1648
  this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
1583
1649
  return;
1584
1650
  }
@@ -1626,14 +1692,68 @@ export class AgentSession {
1626
1692
  tokensBefore = hookCompaction.tokensBefore;
1627
1693
  details = hookCompaction.details;
1628
1694
  } else {
1629
- // Generate compaction result
1630
- const compactResult = await compact(
1631
- preparation,
1632
- this.model,
1633
- apiKey,
1634
- undefined,
1635
- this._autoCompactionAbortController.signal,
1636
- );
1695
+ const candidates = this._getCompactionModelCandidates(availableModels);
1696
+ const retrySettings = this.settingsManager.getRetrySettings();
1697
+ let compactResult: CompactionResult | undefined;
1698
+ let lastError: unknown;
1699
+
1700
+ for (const candidate of candidates) {
1701
+ const apiKey = await this._modelRegistry.getApiKey(candidate);
1702
+ if (!apiKey) continue;
1703
+
1704
+ let attempt = 0;
1705
+ while (true) {
1706
+ try {
1707
+ compactResult = await compact(
1708
+ preparation,
1709
+ candidate,
1710
+ apiKey,
1711
+ undefined,
1712
+ this._autoCompactionAbortController.signal,
1713
+ );
1714
+ break;
1715
+ } catch (error) {
1716
+ if (this._autoCompactionAbortController.signal.aborted) {
1717
+ throw error;
1718
+ }
1719
+
1720
+ const message = error instanceof Error ? error.message : String(error);
1721
+ const retryAfterMs = this._parseRetryAfterMsFromError(message);
1722
+ const shouldRetry =
1723
+ retrySettings.enabled &&
1724
+ attempt < retrySettings.maxRetries &&
1725
+ (retryAfterMs !== undefined || this._isRetryableErrorMessage(message));
1726
+ if (!shouldRetry) {
1727
+ lastError = error;
1728
+ break;
1729
+ }
1730
+
1731
+ const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
1732
+ const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
1733
+ attempt++;
1734
+ logger.warn("Auto-compaction failed, retrying", {
1735
+ attempt,
1736
+ maxRetries: retrySettings.maxRetries,
1737
+ delayMs,
1738
+ retryAfterMs,
1739
+ error: message,
1740
+ });
1741
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1742
+ }
1743
+ }
1744
+
1745
+ if (compactResult) {
1746
+ break;
1747
+ }
1748
+ }
1749
+
1750
+ if (!compactResult) {
1751
+ if (lastError) {
1752
+ throw lastError;
1753
+ }
1754
+ throw new Error("Compaction failed: no available model");
1755
+ }
1756
+
1637
1757
  summary = compactResult.summary;
1638
1758
  firstKeptEntryId = compactResult.firstKeptEntryId;
1639
1759
  tokensBefore = compactResult.tokensBefore;
@@ -1725,12 +1845,61 @@ export class AgentSession {
1725
1845
  if (isContextOverflow(message, contextWindow)) return false;
1726
1846
 
1727
1847
  const err = message.errorMessage;
1848
+ return this._isRetryableErrorMessage(err);
1849
+ }
1850
+
1851
+ private _isRetryableErrorMessage(errorMessage: string): boolean {
1728
1852
  // Match: overloaded_error, rate limit, 429, 500, 502, 503, 504, service unavailable, connection error
1729
1853
  return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error/i.test(
1730
- err,
1854
+ errorMessage,
1731
1855
  );
1732
1856
  }
1733
1857
 
1858
+ private _parseRetryAfterMsFromError(errorMessage: string): number | undefined {
1859
+ const now = Date.now();
1860
+ const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
1861
+ if (retryAfterMsMatch) {
1862
+ return Math.max(0, Number(retryAfterMsMatch[1]));
1863
+ }
1864
+
1865
+ const retryAfterMatch = /retry-after\s*[:=]\s*([^\s,;]+)/i.exec(errorMessage);
1866
+ if (retryAfterMatch) {
1867
+ const value = retryAfterMatch[1];
1868
+ const seconds = Number(value);
1869
+ if (!Number.isNaN(seconds)) {
1870
+ return Math.max(0, seconds * 1000);
1871
+ }
1872
+ const dateMs = Date.parse(value);
1873
+ if (!Number.isNaN(dateMs)) {
1874
+ return Math.max(0, dateMs - now);
1875
+ }
1876
+ }
1877
+
1878
+ const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
1879
+ if (resetMsMatch) {
1880
+ const resetMs = Number(resetMsMatch[1]);
1881
+ if (!Number.isNaN(resetMs)) {
1882
+ if (resetMs > 1_000_000_000_000) {
1883
+ return Math.max(0, resetMs - now);
1884
+ }
1885
+ return Math.max(0, resetMs);
1886
+ }
1887
+ }
1888
+
1889
+ const resetMatch = /x-ratelimit-reset\s*[:=]\s*(\d+)/i.exec(errorMessage);
1890
+ if (resetMatch) {
1891
+ const resetSeconds = Number(resetMatch[1]);
1892
+ if (!Number.isNaN(resetSeconds)) {
1893
+ if (resetSeconds > 1_000_000_000) {
1894
+ return Math.max(0, resetSeconds * 1000 - now);
1895
+ }
1896
+ return Math.max(0, resetSeconds * 1000);
1897
+ }
1898
+ }
1899
+
1900
+ return undefined;
1901
+ }
1902
+
1734
1903
  /**
1735
1904
  * Handle retryable errors with exponential backoff.
1736
1905
  * @returns true if retry was initiated, false if max retries exceeded or disabled
package/src/core/sdk.ts CHANGED
@@ -633,6 +633,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
633
633
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
634
634
  time("discoverContextFiles");
635
635
 
636
+ let agent: Agent;
637
+ let session: AgentSession;
638
+
636
639
  const toolSession: ToolSession = {
637
640
  cwd,
638
641
  hasUI: options.hasUI ?? false,
@@ -643,6 +646,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
643
646
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
644
647
  getSessionSpawns: () => options.spawns ?? "*",
645
648
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
649
+ getActiveModelString: () => {
650
+ const activeModel = agent?.state.model;
651
+ return activeModel ? formatModelString(activeModel) : undefined;
652
+ },
646
653
  settings: settingsManager,
647
654
  };
648
655
 
@@ -782,8 +789,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
782
789
  extensionRunner = new ExtensionRunner(extensionsResult.extensions, cwd, sessionManager, modelRegistry);
783
790
  }
784
791
 
785
- let agent: Agent;
786
- let session: AgentSession;
787
792
  const getSessionContext = () => ({
788
793
  sessionManager,
789
794
  modelRegistry,
@@ -1338,6 +1338,21 @@ export class SessionManager {
1338
1338
  return this.leafId ? this.byId.get(this.leafId) : undefined;
1339
1339
  }
1340
1340
 
1341
+ /**
1342
+ * Get the most recent model role from the current session path.
1343
+ * Returns undefined if no model change has been recorded.
1344
+ */
1345
+ getLastModelChangeRole(): string | undefined {
1346
+ let current = this.getLeafEntry();
1347
+ while (current) {
1348
+ if (current.type === "model_change") {
1349
+ return current.role ?? "default";
1350
+ }
1351
+ current = current.parentId ? this.byId.get(current.parentId) : undefined;
1352
+ }
1353
+ return undefined;
1354
+ }
1355
+
1341
1356
  getEntry(id: string): SessionEntry | undefined {
1342
1357
  return this.byId.get(id);
1343
1358
  }
@@ -371,7 +371,8 @@ export class SettingsManager {
371
371
  private settingsPath: string | null;
372
372
  private cwd: string | null;
373
373
  private globalSettings: Settings;
374
- private settings: Settings;
374
+ private overrides: Settings;
375
+ private settings!: Settings;
375
376
  private persist: boolean;
376
377
 
377
378
  private constructor(settingsPath: string | null, cwd: string | null, initialSettings: Settings, persist: boolean) {
@@ -379,8 +380,8 @@ export class SettingsManager {
379
380
  this.cwd = cwd;
380
381
  this.persist = persist;
381
382
  this.globalSettings = initialSettings;
382
- const projectSettings = this.loadProjectSettings();
383
- this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
383
+ this.overrides = {};
384
+ this.rebuildSettings();
384
385
 
385
386
  // Apply environment variables from settings
386
387
  this.applyEnvironmentVariables();
@@ -474,9 +475,17 @@ export class SettingsManager {
474
475
  return SettingsManager.migrateSettings(merged as Record<string, unknown>);
475
476
  }
476
477
 
478
+ private rebuildSettings(projectSettings?: Settings): void {
479
+ const resolvedProjectSettings = projectSettings ?? this.loadProjectSettings();
480
+ this.settings = normalizeSettings(
481
+ deepMergeSettings(deepMergeSettings(this.globalSettings, resolvedProjectSettings), this.overrides),
482
+ );
483
+ }
484
+
477
485
  /** Apply additional overrides on top of current settings */
478
486
  applyOverrides(overrides: Partial<Settings>): void {
479
- this.settings = normalizeSettings(deepMergeSettings(this.settings, overrides));
487
+ this.overrides = deepMergeSettings(this.overrides, overrides);
488
+ this.rebuildSettings();
480
489
  }
481
490
 
482
491
  private save(): void {
@@ -491,9 +500,9 @@ export class SettingsManager {
491
500
  // Save only global settings (project settings are read-only)
492
501
  writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
493
502
 
494
- // Re-merge project settings into active settings
503
+ // Re-merge project settings into active settings (preserve overrides)
495
504
  const projectSettings = this.loadProjectSettings();
496
- this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
505
+ this.rebuildSettings(projectSettings);
497
506
  } catch (error) {
498
507
  console.error(`Warning: Could not save settings file: ${error}`);
499
508
  }
@@ -523,6 +532,11 @@ export class SettingsManager {
523
532
  this.globalSettings.modelRoles = {};
524
533
  }
525
534
  this.globalSettings.modelRoles[role] = model;
535
+
536
+ if (this.overrides.modelRoles && this.overrides.modelRoles[role] !== undefined) {
537
+ this.overrides.modelRoles[role] = model;
538
+ }
539
+
526
540
  this.save();
527
541
  }
528
542
 
@@ -229,6 +229,7 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
229
229
  }
230
230
 
231
231
  export const editToolRenderer = {
232
+ mergeCallAndResult: true,
232
233
  renderCall(args: EditRenderArgs, uiTheme: Theme): Component {
233
234
  const ui = createToolUIKit(uiTheme);
234
235
  const rawPath = args.file_path || args.path || "";
@@ -196,7 +196,7 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
196
196
  args.push("--type", type);
197
197
  }
198
198
 
199
- args.push(pattern, searchPath);
199
+ args.push("--", pattern, searchPath);
200
200
 
201
201
  const child: Subprocess = Bun.spawn([rgPath, ...args], {
202
202
  stdin: "ignore",
@@ -98,6 +98,8 @@ export interface ToolSession {
98
98
  getSessionSpawns: () => string | null;
99
99
  /** Get resolved model string if explicitly set for this session */
100
100
  getModelString?: () => string | undefined;
101
+ /** Get the current session model string, regardless of how it was chosen */
102
+ getActiveModelString?: () => string | undefined;
101
103
  /** Settings manager (optional) */
102
104
  settings?: {
103
105
  getImageAutoResize(): boolean;
@@ -31,6 +31,7 @@ type ToolRenderer = {
31
31
  theme: Theme,
32
32
  args?: unknown,
33
33
  ) => Component;
34
+ mergeCallAndResult?: boolean;
34
35
  };
35
36
 
36
37
  export const toolRenderers: Record<string, ToolRenderer> = {
@@ -135,7 +135,12 @@ export async function createTaskTool(
135
135
  const startTime = Date.now();
136
136
  const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
137
137
  const { agent: agentName, context, model, output: outputSchema } = params;
138
- const modelOverride = model ?? session.getModelString?.();
138
+
139
+ const isDefaultModelAlias = (value: string | undefined): boolean => {
140
+ if (!value) return true;
141
+ const normalized = value.trim().toLowerCase();
142
+ return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
143
+ };
139
144
 
140
145
  // Validate agent exists
141
146
  const agent = getAgent(agents, agentName);
@@ -156,6 +161,10 @@ export async function createTaskTool(
156
161
  };
157
162
  }
158
163
 
164
+ const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model);
165
+ const sessionModel = shouldInheritSessionModel ? session.getActiveModelString?.() : undefined;
166
+ const modelOverride = model ?? sessionModel ?? session.getModelString?.();
167
+
159
168
  // Handle empty or missing tasks
160
169
  if (!params.tasks || params.tasks.length === 0) {
161
170
  return {
@@ -8,7 +8,7 @@
8
8
  * - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
9
9
  * - Comma fallback: "gpt, opus" → tries gpt first, then opus
10
10
  * - "default" → undefined (use system default)
11
- * - "omp/slow" → configured slow model from settings
11
+ * - "omp/slow" or "pi/slow" → configured slow model from settings
12
12
  */
13
13
 
14
14
  import { type Settings, settingsCapability } from "../../../capability/settings";
@@ -145,9 +145,10 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
145
145
  .filter(Boolean);
146
146
 
147
147
  for (const p of patterns) {
148
- // Handle omp/<role> aliases - looks up role in settings.modelRoles
149
- if (p.toLowerCase().startsWith("omp/")) {
150
- const role = p.slice(4); // Remove "omp/" prefix
148
+ // Handle omp/<role> or pi/<role> aliases - looks up role in settings.modelRoles
149
+ const lower = p.toLowerCase();
150
+ if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
151
+ const role = lower.startsWith("omp/") ? p.slice(4) : p.slice(3);
151
152
  const resolved = resolveOmpAlias(role, models);
152
153
  if (resolved) return resolved;
153
154
  continue; // Role not configured, try next pattern
package/src/main.ts CHANGED
@@ -289,6 +289,9 @@ async function buildSessionOptions(
289
289
  process.exit(1);
290
290
  }
291
291
  options.model = model;
292
+ settingsManager.applyOverrides({
293
+ modelRoles: { default: `${model.provider}/${model.id}` },
294
+ });
292
295
  } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
293
296
  options.model = scopedModels[0].model;
294
297
  }
@@ -369,20 +369,23 @@ export class ToolExecutionComponent extends Container {
369
369
  this.contentBox.setBgFn(bgFn);
370
370
  this.contentBox.clear();
371
371
 
372
- // Render call component
373
- try {
374
- const callComponent = renderer.renderCall(this.args, theme);
375
- if (callComponent) {
376
- // Ensure component has invalidate() method for Component interface
377
- const component = callComponent as any;
378
- if (!component.invalidate) {
379
- component.invalidate = () => {};
372
+ const shouldRenderCall = !this.result || !renderer.mergeCallAndResult;
373
+ if (shouldRenderCall) {
374
+ // Render call component
375
+ try {
376
+ const callComponent = renderer.renderCall(this.args, theme);
377
+ if (callComponent) {
378
+ // Ensure component has invalidate() method for Component interface
379
+ const component = callComponent as any;
380
+ if (!component.invalidate) {
381
+ component.invalidate = () => {};
382
+ }
383
+ this.contentBox.addChild(component);
380
384
  }
381
- this.contentBox.addChild(component);
385
+ } catch {
386
+ // Fall back to default on error
387
+ this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
382
388
  }
383
- } catch {
384
- // Fall back to default on error
385
- this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
386
389
  }
387
390
 
388
391
  // Render result component if we have a result
@@ -38,6 +38,7 @@ import { VoiceSupervisor } from "../../core/voice-supervisor";
38
38
  import { disableProvider, enableProvider } from "../../discovery";
39
39
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
40
40
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
41
+ import { resizeImage } from "../../utils/image-resize";
41
42
  import { registerAsyncCleanup } from "../cleanup";
42
43
  import { ArminComponent } from "./components/armin";
43
44
  import { AssistantMessageComponent } from "./components/assistant-message";
@@ -1139,7 +1140,9 @@ export class InteractiveMode {
1139
1140
  if (this.session.isStreaming) {
1140
1141
  this.editor.addToHistory(text);
1141
1142
  this.editor.setText("");
1142
- await this.session.prompt(text, { streamingBehavior: "steer" });
1143
+ const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
1144
+ this.pendingImages = [];
1145
+ await this.session.prompt(text, { streamingBehavior: "steer", images });
1143
1146
  this.updatePendingMessagesDisplay();
1144
1147
  this.ui.requestRender();
1145
1148
  return;
@@ -1504,22 +1507,24 @@ export class InteractiveMode {
1504
1507
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
1505
1508
  * we update the previous status line instead of appending new ones to avoid log spam.
1506
1509
  */
1507
- private showStatus(message: string): void {
1510
+ private showStatus(message: string, options?: { dim?: boolean }): void {
1508
1511
  if (this.isBackgrounded) {
1509
1512
  return;
1510
1513
  }
1511
1514
  const children = this.chatContainer.children;
1512
1515
  const last = children.length > 0 ? children[children.length - 1] : undefined;
1513
1516
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
1517
+ const useDim = options?.dim ?? true;
1518
+ const rendered = useDim ? theme.fg("dim", message) : message;
1514
1519
 
1515
1520
  if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
1516
- this.lastStatusText.setText(theme.fg("dim", message));
1521
+ this.lastStatusText.setText(rendered);
1517
1522
  this.ui.requestRender();
1518
1523
  return;
1519
1524
  }
1520
1525
 
1521
1526
  const spacer = new Spacer(1);
1522
- const text = new Text(theme.fg("dim", message), 1, 0);
1527
+ const text = new Text(rendered, 1, 0);
1523
1528
  this.chatContainer.addChild(spacer);
1524
1529
  this.chatContainer.addChild(text);
1525
1530
  this.lastStatusSpacer = spacer;
@@ -1822,10 +1827,24 @@ export class InteractiveMode {
1822
1827
  try {
1823
1828
  const image = await readImageFromClipboard();
1824
1829
  if (image) {
1830
+ let imageData = image;
1831
+ if (this.settingsManager.getImageAutoResize()) {
1832
+ try {
1833
+ const resized = await resizeImage({
1834
+ type: "image",
1835
+ data: image.data,
1836
+ mimeType: image.mimeType,
1837
+ });
1838
+ imageData = { data: resized.data, mimeType: resized.mimeType };
1839
+ } catch {
1840
+ imageData = image;
1841
+ }
1842
+ }
1843
+
1825
1844
  this.pendingImages.push({
1826
1845
  type: "image",
1827
- data: image.data,
1828
- mimeType: image.mimeType,
1846
+ data: imageData.data,
1847
+ mimeType: imageData.mimeType,
1829
1848
  });
1830
1849
  // Insert styled placeholder at cursor like Claude does
1831
1850
  const imageNum = this.pendingImages.length;
@@ -1980,7 +1999,8 @@ export class InteractiveMode {
1980
1999
 
1981
2000
  private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
1982
2001
  try {
1983
- const result = await this.session.cycleRoleModels(["slow", "default", "smol"], options);
2002
+ const roleOrder = ["slow", "default", "smol"];
2003
+ const result = await this.session.cycleRoleModels(roleOrder, options);
1984
2004
  if (!result) {
1985
2005
  this.showStatus("Only one role model available");
1986
2006
  return;
@@ -1989,10 +2009,24 @@ export class InteractiveMode {
1989
2009
  this.statusLine.invalidate();
1990
2010
  this.updateEditorBorderColor();
1991
2011
  const roleLabel = result.role === "default" ? "default" : result.role;
2012
+ const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
1992
2013
  const thinkingStr =
1993
2014
  result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
1994
2015
  const tempLabel = options?.temporary ? " (temporary)" : "";
1995
- this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}`);
2016
+ const cycleSeparator = theme.fg("dim", " > ");
2017
+ const cycleLabel = roleOrder
2018
+ .map((role) => {
2019
+ if (role === result.role) {
2020
+ return theme.bold(theme.fg("accent", role));
2021
+ }
2022
+ return theme.fg("muted", role);
2023
+ })
2024
+ .join(cycleSeparator);
2025
+ const orderLabel = ` (cycle: ${cycleLabel})`;
2026
+ this.showStatus(
2027
+ `Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
2028
+ { dim: false },
2029
+ );
1996
2030
  } catch (error) {
1997
2031
  this.showError(error instanceof Error ? error.message : String(error));
1998
2032
  }