@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 +25 -1
- package/package.json +5 -5
- package/src/core/agent-session.ts +195 -26
- package/src/core/sdk.ts +7 -2
- package/src/core/session-manager.ts +15 -0
- package/src/core/settings-manager.ts +20 -6
- package/src/core/tools/edit.ts +1 -0
- package/src/core/tools/grep.ts +1 -1
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/renderers.ts +1 -0
- package/src/core/tools/task/index.ts +10 -1
- package/src/core/tools/task/model-resolver.ts +5 -4
- package/src/main.ts +3 -0
- package/src/modes/interactive/components/tool-execution.ts +15 -12
- package/src/modes/interactive/interactive-mode.ts +42 -8
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.
|
|
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.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1581
|
-
if (
|
|
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
|
-
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
383
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -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 || "";
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -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",
|
package/src/core/tools/index.ts
CHANGED
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
component
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
1828
|
-
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
|
|
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
|
-
|
|
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
|
}
|