@oh-my-pi/pi-coding-agent 12.3.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 +66 -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 +6 -2
- 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/lsp/render.ts +1 -1
- package/src/main.ts +7 -1
- package/src/memories/index.ts +11 -7
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +3 -1
- 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/session/streaming-output.ts +1 -1
- 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/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Text,
|
|
16
16
|
TUI,
|
|
17
17
|
} from "@oh-my-pi/pi-tui";
|
|
18
|
-
import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { $env, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import { KeybindingsManager } from "../config/keybindings";
|
|
@@ -30,6 +30,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
|
30
30
|
import { HistoryStorage } from "../session/history-storage";
|
|
31
31
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
32
32
|
import { getRecentSessions } from "../session/session-manager";
|
|
33
|
+
import { STTController, type SttState } from "../stt";
|
|
33
34
|
import type { ExitPlanModeDetails } from "../tools";
|
|
34
35
|
import { setTerminalTitle } from "../utils/title-generator";
|
|
35
36
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
@@ -152,6 +153,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
152
153
|
readonly #inputController: InputController;
|
|
153
154
|
readonly #selectorController: SelectorController;
|
|
154
155
|
readonly #uiHelpers: UiHelpers;
|
|
156
|
+
#sttController: STTController | undefined;
|
|
157
|
+
#voiceAnimationInterval: NodeJS.Timeout | undefined;
|
|
158
|
+
#voiceHue = 0;
|
|
159
|
+
#voicePreviousShowHardwareCursor: boolean | null = null;
|
|
160
|
+
#voicePreviousUseTerminalCursor: boolean | null = null;
|
|
155
161
|
|
|
156
162
|
constructor(
|
|
157
163
|
session: AgentSession,
|
|
@@ -709,6 +715,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
709
715
|
this.loadingAnimation.stop();
|
|
710
716
|
this.loadingAnimation = undefined;
|
|
711
717
|
}
|
|
718
|
+
this.#cleanupMicAnimation();
|
|
719
|
+
if (this.#sttController) {
|
|
720
|
+
this.#sttController.dispose();
|
|
721
|
+
this.#sttController = undefined;
|
|
722
|
+
}
|
|
712
723
|
this.statusLine.dispose();
|
|
713
724
|
if (this.unsubscribe) {
|
|
714
725
|
this.unsubscribe();
|
|
@@ -923,6 +934,78 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
923
934
|
return this.#commandController.handleMemoryCommand(text);
|
|
924
935
|
}
|
|
925
936
|
|
|
937
|
+
async handleSTTToggle(): Promise<void> {
|
|
938
|
+
if (!settings.get("stt.enabled")) {
|
|
939
|
+
this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (!this.#sttController) {
|
|
943
|
+
this.#sttController = new STTController();
|
|
944
|
+
}
|
|
945
|
+
await this.#sttController.toggle(this.editor, {
|
|
946
|
+
showWarning: (msg: string) => this.showWarning(msg),
|
|
947
|
+
showStatus: (msg: string) => this.showStatus(msg),
|
|
948
|
+
onStateChange: (state: SttState) => {
|
|
949
|
+
if (state === "recording") {
|
|
950
|
+
this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
|
|
951
|
+
this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
|
|
952
|
+
this.ui.setShowHardwareCursor(false);
|
|
953
|
+
this.editor.setUseTerminalCursor(false);
|
|
954
|
+
this.#startMicAnimation();
|
|
955
|
+
} else if (state === "transcribing") {
|
|
956
|
+
this.#stopMicAnimation();
|
|
957
|
+
this.editor.cursorOverride = `\x1b[38;2;200;200;200m${theme.icon.mic}\x1b[0m`;
|
|
958
|
+
this.editor.cursorOverrideWidth = 1;
|
|
959
|
+
} else {
|
|
960
|
+
this.#cleanupMicAnimation();
|
|
961
|
+
}
|
|
962
|
+
this.updateEditorTopBorder();
|
|
963
|
+
this.ui.requestRender();
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
#updateMicIcon(): void {
|
|
969
|
+
const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
|
|
970
|
+
this.editor.cursorOverride = `\x1b[38;2;${r};${g};${b}m${theme.icon.mic}\x1b[0m`;
|
|
971
|
+
this.editor.cursorOverrideWidth = 1;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
#startMicAnimation(): void {
|
|
975
|
+
if (this.#voiceAnimationInterval) return;
|
|
976
|
+
this.#voiceHue = 0;
|
|
977
|
+
this.#updateMicIcon();
|
|
978
|
+
this.#voiceAnimationInterval = setInterval(() => {
|
|
979
|
+
this.#voiceHue = (this.#voiceHue + 8) % 360;
|
|
980
|
+
this.#updateMicIcon();
|
|
981
|
+
this.ui.requestRender();
|
|
982
|
+
}, 60);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
#stopMicAnimation(): void {
|
|
986
|
+
if (this.#voiceAnimationInterval) {
|
|
987
|
+
clearInterval(this.#voiceAnimationInterval);
|
|
988
|
+
this.#voiceAnimationInterval = undefined;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
#cleanupMicAnimation(): void {
|
|
993
|
+
if (this.#voiceAnimationInterval) {
|
|
994
|
+
clearInterval(this.#voiceAnimationInterval);
|
|
995
|
+
this.#voiceAnimationInterval = undefined;
|
|
996
|
+
}
|
|
997
|
+
this.editor.cursorOverride = undefined;
|
|
998
|
+
this.editor.cursorOverrideWidth = undefined;
|
|
999
|
+
if (this.#voicePreviousShowHardwareCursor !== null) {
|
|
1000
|
+
this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
|
|
1001
|
+
this.#voicePreviousShowHardwareCursor = null;
|
|
1002
|
+
}
|
|
1003
|
+
if (this.#voicePreviousUseTerminalCursor !== null) {
|
|
1004
|
+
this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
|
|
1005
|
+
this.#voicePreviousUseTerminalCursor = null;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
926
1009
|
showDebugSelector(): void {
|
|
927
1010
|
this.#selectorController.showDebugSelector();
|
|
928
1011
|
}
|
|
@@ -207,6 +207,13 @@ export class RpcClient {
|
|
|
207
207
|
await this.#send({ type: "abort" });
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Abort current operation and immediately start a new turn with the given message.
|
|
212
|
+
*/
|
|
213
|
+
async abortAndPrompt(message: string, images?: ImageContent[]): Promise<void> {
|
|
214
|
+
await this.#send({ type: "abort_and_prompt", message, images });
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
/**
|
|
211
218
|
* Start a new session, optionally with parent tracking.
|
|
212
219
|
* @param parentSession - Optional parent session path for lineage tracking
|
|
@@ -449,6 +449,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
449
449
|
return success(id, "abort");
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
case "abort_and_prompt": {
|
|
453
|
+
await session.abort();
|
|
454
|
+
session
|
|
455
|
+
.prompt(command.message, { images: command.images })
|
|
456
|
+
.catch(e => output(error(id, "abort_and_prompt", e.message)));
|
|
457
|
+
return success(id, "abort_and_prompt");
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
case "new_session": {
|
|
453
461
|
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
|
454
462
|
const cancelled = !(await session.newSession(options));
|
|
@@ -20,6 +20,7 @@ export type RpcCommand =
|
|
|
20
20
|
| { id?: string; type: "steer"; message: string; images?: ImageContent[] }
|
|
21
21
|
| { id?: string; type: "follow_up"; message: string; images?: ImageContent[] }
|
|
22
22
|
| { id?: string; type: "abort" }
|
|
23
|
+
| { id?: string; type: "abort_and_prompt"; message: string; images?: ImageContent[] }
|
|
23
24
|
| { id?: string; type: "new_session"; parentSession?: string }
|
|
24
25
|
|
|
25
26
|
// State
|
|
@@ -94,6 +95,7 @@ export type RpcResponse =
|
|
|
94
95
|
| { id?: string; type: "response"; command: "steer"; success: true }
|
|
95
96
|
| { id?: string; type: "response"; command: "follow_up"; success: true }
|
|
96
97
|
| { id?: string; type: "response"; command: "abort"; success: true }
|
|
98
|
+
| { id?: string; type: "response"; command: "abort_and_prompt"; success: true }
|
|
97
99
|
| { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
|
|
98
100
|
|
|
99
101
|
// State
|
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>
|