@zigai/pi-mode 0.1.4 → 0.3.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/README.md +12 -2
- package/package.json +4 -1
- package/src/constants.ts +2 -0
- package/src/editor.ts +2 -1
- package/src/index.ts +6 -1
- package/src/mode-state.ts +98 -20
- package/src/settings.ts +158 -23
- package/src/types.ts +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Pi Mode
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zigai/pi-mode)
|
|
4
|
+
[](https://www.npmjs.com/package/@zigai/pi-mode)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
3
7
|
This Pi extension adds prompt modes for model and thinking-level switching.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
@@ -14,8 +18,14 @@ pi install npm:@zigai/pi-mode
|
|
|
14
18
|
- Adds `Ctrl+Shift+M` to select a mode.
|
|
15
19
|
- Adds `Ctrl+Space` to cycle modes.
|
|
16
20
|
- Can show the current mode in the prompt editor border when enabled.
|
|
17
|
-
- Colors the prompt editor border from the active mode
|
|
21
|
+
- Colors the prompt editor border from the active mode, with an opt-in setting for thinking-derived border colors.
|
|
18
22
|
|
|
19
23
|
Modes can store a provider, model, thinking level, and optional color. Project-local modes live in `.pi/modes.json` when present; otherwise global modes live in `~/.pi/agent/modes.json`.
|
|
20
24
|
|
|
21
|
-
By default, Pi Mode does not print the mode name in the editor border. To opt in, toggle `Show mode name` from `/mode` → `Configure modes…`, or set `"modeShowName": true` in `~/.pi/agent/settings.json`.
|
|
25
|
+
By default, Pi Mode does not print the mode name in the editor border. To opt in, toggle `Show mode name` from `/mode` → `Configure modes…`, or set `"modeShowName": true` in global `~/.pi/agent/settings.json` or trusted project `.pi/settings.json`. The `/mode` toggle writes to global settings.
|
|
26
|
+
|
|
27
|
+
By default, Pi Mode uses the normal editor border color when the active mode does not define an explicit `color`. To opt in to thinking-derived border colors, toggle `Thinking border colors` from `/mode` → `Configure modes…`, or set `"modeUseThinkingBorderColors": true` in global `~/.pi/agent/settings.json` or trusted project `.pi/settings.json`. Explicit per-mode `color` values still apply.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zigai/pi-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi package that adds prompt modes for model and thinking-level switching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mode",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"typebox": "^1.1.38"
|
|
34
|
+
},
|
|
32
35
|
"peerDependencies": {
|
|
33
36
|
"@earendil-works/pi-agent-core": "*",
|
|
34
37
|
"@earendil-works/pi-ai": "*",
|
package/src/constants.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const MODE_UI_CONFIGURE = "Configure modes…";
|
|
|
7
7
|
export const MODE_UI_ADD = "Add mode…";
|
|
8
8
|
export const MODE_UI_SHOW_NAME_ON = "Show mode name: on";
|
|
9
9
|
export const MODE_UI_SHOW_NAME_OFF = "Show mode name: off";
|
|
10
|
+
export const MODE_UI_THINKING_COLORS_ON = "Thinking border colors: on";
|
|
11
|
+
export const MODE_UI_THINKING_COLORS_OFF = "Thinking border colors: off";
|
|
10
12
|
export const MODE_UI_BACK = "Back";
|
|
11
13
|
|
|
12
14
|
export const ALL_THINKING_LEVELS: ThinkingLevel[] = [
|
package/src/editor.ts
CHANGED
|
@@ -63,6 +63,7 @@ function enhanceEditor(
|
|
|
63
63
|
requestRender: () => void,
|
|
64
64
|
): EditorLike {
|
|
65
65
|
const originalRender = editor.render.bind(editor);
|
|
66
|
+
const defaultBorderColor = editor.borderColor;
|
|
66
67
|
editor.render = (width: number) => modeLabelLine(originalRender(width), width, editor);
|
|
67
68
|
|
|
68
69
|
const borderColor = (text: string) => {
|
|
@@ -70,7 +71,7 @@ function enhanceEditor(
|
|
|
70
71
|
if (isBashMode) {
|
|
71
72
|
return ctx.ui.theme.getBashModeBorderColor()(text);
|
|
72
73
|
}
|
|
73
|
-
return getModeBorderColor(ctx, pi, getCurrentMode())(text);
|
|
74
|
+
return getModeBorderColor(ctx, pi, getCurrentMode(), defaultBorderColor)(text);
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
Object.defineProperty(editor, "borderColor", {
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
handleSessionActivated,
|
|
8
8
|
selectModeUI,
|
|
9
9
|
} from "./mode-state.ts";
|
|
10
|
+
import { setSettingsContext } from "./settings.ts";
|
|
10
11
|
|
|
11
12
|
export default function (pi: ExtensionAPI) {
|
|
12
13
|
pi.registerCommand("mode", {
|
|
@@ -31,8 +32,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
pi.on("session_start", async (_event, ctx) => {
|
|
34
|
-
|
|
35
|
+
setSettingsContext(ctx);
|
|
36
|
+
// Install the editor wrapper before loading mode files so startup renders use
|
|
37
|
+
// the configured border color immediately instead of briefly showing Pi's
|
|
38
|
+
// thinking-level border color.
|
|
35
39
|
applyModeEditor(pi, ctx);
|
|
40
|
+
await handleSessionActivated(pi, ctx);
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
pi.on("model_select", async (event, ctx) => {
|
package/src/mode-state.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
11
|
+
import { Value } from "typebox/value";
|
|
10
12
|
import {
|
|
11
13
|
ALL_THINKING_LEVELS,
|
|
12
14
|
CUSTOM_MODE_NAME,
|
|
@@ -16,6 +18,8 @@ import {
|
|
|
16
18
|
MODE_UI_CONFIGURE,
|
|
17
19
|
MODE_UI_SHOW_NAME_OFF,
|
|
18
20
|
MODE_UI_SHOW_NAME_ON,
|
|
21
|
+
MODE_UI_THINKING_COLORS_OFF,
|
|
22
|
+
MODE_UI_THINKING_COLORS_ON,
|
|
19
23
|
THINKING_UNSET_LABEL,
|
|
20
24
|
} from "./constants.ts";
|
|
21
25
|
import {
|
|
@@ -26,7 +30,12 @@ import {
|
|
|
26
30
|
getProjectModesPath,
|
|
27
31
|
withFileLock,
|
|
28
32
|
} from "./storage.ts";
|
|
29
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
setShowModeName,
|
|
35
|
+
setUseThinkingBorderColors,
|
|
36
|
+
shouldShowModeName,
|
|
37
|
+
shouldUseThinkingBorderColors,
|
|
38
|
+
} from "./settings.ts";
|
|
30
39
|
import type { ModeRuntime, ModesFile, ModesPatch, ModeSpec, ModeSpecPatch } from "./types.ts";
|
|
31
40
|
|
|
32
41
|
type ScopedModelItem = {
|
|
@@ -34,17 +43,51 @@ type ScopedModelItem = {
|
|
|
34
43
|
thinkingLevel?: string;
|
|
35
44
|
};
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
provider
|
|
39
|
-
modelId
|
|
40
|
-
thinkingLevel
|
|
41
|
-
color
|
|
42
|
-
};
|
|
46
|
+
const ModeSpecJsonSchema = Type.Object({
|
|
47
|
+
provider: Type.Optional(Type.String()),
|
|
48
|
+
modelId: Type.Optional(Type.String()),
|
|
49
|
+
thinkingLevel: Type.Optional(Type.Unknown()),
|
|
50
|
+
color: Type.Optional(Type.String()),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ModesFileJsonSchema = Type.Object({
|
|
54
|
+
$schema: Type.Optional(Type.String()),
|
|
55
|
+
version: Type.Optional(Type.Number()),
|
|
56
|
+
currentMode: Type.Optional(Type.String()),
|
|
57
|
+
modes: Type.Optional(Type.Record(Type.String(), ModeSpecJsonSchema)),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
type ModeSpecJson = Static<typeof ModeSpecJsonSchema>;
|
|
61
|
+
type ModesFileJson = Static<typeof ModesFileJsonSchema>;
|
|
62
|
+
|
|
63
|
+
function formatSchemaPath(instancePath: string): string {
|
|
64
|
+
if (instancePath.length === 0) return "root";
|
|
65
|
+
return instancePath
|
|
66
|
+
.slice(1)
|
|
67
|
+
.split("/")
|
|
68
|
+
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
69
|
+
.join(".");
|
|
70
|
+
}
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
function parseSchema(schema: TSchema, value: unknown, label: string): unknown {
|
|
73
|
+
const errors = [...Value.Errors(schema, value)];
|
|
74
|
+
if (errors.length > 0) {
|
|
75
|
+
const messages = errors
|
|
76
|
+
.slice(0, 5)
|
|
77
|
+
.map((error) => `${formatSchemaPath(error.instancePath)} ${error.message}`);
|
|
78
|
+
let suffix = "";
|
|
79
|
+
if (errors.length > messages.length) {
|
|
80
|
+
suffix = `; and ${errors.length - messages.length} more`;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`${label} is invalid: ${messages.join("; ")}${suffix}`);
|
|
83
|
+
}
|
|
84
|
+
const parsed: unknown = Value.Parse(schema, value);
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseModesFileJson(value: unknown): ModesFileJson {
|
|
89
|
+
return parseSchema(ModesFileJsonSchema, value, "modes.json") as ModesFileJson;
|
|
90
|
+
}
|
|
48
91
|
|
|
49
92
|
function cloneModesFile(file: ModesFile): ModesFile {
|
|
50
93
|
return JSON.parse(JSON.stringify(file)) as ModesFile;
|
|
@@ -55,7 +98,7 @@ function modeSpec(modes: Record<string, ModeSpec>, name: string): ModeSpec | und
|
|
|
55
98
|
return undefined;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
function computeModesPatch(
|
|
101
|
+
export function computeModesPatch(
|
|
59
102
|
base: ModesFile,
|
|
60
103
|
next: ModesFile,
|
|
61
104
|
includeCurrentMode: boolean,
|
|
@@ -108,7 +151,7 @@ function computeModesPatch(
|
|
|
108
151
|
return patch;
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
|
|
154
|
+
export function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
|
|
112
155
|
if (patch.currentMode !== undefined) {
|
|
113
156
|
target.currentMode = patch.currentMode;
|
|
114
157
|
}
|
|
@@ -153,10 +196,10 @@ function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
|
|
|
153
196
|
}
|
|
154
197
|
}
|
|
155
198
|
|
|
156
|
-
function normalizeThinkingLevel(level:
|
|
157
|
-
if (level
|
|
158
|
-
if (ALL_THINKING_LEVELS.includes(level)) {
|
|
159
|
-
return level;
|
|
199
|
+
function normalizeThinkingLevel(level: unknown): ThinkingLevel | undefined {
|
|
200
|
+
if (typeof level !== "string") return undefined;
|
|
201
|
+
if (ALL_THINKING_LEVELS.includes(level as ThinkingLevel)) {
|
|
202
|
+
return level as ThinkingLevel;
|
|
160
203
|
}
|
|
161
204
|
return undefined;
|
|
162
205
|
}
|
|
@@ -185,7 +228,7 @@ function sanitizeModeSpec(spec: ModeSpecJson | undefined): ModeSpec {
|
|
|
185
228
|
};
|
|
186
229
|
if (spec.provider !== undefined) sanitized.provider = spec.provider;
|
|
187
230
|
if (spec.modelId !== undefined) sanitized.modelId = spec.modelId;
|
|
188
|
-
if (spec.color !== undefined) sanitized.color = spec.color;
|
|
231
|
+
if (spec.color !== undefined) sanitized.color = spec.color as ModeSpec["color"];
|
|
189
232
|
return sanitized;
|
|
190
233
|
}
|
|
191
234
|
|
|
@@ -245,7 +288,7 @@ async function loadModesFile(
|
|
|
245
288
|
): Promise<ModesFile> {
|
|
246
289
|
try {
|
|
247
290
|
const raw = await fs.readFile(filePath, "utf8");
|
|
248
|
-
const parsed = JSON.parse(raw)
|
|
291
|
+
const parsed = parseModesFileJson(JSON.parse(raw));
|
|
249
292
|
const currentMode = parsed.currentMode ?? "default";
|
|
250
293
|
const modesRaw = parsed.modes ?? {};
|
|
251
294
|
|
|
@@ -367,6 +410,7 @@ export function getModeBorderColor(
|
|
|
367
410
|
ctx: ExtensionContext,
|
|
368
411
|
pi: ExtensionAPI,
|
|
369
412
|
mode: string,
|
|
413
|
+
fallbackBorderColor?: (text: string) => string,
|
|
370
414
|
): (text: string) => string {
|
|
371
415
|
const theme = ctx.ui.theme;
|
|
372
416
|
const spec = runtime.data.modes[mode];
|
|
@@ -377,10 +421,15 @@ export function getModeBorderColor(
|
|
|
377
421
|
theme.getFgAnsi(color);
|
|
378
422
|
return (text: string) => theme.fg(color, text);
|
|
379
423
|
} catch {
|
|
380
|
-
// fall
|
|
424
|
+
// fall through to the configured fallback border color
|
|
381
425
|
}
|
|
382
426
|
}
|
|
383
427
|
|
|
428
|
+
if (!shouldUseThinkingBorderColors()) {
|
|
429
|
+
if (fallbackBorderColor !== undefined) return fallbackBorderColor;
|
|
430
|
+
return (text: string) => theme.fg("borderMuted", text);
|
|
431
|
+
}
|
|
432
|
+
|
|
384
433
|
return theme.getThinkingBorderColor(pi.getThinkingLevel());
|
|
385
434
|
}
|
|
386
435
|
|
|
@@ -818,10 +867,15 @@ async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promis
|
|
|
818
867
|
if (shouldShowModeName()) {
|
|
819
868
|
showModeNameChoice = MODE_UI_SHOW_NAME_ON;
|
|
820
869
|
}
|
|
870
|
+
let thinkingColorsChoice = MODE_UI_THINKING_COLORS_OFF;
|
|
871
|
+
if (shouldUseThinkingBorderColors()) {
|
|
872
|
+
thinkingColorsChoice = MODE_UI_THINKING_COLORS_ON;
|
|
873
|
+
}
|
|
821
874
|
const choice = await ctx.ui.select("Configure modes", [
|
|
822
875
|
...names,
|
|
823
876
|
MODE_UI_ADD,
|
|
824
877
|
showModeNameChoice,
|
|
878
|
+
thinkingColorsChoice,
|
|
825
879
|
MODE_UI_BACK,
|
|
826
880
|
]);
|
|
827
881
|
if (choice === undefined || choice.length === 0 || choice === MODE_UI_BACK) return;
|
|
@@ -851,6 +905,26 @@ async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promis
|
|
|
851
905
|
continue;
|
|
852
906
|
}
|
|
853
907
|
|
|
908
|
+
if (choice === MODE_UI_THINKING_COLORS_ON || choice === MODE_UI_THINKING_COLORS_OFF) {
|
|
909
|
+
const next = !shouldUseThinkingBorderColors();
|
|
910
|
+
try {
|
|
911
|
+
setUseThinkingBorderColors(next);
|
|
912
|
+
} catch (error: unknown) {
|
|
913
|
+
ctx.ui.notify(
|
|
914
|
+
`Thinking border colors were not saved: ${errorMessage(error)}`,
|
|
915
|
+
"error",
|
|
916
|
+
);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
requestEditorRender?.();
|
|
920
|
+
let displayState = "disabled";
|
|
921
|
+
if (next) {
|
|
922
|
+
displayState = "enabled";
|
|
923
|
+
}
|
|
924
|
+
ctx.ui.notify(`Thinking border colors ${displayState}`, "info");
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
|
|
854
928
|
await editModeUI(pi, ctx, choice);
|
|
855
929
|
}
|
|
856
930
|
}
|
|
@@ -983,6 +1057,10 @@ export async function handleSessionActivated(
|
|
|
983
1057
|
runtime.currentMode = CUSTOM_MODE_NAME;
|
|
984
1058
|
customOverlay = getCurrentSelectionSpec(pi);
|
|
985
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
if (ctx.hasUI) {
|
|
1062
|
+
requestEditorRender?.();
|
|
1063
|
+
}
|
|
986
1064
|
}
|
|
987
1065
|
|
|
988
1066
|
export async function handleModelSelect(
|
package/src/settings.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getAgentDir,
|
|
3
|
+
SettingsManager,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import {
|
|
3
7
|
closeSync,
|
|
4
8
|
existsSync,
|
|
@@ -11,19 +15,64 @@ import {
|
|
|
11
15
|
writeFileSync,
|
|
12
16
|
} from "node:fs";
|
|
13
17
|
import { dirname, join } from "node:path";
|
|
18
|
+
import { Type, type TSchema } from "typebox";
|
|
19
|
+
import { Value } from "typebox/value";
|
|
14
20
|
|
|
15
21
|
export const SHOW_MODE_NAME_SETTINGS_KEY = "modeShowName";
|
|
22
|
+
export const USE_THINKING_BORDER_COLORS_SETTINGS_KEY = "modeUseThinkingBorderColors";
|
|
16
23
|
|
|
17
24
|
const SETTINGS_LOCK_TIMEOUT_MS = 5_000;
|
|
18
25
|
const STALE_SETTINGS_LOCK_MS = 30_000;
|
|
26
|
+
const SettingsObjectSchema = Type.Object({});
|
|
27
|
+
const BooleanSettingSchema = Type.Boolean();
|
|
28
|
+
|
|
29
|
+
type SettingsReadContext = {
|
|
30
|
+
cwd: string;
|
|
31
|
+
projectTrusted: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let settingsReadContext: SettingsReadContext | undefined;
|
|
35
|
+
let cachedSettings:
|
|
36
|
+
| {
|
|
37
|
+
mtimeKey: string;
|
|
38
|
+
showModeName: boolean;
|
|
39
|
+
useThinkingBorderColors: boolean;
|
|
40
|
+
}
|
|
41
|
+
| undefined;
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
43
|
+
type ProjectTrustContext = ExtensionContext & {
|
|
44
|
+
isProjectTrusted?: () => boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
48
|
+
return (ctx as ProjectTrustContext).isProjectTrusted?.() ?? true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setSettingsContext(ctx: ExtensionContext): void {
|
|
52
|
+
const next: SettingsReadContext = {
|
|
53
|
+
cwd: ctx.cwd,
|
|
54
|
+
projectTrusted: isProjectTrusted(ctx),
|
|
55
|
+
};
|
|
56
|
+
if (
|
|
57
|
+
settingsReadContext?.cwd !== next.cwd ||
|
|
58
|
+
settingsReadContext.projectTrusted !== next.projectTrusted
|
|
59
|
+
) {
|
|
60
|
+
settingsReadContext = next;
|
|
61
|
+
cachedSettings = undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
22
64
|
|
|
23
65
|
function getSettingsPath(): string {
|
|
24
66
|
return join(getAgentDir(), "settings.json");
|
|
25
67
|
}
|
|
26
68
|
|
|
69
|
+
function getProjectSettingsPath(): string | undefined {
|
|
70
|
+
if (settingsReadContext === undefined || !settingsReadContext.projectTrusted) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return join(settingsReadContext.cwd, ".pi", "settings.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
function getErrorCode(error: unknown): string | undefined {
|
|
28
77
|
if (!(error instanceof Error)) return undefined;
|
|
29
78
|
const code = (error as NodeJS.ErrnoException).code;
|
|
@@ -41,6 +90,14 @@ function sleepSync(ms: number): void {
|
|
|
41
90
|
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
42
91
|
}
|
|
43
92
|
|
|
93
|
+
function parseOptionalBoolean(schema: TSchema, value: unknown): boolean | undefined {
|
|
94
|
+
if (value === undefined) return undefined;
|
|
95
|
+
if (!Value.Check(schema, value)) return undefined;
|
|
96
|
+
const parsed: unknown = Value.Parse(schema, value);
|
|
97
|
+
if (typeof parsed === "boolean") return parsed;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
44
101
|
function withSettingsLock<T>(settingsPath: string, fn: () => T): T {
|
|
45
102
|
const lockPath = `${settingsPath}.lock`;
|
|
46
103
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
@@ -128,26 +185,51 @@ function atomicWriteUtf8Sync(filePath: string, content: string): void {
|
|
|
128
185
|
}
|
|
129
186
|
}
|
|
130
187
|
|
|
131
|
-
function
|
|
188
|
+
function getFileMtimeMs(filePath: string | undefined): number | null {
|
|
189
|
+
if (filePath === undefined) return null;
|
|
132
190
|
try {
|
|
133
|
-
if (!existsSync(
|
|
134
|
-
return statSync(
|
|
191
|
+
if (!existsSync(filePath)) return null;
|
|
192
|
+
return statSync(filePath).mtimeMs;
|
|
135
193
|
} catch {
|
|
136
194
|
return null;
|
|
137
195
|
}
|
|
138
196
|
}
|
|
139
197
|
|
|
198
|
+
function getSettingsMtimeKey(): string {
|
|
199
|
+
return `${getFileMtimeMs(getSettingsPath())}:${getFileMtimeMs(getProjectSettingsPath())}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatSchemaPath(instancePath: string): string {
|
|
203
|
+
if (instancePath.length === 0) return "root";
|
|
204
|
+
return instancePath
|
|
205
|
+
.slice(1)
|
|
206
|
+
.split("/")
|
|
207
|
+
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
208
|
+
.join(".");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseSettingsObject(value: unknown, settingsPath: string): Record<string, unknown> {
|
|
212
|
+
const errors = [...Value.Errors(SettingsObjectSchema, value)];
|
|
213
|
+
if (errors.length > 0) {
|
|
214
|
+
const messages = errors
|
|
215
|
+
.slice(0, 5)
|
|
216
|
+
.map((error) => `${formatSchemaPath(error.instancePath)} ${error.message}`);
|
|
217
|
+
let suffix = "";
|
|
218
|
+
if (errors.length > messages.length) {
|
|
219
|
+
suffix = `; and ${errors.length - messages.length} more`;
|
|
220
|
+
}
|
|
221
|
+
throw new Error(
|
|
222
|
+
`${settingsPath} must contain a JSON object: ${messages.join("; ")}${suffix}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return { ...(Value.Parse(SettingsObjectSchema, value) as Record<string, unknown>) };
|
|
226
|
+
}
|
|
227
|
+
|
|
140
228
|
function readSettingsObject(options?: { throwOnInvalid?: boolean }): Record<string, unknown> {
|
|
141
229
|
const settingsPath = getSettingsPath();
|
|
142
230
|
try {
|
|
143
231
|
const raw = readFileSync(settingsPath, "utf8");
|
|
144
|
-
|
|
145
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
146
|
-
return { ...parsed };
|
|
147
|
-
}
|
|
148
|
-
if (options?.throwOnInvalid === true) {
|
|
149
|
-
throw new Error(`${settingsPath} must contain a JSON object.`);
|
|
150
|
-
}
|
|
232
|
+
return parseSettingsObject(JSON.parse(raw), settingsPath);
|
|
151
233
|
} catch (error: unknown) {
|
|
152
234
|
if (getErrorCode(error) === "ENOENT") return {};
|
|
153
235
|
if (options?.throwOnInvalid === true) throwError(error);
|
|
@@ -166,16 +248,62 @@ function updateSettingsObject(update: (settings: Record<string, unknown>) => voi
|
|
|
166
248
|
});
|
|
167
249
|
}
|
|
168
250
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
251
|
+
function applyBooleanSetting(
|
|
252
|
+
settings: Record<string, unknown>,
|
|
253
|
+
key: string,
|
|
254
|
+
fallback: boolean,
|
|
255
|
+
): boolean {
|
|
256
|
+
const parsed = parseOptionalBoolean(BooleanSettingSchema, settings[key]);
|
|
257
|
+
if (parsed === undefined) return fallback;
|
|
258
|
+
return parsed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readModeSettings(): {
|
|
262
|
+
showModeName: boolean;
|
|
263
|
+
useThinkingBorderColors: boolean;
|
|
264
|
+
} {
|
|
265
|
+
const mtimeKey = getSettingsMtimeKey();
|
|
266
|
+
if (cachedSettings?.mtimeKey === mtimeKey) {
|
|
267
|
+
return cachedSettings;
|
|
173
268
|
}
|
|
174
269
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
270
|
+
const context = settingsReadContext ?? { cwd: process.cwd(), projectTrusted: false };
|
|
271
|
+
const manager = SettingsManager.create(context.cwd, getAgentDir(), {
|
|
272
|
+
projectTrusted: context.projectTrusted,
|
|
273
|
+
});
|
|
274
|
+
let showModeName = false;
|
|
275
|
+
let useThinkingBorderColors = false;
|
|
276
|
+
|
|
277
|
+
const globalSettings = manager.getGlobalSettings() as Record<string, unknown>;
|
|
278
|
+
showModeName = applyBooleanSetting(globalSettings, SHOW_MODE_NAME_SETTINGS_KEY, showModeName);
|
|
279
|
+
useThinkingBorderColors = applyBooleanSetting(
|
|
280
|
+
globalSettings,
|
|
281
|
+
USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
|
|
282
|
+
useThinkingBorderColors,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const projectSettings = manager.getProjectSettings() as Record<string, unknown>;
|
|
286
|
+
showModeName = applyBooleanSetting(projectSettings, SHOW_MODE_NAME_SETTINGS_KEY, showModeName);
|
|
287
|
+
useThinkingBorderColors = applyBooleanSetting(
|
|
288
|
+
projectSettings,
|
|
289
|
+
USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
|
|
290
|
+
useThinkingBorderColors,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
cachedSettings = {
|
|
294
|
+
mtimeKey,
|
|
295
|
+
showModeName,
|
|
296
|
+
useThinkingBorderColors,
|
|
297
|
+
};
|
|
298
|
+
return cachedSettings;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function shouldShowModeName(): boolean {
|
|
302
|
+
return readModeSettings().showModeName;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function shouldUseThinkingBorderColors(): boolean {
|
|
306
|
+
return readModeSettings().useThinkingBorderColors;
|
|
179
307
|
}
|
|
180
308
|
|
|
181
309
|
export function setShowModeName(show: boolean): void {
|
|
@@ -183,6 +311,13 @@ export function setShowModeName(show: boolean): void {
|
|
|
183
311
|
settings[SHOW_MODE_NAME_SETTINGS_KEY] = show;
|
|
184
312
|
});
|
|
185
313
|
|
|
186
|
-
|
|
187
|
-
|
|
314
|
+
cachedSettings = undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function setUseThinkingBorderColors(useThinkingBorderColors: boolean): void {
|
|
318
|
+
updateSettingsObject((settings) => {
|
|
319
|
+
settings[USE_THINKING_BORDER_COLORS_SETTINGS_KEY] = useThinkingBorderColors;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
cachedSettings = undefined;
|
|
188
323
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,8 @@ export type ModeSpec = {
|
|
|
9
9
|
thinkingLevel?: ThinkingLevel;
|
|
10
10
|
/**
|
|
11
11
|
* Optional theme color token to use for the editor border.
|
|
12
|
-
* If unset, the border
|
|
12
|
+
* If unset, the default editor border is used unless thinking-derived
|
|
13
|
+
* border colors are enabled in settings.
|
|
13
14
|
*/
|
|
14
15
|
color?: ThemeColor;
|
|
15
16
|
};
|