@oh-my-pi/pi-coding-agent 12.4.0 โ 12.5.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 +51 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +4 -1
- package/src/config/settings-schema.ts +58 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/main.ts +7 -1
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +10 -0
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +82 -9
- package/src/session/agent-session.ts +137 -29
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/task/executor.ts +10 -2
package/src/modes/theme/theme.ts
CHANGED
|
@@ -114,6 +114,8 @@ export type SymbolKey =
|
|
|
114
114
|
| "icon.extensionPrompt"
|
|
115
115
|
| "icon.extensionContextFile"
|
|
116
116
|
| "icon.extensionInstruction"
|
|
117
|
+
// STT
|
|
118
|
+
| "icon.mic"
|
|
117
119
|
// Thinking Levels
|
|
118
120
|
| "thinking.minimal"
|
|
119
121
|
| "thinking.low"
|
|
@@ -271,6 +273,8 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
271
273
|
"icon.extensionPrompt": "โ",
|
|
272
274
|
"icon.extensionContextFile": "๐",
|
|
273
275
|
"icon.extensionInstruction": "๐",
|
|
276
|
+
// STT
|
|
277
|
+
"icon.mic": "๐ค",
|
|
274
278
|
// Thinking levels
|
|
275
279
|
"thinking.minimal": "โ min",
|
|
276
280
|
"thinking.low": "โ low",
|
|
@@ -507,6 +511,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
507
511
|
"icon.extensionContextFile": "\uf0f6",
|
|
508
512
|
// pick: ๏ญ | alt: ๏ถ ๏
|
|
509
513
|
"icon.extensionInstruction": "\uf02d",
|
|
514
|
+
// STT - fa-microphone
|
|
515
|
+
"icon.mic": "\uf130",
|
|
510
516
|
// Thinking Levels - emoji labels
|
|
511
517
|
// pick: ๐คจ min | alt: ๏ min ๏ min
|
|
512
518
|
"thinking.minimal": "\u{F0E7} min",
|
|
@@ -676,6 +682,8 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
676
682
|
"icon.extensionPrompt": "PR",
|
|
677
683
|
"icon.extensionContextFile": "CF",
|
|
678
684
|
"icon.extensionInstruction": "IN",
|
|
685
|
+
// STT
|
|
686
|
+
"icon.mic": "MIC",
|
|
679
687
|
// Thinking Levels
|
|
680
688
|
"thinking.minimal": "[min]",
|
|
681
689
|
"thinking.low": "[low]",
|
|
@@ -1374,6 +1382,7 @@ export class Theme {
|
|
|
1374
1382
|
extensionPrompt: this.#symbols["icon.extensionPrompt"],
|
|
1375
1383
|
extensionContextFile: this.#symbols["icon.extensionContextFile"],
|
|
1376
1384
|
extensionInstruction: this.#symbols["icon.extensionInstruction"],
|
|
1385
|
+
mic: this.#symbols["icon.mic"],
|
|
1377
1386
|
};
|
|
1378
1387
|
}
|
|
1379
1388
|
|
|
@@ -1624,7 +1633,8 @@ function detectTerminalBackground(): "dark" | "light" {
|
|
|
1624
1633
|
}
|
|
1625
1634
|
|
|
1626
1635
|
function getDefaultTheme(): string {
|
|
1627
|
-
|
|
1636
|
+
const bg = detectTerminalBackground();
|
|
1637
|
+
return bg === "light" ? autoLightTheme : autoDarkTheme;
|
|
1628
1638
|
}
|
|
1629
1639
|
|
|
1630
1640
|
// ============================================================================
|
|
@@ -1633,10 +1643,20 @@ function getDefaultTheme(): string {
|
|
|
1633
1643
|
|
|
1634
1644
|
export var theme: Theme;
|
|
1635
1645
|
var currentThemeName: string | undefined;
|
|
1646
|
+
|
|
1647
|
+
/** Get the name of the currently active theme. */
|
|
1648
|
+
export function getCurrentThemeName(): string | undefined {
|
|
1649
|
+
return currentThemeName;
|
|
1650
|
+
}
|
|
1636
1651
|
var currentSymbolPresetOverride: SymbolPreset | undefined;
|
|
1637
1652
|
var currentColorBlindMode: boolean = false;
|
|
1638
1653
|
var themeWatcher: fs.FSWatcher | undefined;
|
|
1654
|
+
var sigwinchHandler: (() => void) | undefined;
|
|
1655
|
+
var autoDetectedTheme: boolean = false;
|
|
1656
|
+
var autoDarkTheme: string = "dark";
|
|
1657
|
+
var autoLightTheme: string = "light";
|
|
1639
1658
|
var onThemeChangeCallback: (() => void) | undefined;
|
|
1659
|
+
var themeLoadRequestId: number = 0;
|
|
1640
1660
|
|
|
1641
1661
|
function getCurrentThemeOptions(): CreateThemeOptions {
|
|
1642
1662
|
return {
|
|
@@ -1646,12 +1666,16 @@ function getCurrentThemeOptions(): CreateThemeOptions {
|
|
|
1646
1666
|
}
|
|
1647
1667
|
|
|
1648
1668
|
export async function initTheme(
|
|
1649
|
-
themeName?: string,
|
|
1650
1669
|
enableWatcher: boolean = false,
|
|
1651
1670
|
symbolPreset?: SymbolPreset,
|
|
1652
1671
|
colorBlindMode?: boolean,
|
|
1672
|
+
darkTheme?: string,
|
|
1673
|
+
lightTheme?: string,
|
|
1653
1674
|
): Promise<void> {
|
|
1654
|
-
|
|
1675
|
+
autoDetectedTheme = true;
|
|
1676
|
+
autoDarkTheme = darkTheme ?? "dark";
|
|
1677
|
+
autoLightTheme = lightTheme ?? "light";
|
|
1678
|
+
const name = getDefaultTheme();
|
|
1655
1679
|
currentThemeName = name;
|
|
1656
1680
|
currentSymbolPresetOverride = symbolPreset;
|
|
1657
1681
|
currentColorBlindMode = colorBlindMode ?? false;
|
|
@@ -1659,6 +1683,7 @@ export async function initTheme(
|
|
|
1659
1683
|
theme = await loadTheme(name, getCurrentThemeOptions());
|
|
1660
1684
|
if (enableWatcher) {
|
|
1661
1685
|
await startThemeWatcher();
|
|
1686
|
+
startSigwinchListener();
|
|
1662
1687
|
}
|
|
1663
1688
|
} catch (err) {
|
|
1664
1689
|
logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
|
|
@@ -1672,9 +1697,15 @@ export async function setTheme(
|
|
|
1672
1697
|
name: string,
|
|
1673
1698
|
enableWatcher: boolean = false,
|
|
1674
1699
|
): Promise<{ success: boolean; error?: string }> {
|
|
1700
|
+
autoDetectedTheme = false;
|
|
1675
1701
|
currentThemeName = name;
|
|
1702
|
+
const requestId = ++themeLoadRequestId;
|
|
1676
1703
|
try {
|
|
1677
|
-
|
|
1704
|
+
const loadedTheme = await loadTheme(name, getCurrentThemeOptions());
|
|
1705
|
+
if (requestId !== themeLoadRequestId) {
|
|
1706
|
+
return { success: false, error: "Theme change superseded by a newer request" };
|
|
1707
|
+
}
|
|
1708
|
+
theme = loadedTheme;
|
|
1678
1709
|
if (enableWatcher) {
|
|
1679
1710
|
await startThemeWatcher();
|
|
1680
1711
|
}
|
|
@@ -1683,6 +1714,9 @@ export async function setTheme(
|
|
|
1683
1714
|
}
|
|
1684
1715
|
return { success: true };
|
|
1685
1716
|
} catch (error) {
|
|
1717
|
+
if (requestId !== themeLoadRequestId) {
|
|
1718
|
+
return { success: false, error: "Theme change superseded by a newer request" };
|
|
1719
|
+
}
|
|
1686
1720
|
// Theme is invalid - fall back to dark theme
|
|
1687
1721
|
currentThemeName = "dark";
|
|
1688
1722
|
theme = await loadTheme("dark", getCurrentThemeOptions());
|
|
@@ -1694,7 +1728,74 @@ export async function setTheme(
|
|
|
1694
1728
|
}
|
|
1695
1729
|
}
|
|
1696
1730
|
|
|
1731
|
+
export async function previewTheme(name: string): Promise<{ success: boolean; error?: string }> {
|
|
1732
|
+
const requestId = ++themeLoadRequestId;
|
|
1733
|
+
try {
|
|
1734
|
+
const loadedTheme = await loadTheme(name, getCurrentThemeOptions());
|
|
1735
|
+
if (requestId !== themeLoadRequestId) {
|
|
1736
|
+
return { success: false, error: "Theme preview superseded by a newer request" };
|
|
1737
|
+
}
|
|
1738
|
+
theme = loadedTheme;
|
|
1739
|
+
if (onThemeChangeCallback) {
|
|
1740
|
+
onThemeChangeCallback();
|
|
1741
|
+
}
|
|
1742
|
+
return { success: true };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
if (requestId !== themeLoadRequestId) {
|
|
1745
|
+
return { success: false, error: "Theme preview superseded by a newer request" };
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
success: false,
|
|
1749
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Enable auto-detection mode, switching to the appropriate dark/light theme.
|
|
1756
|
+
*/
|
|
1757
|
+
export function enableAutoTheme(): void {
|
|
1758
|
+
autoDetectedTheme = true;
|
|
1759
|
+
const resolved = getDefaultTheme();
|
|
1760
|
+
if (resolved === currentThemeName) return;
|
|
1761
|
+
currentThemeName = resolved;
|
|
1762
|
+
loadTheme(resolved, getCurrentThemeOptions())
|
|
1763
|
+
.then(loadedTheme => {
|
|
1764
|
+
theme = loadedTheme;
|
|
1765
|
+
if (onThemeChangeCallback) {
|
|
1766
|
+
onThemeChangeCallback();
|
|
1767
|
+
}
|
|
1768
|
+
})
|
|
1769
|
+
.catch(err => {
|
|
1770
|
+
logger.debug("Auto theme switch failed", { error: String(err) });
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Update the theme mappings for auto-detection mode.
|
|
1776
|
+
* When a dark/light mapping changes and auto-detection is active, re-evaluate the theme.
|
|
1777
|
+
*/
|
|
1778
|
+
export function setAutoThemeMapping(mode: "dark" | "light", themeName: string): void {
|
|
1779
|
+
if (mode === "dark") autoDarkTheme = themeName;
|
|
1780
|
+
else autoLightTheme = themeName;
|
|
1781
|
+
if (!autoDetectedTheme) return;
|
|
1782
|
+
const resolved = getDefaultTheme();
|
|
1783
|
+
if (resolved === currentThemeName) return;
|
|
1784
|
+
currentThemeName = resolved;
|
|
1785
|
+
loadTheme(resolved, getCurrentThemeOptions())
|
|
1786
|
+
.then(loadedTheme => {
|
|
1787
|
+
theme = loadedTheme;
|
|
1788
|
+
if (onThemeChangeCallback) {
|
|
1789
|
+
onThemeChangeCallback();
|
|
1790
|
+
}
|
|
1791
|
+
})
|
|
1792
|
+
.catch(err => {
|
|
1793
|
+
logger.debug("Auto theme mapping switch failed", { error: String(err) });
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1697
1797
|
export function setThemeInstance(themeInstance: Theme): void {
|
|
1798
|
+
autoDetectedTheme = false;
|
|
1698
1799
|
theme = themeInstance;
|
|
1699
1800
|
currentThemeName = "<in-memory>";
|
|
1700
1801
|
stopThemeWatcher();
|
|
@@ -1836,11 +1937,41 @@ async function startThemeWatcher(): Promise<void> {
|
|
|
1836
1937
|
}
|
|
1837
1938
|
}
|
|
1838
1939
|
|
|
1940
|
+
/** Re-check COLORFGBG on SIGWINCH and switch dark/light when using auto-detected theme. */
|
|
1941
|
+
function startSigwinchListener(): void {
|
|
1942
|
+
stopSigwinchListener();
|
|
1943
|
+
sigwinchHandler = () => {
|
|
1944
|
+
if (!autoDetectedTheme) return;
|
|
1945
|
+
const resolved = getDefaultTheme();
|
|
1946
|
+
if (resolved === currentThemeName) return;
|
|
1947
|
+
currentThemeName = resolved;
|
|
1948
|
+
loadTheme(resolved, getCurrentThemeOptions())
|
|
1949
|
+
.then(loadedTheme => {
|
|
1950
|
+
theme = loadedTheme;
|
|
1951
|
+
if (onThemeChangeCallback) {
|
|
1952
|
+
onThemeChangeCallback();
|
|
1953
|
+
}
|
|
1954
|
+
})
|
|
1955
|
+
.catch(err => {
|
|
1956
|
+
logger.debug("Theme switch on SIGWINCH failed", { error: String(err) });
|
|
1957
|
+
});
|
|
1958
|
+
};
|
|
1959
|
+
process.on("SIGWINCH", sigwinchHandler);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function stopSigwinchListener(): void {
|
|
1963
|
+
if (sigwinchHandler) {
|
|
1964
|
+
process.removeListener("SIGWINCH", sigwinchHandler);
|
|
1965
|
+
sigwinchHandler = undefined;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1839
1969
|
export function stopThemeWatcher(): void {
|
|
1840
1970
|
if (themeWatcher) {
|
|
1841
1971
|
themeWatcher.close();
|
|
1842
1972
|
themeWatcher = undefined;
|
|
1843
1973
|
}
|
|
1974
|
+
stopSigwinchListener();
|
|
1844
1975
|
}
|
|
1845
1976
|
|
|
1846
1977
|
// ============================================================================
|
|
@@ -1921,11 +2052,36 @@ export async function getResolvedThemeColors(themeName?: string): Promise<Record
|
|
|
1921
2052
|
}
|
|
1922
2053
|
|
|
1923
2054
|
/**
|
|
1924
|
-
* Check if a theme is a "light" theme
|
|
2055
|
+
* Check if a theme is a "light" theme by analyzing its background color luminance.
|
|
2056
|
+
* Loads theme JSON synchronously (built-in or custom file) and resolves userMessageBg.
|
|
1925
2057
|
*/
|
|
1926
2058
|
export function isLightTheme(themeName?: string): boolean {
|
|
1927
|
-
|
|
1928
|
-
|
|
2059
|
+
const name = themeName ?? "dark";
|
|
2060
|
+
const builtinThemes = getBuiltinThemes();
|
|
2061
|
+
let themeJson: ThemeJson | undefined;
|
|
2062
|
+
if (name in builtinThemes) {
|
|
2063
|
+
themeJson = builtinThemes[name];
|
|
2064
|
+
} else {
|
|
2065
|
+
try {
|
|
2066
|
+
const customPath = path.join(getCustomThemesDir(), `${name}.json`);
|
|
2067
|
+
const content = fs.readFileSync(customPath, "utf-8");
|
|
2068
|
+
themeJson = JSON.parse(content) as ThemeJson;
|
|
2069
|
+
} catch {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
try {
|
|
2074
|
+
const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
|
|
2075
|
+
if (typeof resolved !== "string" || !resolved.startsWith("#") || resolved.length !== 7) return false;
|
|
2076
|
+
const r = parseInt(resolved.slice(1, 3), 16) / 255;
|
|
2077
|
+
const g = parseInt(resolved.slice(3, 5), 16) / 255;
|
|
2078
|
+
const b = parseInt(resolved.slice(5, 7), 16) / 255;
|
|
2079
|
+
// Relative luminance (ITU-R BT.709)
|
|
2080
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2081
|
+
return luminance > 0.5;
|
|
2082
|
+
} catch {
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
1929
2085
|
}
|
|
1930
2086
|
|
|
1931
2087
|
/**
|
package/src/modes/types.ts
CHANGED
|
@@ -152,6 +152,7 @@ export interface InteractiveModeContext {
|
|
|
152
152
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
153
153
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
154
154
|
handleMemoryCommand(text: string): Promise<void>;
|
|
155
|
+
handleSTTToggle(): Promise<void>;
|
|
155
156
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|
|
156
157
|
openInBrowser(urlOrPath: string): void;
|
|
157
158
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
package/src/patch/hashline.ts
CHANGED
|
@@ -56,7 +56,7 @@ function splitDstLines(dst: string): string[] {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Pattern matching hashline display format: `LINE:HASH|CONTENT` */
|
|
59
|
-
const HASHLINE_PREFIX_RE = /^\d+:[0-9a-zA-Z]{1,16}\|/;
|
|
59
|
+
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+:[0-9a-zA-Z]{1,16}\|/;
|
|
60
60
|
|
|
61
61
|
/** Pattern matching a unified-diff `+` prefix (but not `++`) */
|
|
62
62
|
const DIFF_PLUS_RE = /^\+(?!\+)/;
|
|
@@ -508,6 +508,7 @@ export function parseLineRef(ref: string): { line: number; hash: string } {
|
|
|
508
508
|
const cleaned = ref
|
|
509
509
|
.replace(/\|.*$/, "")
|
|
510
510
|
.replace(/ {2}.*$/, "")
|
|
511
|
+
.replace(/^>+\s*/, "")
|
|
511
512
|
.trim();
|
|
512
513
|
const normalized = cleaned.replace(/\s*:\s*/, ":");
|
|
513
514
|
const strictMatch = normalized.match(/^(\d+):([0-9a-zA-Z]{1,16})$/);
|
package/src/patch/shared.ts
CHANGED
|
@@ -121,7 +121,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
121
121
|
return text;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function formatStreamingHashlineEdits(edits:
|
|
124
|
+
function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme, ui: ToolUIKit): string {
|
|
125
125
|
const MAX_EDITS = 4;
|
|
126
126
|
const MAX_DST_LINES = 8;
|
|
127
127
|
let text = "\n\n";
|
|
@@ -156,28 +156,59 @@ function formatStreamingHashlineEdits(edits: HashlineEditPreview[], uiTheme: The
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
return text.trimEnd();
|
|
159
|
-
function formatHashlineEdit(edit:
|
|
160
|
-
|
|
159
|
+
function formatHashlineEdit(edit: unknown): { srcLabel: string; dst: string } {
|
|
160
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined => {
|
|
161
|
+
if (typeof value === "object" && value !== null) return value as Record<string, unknown>;
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
164
|
+
const editRecord = asRecord(edit);
|
|
165
|
+
if (!editRecord) {
|
|
166
|
+
return {
|
|
167
|
+
srcLabel: "โข (incomplete edit)",
|
|
168
|
+
dst: "",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if ("set_line" in editRecord) {
|
|
172
|
+
const setLine = asRecord(editRecord.set_line);
|
|
173
|
+
return {
|
|
174
|
+
srcLabel: `โข set_line ${typeof setLine?.anchor === "string" ? setLine.anchor : "โฆ"}`,
|
|
175
|
+
dst: typeof setLine?.new_text === "string" ? setLine.new_text : "",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if ("replace_lines" in editRecord) {
|
|
179
|
+
const replaceLines = asRecord(editRecord.replace_lines);
|
|
180
|
+
const start = typeof replaceLines?.start_anchor === "string" ? replaceLines.start_anchor : "โฆ";
|
|
181
|
+
const end = typeof replaceLines?.end_anchor === "string" ? replaceLines.end_anchor : "โฆ";
|
|
161
182
|
return {
|
|
162
|
-
srcLabel: `โข
|
|
163
|
-
dst:
|
|
183
|
+
srcLabel: `โข replace_lines ${start}..${end}`,
|
|
184
|
+
dst: typeof replaceLines?.new_text === "string" ? replaceLines.new_text : "",
|
|
164
185
|
};
|
|
165
186
|
}
|
|
166
|
-
if ("
|
|
187
|
+
if ("replace" in editRecord) {
|
|
188
|
+
const replace = asRecord(editRecord.replace);
|
|
189
|
+
const all = typeof replace?.all === "boolean" ? replace.all : false;
|
|
167
190
|
return {
|
|
168
|
-
srcLabel: `โข
|
|
169
|
-
dst:
|
|
191
|
+
srcLabel: `โข replace old_textโnew_text${all ? " (all)" : ""}`,
|
|
192
|
+
dst: typeof replace?.new_text === "string" ? replace.new_text : "",
|
|
170
193
|
};
|
|
171
194
|
}
|
|
172
|
-
if ("
|
|
195
|
+
if ("insert_after" in editRecord) {
|
|
196
|
+
const insertAfter = asRecord(editRecord.insert_after);
|
|
197
|
+
const anchor = typeof insertAfter?.anchor === "string" ? insertAfter.anchor : "โฆ";
|
|
198
|
+
const text =
|
|
199
|
+
typeof insertAfter?.text === "string"
|
|
200
|
+
? insertAfter.text
|
|
201
|
+
: typeof insertAfter?.content === "string"
|
|
202
|
+
? insertAfter.content
|
|
203
|
+
: "";
|
|
173
204
|
return {
|
|
174
|
-
srcLabel: `โข
|
|
175
|
-
dst:
|
|
205
|
+
srcLabel: `โข insert_after ${anchor}..`,
|
|
206
|
+
dst: text,
|
|
176
207
|
};
|
|
177
208
|
}
|
|
178
209
|
return {
|
|
179
|
-
srcLabel:
|
|
180
|
-
dst:
|
|
210
|
+
srcLabel: "โข (incomplete edit)",
|
|
211
|
+
dst: "",
|
|
181
212
|
};
|
|
182
213
|
}
|
|
183
214
|
}
|
|
@@ -9,6 +9,11 @@ Plan approved. Execute it now.
|
|
|
9
9
|
<instruction>
|
|
10
10
|
Execute this plan step by step. You have full tool access.
|
|
11
11
|
Verify each step before proceeding to the next.
|
|
12
|
+
{{#has tools "todo_write"}}
|
|
13
|
+
Before execution, initialize todo tracking for this plan with `todo_write`.
|
|
14
|
+
After each completed step, immediately update `todo_write` so progress stays visible.
|
|
15
|
+
If a `todo_write` call fails, fix the todo payload and retry before continuing silently.
|
|
16
|
+
{{/has}}
|
|
12
17
|
</instruction>
|
|
13
18
|
|
|
14
19
|
<critical>
|
|
@@ -15,6 +15,7 @@ For additional parent conversation context, check {{contextFile}} (`tail -100` o
|
|
|
15
15
|
- MUST work under working tree: {{worktree}}. Do not modify original repository.
|
|
16
16
|
{{/if}}
|
|
17
17
|
- MUST call `submit_result` exactly once when finished. No JSON in text. No plain-text summary. Pass result via `data` parameter.
|
|
18
|
+
- Todo tracking is parent-owned. Do not create or maintain a separate todo list in this subagent.
|
|
18
19
|
{{#if outputSchema}}
|
|
19
20
|
- If cannot complete, call `submit_result` with `status="aborted"` and error message. Do not provide success result or pretend completion.
|
|
20
21
|
{{else}}
|
|
@@ -123,6 +123,16 @@ Don't open a file hoping. Hope is not a strategy.
|
|
|
123
123
|
**If blocked**: exhaust tools/context/files first. Only then ask โ minimum viable question.
|
|
124
124
|
**If requested change includes refactor**: remove now-unused elements. Note removals.
|
|
125
125
|
|
|
126
|
+
{{#has tools "todo_write"}}
|
|
127
|
+
### Task Tracking
|
|
128
|
+
- Use `todo_write` proactively for non-trivial, multi-step work so progress stays visible.
|
|
129
|
+
- Initialize todos before implementation for complex tasks, then keep them current while working.
|
|
130
|
+
- Mark todo items complete immediately after finishing them; do not batch completion updates.
|
|
131
|
+
- Keep todo items as focused logical units (one coherent outcome per item); split broad work into smaller items.
|
|
132
|
+
- Keep exactly one item `in_progress` at a time and complete in order unless requirements change.
|
|
133
|
+
- Skip `todo_write` for single trivial or purely informational requests.
|
|
134
|
+
{{/has}}
|
|
135
|
+
|
|
126
136
|
### Verification
|
|
127
137
|
- Prefer external proof: tests, linters, type checks, repro steps.
|
|
128
138
|
- If unverified: state what to run and expected result.
|
|
@@ -20,14 +20,16 @@ Use proactively:
|
|
|
20
20
|
2. **Task Management**:
|
|
21
21
|
- Update status in real time
|
|
22
22
|
- Mark complete IMMEDIATELY after finishing (no batching)
|
|
23
|
-
-
|
|
23
|
+
- Keep exactly ONE task in_progress at a time
|
|
24
24
|
- Remove tasks no longer relevant
|
|
25
|
+
- Complete tasks in list order (do not mark later tasks completed while earlier tasks remain incomplete)
|
|
25
26
|
3. **Task Completion Requirements**:
|
|
26
27
|
- ONLY mark completed when FULLY accomplished
|
|
27
28
|
- On errors/blockers/inability to finish, keep in_progress
|
|
28
29
|
- When blocked, create task describing what needs resolving
|
|
29
30
|
4. **Task Breakdown**:
|
|
30
31
|
- Create specific, actionable items
|
|
32
|
+
- Keep each todo scoped to one logical unit of work; split unrelated work into separate items
|
|
31
33
|
- Break complex tasks into smaller steps
|
|
32
34
|
- Use clear, descriptive names
|
|
33
35
|
</protocol>
|
package/src/sdk.ts
CHANGED
|
@@ -441,6 +441,58 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
441
441
|
api.on("session_shutdown", async (_event, ctx) =>
|
|
442
442
|
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
|
|
443
443
|
);
|
|
444
|
+
api.on("auto_compaction_start", async (event, ctx) =>
|
|
445
|
+
runOnSession({ reason: "auto_compaction_start", trigger: event.reason }, ctx),
|
|
446
|
+
);
|
|
447
|
+
api.on("auto_compaction_end", async (event, ctx) =>
|
|
448
|
+
runOnSession(
|
|
449
|
+
{
|
|
450
|
+
reason: "auto_compaction_end",
|
|
451
|
+
result: event.result,
|
|
452
|
+
aborted: event.aborted,
|
|
453
|
+
willRetry: event.willRetry,
|
|
454
|
+
errorMessage: event.errorMessage,
|
|
455
|
+
},
|
|
456
|
+
ctx,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
api.on("auto_retry_start", async (event, ctx) =>
|
|
460
|
+
runOnSession(
|
|
461
|
+
{
|
|
462
|
+
reason: "auto_retry_start",
|
|
463
|
+
attempt: event.attempt,
|
|
464
|
+
maxAttempts: event.maxAttempts,
|
|
465
|
+
delayMs: event.delayMs,
|
|
466
|
+
errorMessage: event.errorMessage,
|
|
467
|
+
},
|
|
468
|
+
ctx,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
api.on("auto_retry_end", async (event, ctx) =>
|
|
472
|
+
runOnSession(
|
|
473
|
+
{
|
|
474
|
+
reason: "auto_retry_end",
|
|
475
|
+
success: event.success,
|
|
476
|
+
attempt: event.attempt,
|
|
477
|
+
finalError: event.finalError,
|
|
478
|
+
},
|
|
479
|
+
ctx,
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
api.on("ttsr_triggered", async (event, ctx) =>
|
|
483
|
+
runOnSession({ reason: "ttsr_triggered", rules: event.rules }, ctx),
|
|
484
|
+
);
|
|
485
|
+
api.on("todo_reminder", async (event, ctx) =>
|
|
486
|
+
runOnSession(
|
|
487
|
+
{
|
|
488
|
+
reason: "todo_reminder",
|
|
489
|
+
todos: event.todos,
|
|
490
|
+
attempt: event.attempt,
|
|
491
|
+
maxAttempts: event.maxAttempts,
|
|
492
|
+
},
|
|
493
|
+
ctx,
|
|
494
|
+
),
|
|
495
|
+
);
|
|
444
496
|
};
|
|
445
497
|
}
|
|
446
498
|
|
|
@@ -496,6 +548,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
496
548
|
time("settings");
|
|
497
549
|
initializeWithSettings(settings);
|
|
498
550
|
time("initializeWithSettings");
|
|
551
|
+
const skillsSettings = settings.getGroup("skills") as SkillsSettings;
|
|
552
|
+
const discoveredSkillsPromise =
|
|
553
|
+
options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
|
|
499
554
|
|
|
500
555
|
// Initialize provider preferences from settings
|
|
501
556
|
setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
|
|
@@ -504,6 +559,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
504
559
|
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
|
|
505
560
|
time("sessionManager");
|
|
506
561
|
const sessionId = sessionManager.getSessionId();
|
|
562
|
+
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
563
|
+
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
564
|
+
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
565
|
+
const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
|
|
566
|
+
const availabilityKey = getModelAvailabilityKey(candidate);
|
|
567
|
+
const cached = modelApiKeyAvailability.get(availabilityKey);
|
|
568
|
+
if (cached !== undefined) {
|
|
569
|
+
return cached;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const hasKey = !!(await modelRegistry.getApiKey(candidate, sessionId));
|
|
573
|
+
modelApiKeyAvailability.set(availabilityKey, hasKey);
|
|
574
|
+
return hasKey;
|
|
575
|
+
};
|
|
507
576
|
|
|
508
577
|
// Check if session has existing data to restore
|
|
509
578
|
const existingSession = sessionManager.buildSessionContext();
|
|
@@ -521,7 +590,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
521
590
|
const parsedModel = parseModelString(defaultModelStr);
|
|
522
591
|
if (parsedModel) {
|
|
523
592
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
524
|
-
if (restoredModel && (await
|
|
593
|
+
if (restoredModel && (await hasModelApiKey(restoredModel))) {
|
|
525
594
|
model = restoredModel;
|
|
526
595
|
}
|
|
527
596
|
}
|
|
@@ -537,7 +606,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
537
606
|
const parsedModel = parseModelString(settingsDefaultModel);
|
|
538
607
|
if (parsedModel) {
|
|
539
608
|
const settingsModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
540
|
-
if (settingsModel && (await
|
|
609
|
+
if (settingsModel && (await hasModelApiKey(settingsModel))) {
|
|
541
610
|
model = settingsModel;
|
|
542
611
|
}
|
|
543
612
|
}
|
|
@@ -547,10 +616,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
547
616
|
// Fall back to first available model with a valid API key
|
|
548
617
|
if (!model) {
|
|
549
618
|
const allModels = modelRegistry.getAll();
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
619
|
+
for (const candidate of allModels) {
|
|
620
|
+
if (await hasModelApiKey(candidate)) {
|
|
621
|
+
model = candidate;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
554
625
|
time("findAvailableModel");
|
|
555
626
|
if (model) {
|
|
556
627
|
if (modelFallbackMessage) {
|
|
@@ -563,6 +634,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
563
634
|
}
|
|
564
635
|
}
|
|
565
636
|
|
|
637
|
+
time("findModel");
|
|
638
|
+
|
|
566
639
|
// For subagent sessions using GitHub Copilot, add X-Initiator header
|
|
567
640
|
// to ensure proper billing (agent-initiated vs user-initiated)
|
|
568
641
|
const taskDepth = options.taskDepth ?? 0;
|
|
@@ -604,12 +677,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
604
677
|
skills = options.skills;
|
|
605
678
|
skillWarnings = [];
|
|
606
679
|
} else {
|
|
607
|
-
const
|
|
608
|
-
|
|
680
|
+
const discovered = discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] };
|
|
681
|
+
time("discoverSkills");
|
|
609
682
|
skills = discovered.skills;
|
|
610
683
|
skillWarnings = discovered.warnings;
|
|
611
684
|
}
|
|
612
|
-
|
|
685
|
+
|
|
613
686
|
debugStartup("sdk:discoverSkills");
|
|
614
687
|
|
|
615
688
|
// Discover rules
|