@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Pi Mode
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@zigai/pi-mode.svg?color=blue)](https://www.npmjs.com/package/@zigai/pi-mode)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@zigai/pi-mode.svg)](https://www.npmjs.com/package/@zigai/pi-mode)
5
+ [![license](https://img.shields.io/npm/l/@zigai/pi-mode.svg)](../../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 or thinking level.
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.1.4",
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
- await handleSessionActivated(pi, ctx);
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 { setShowModeName, shouldShowModeName } from "./settings.ts";
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
- type ModeSpecJson = {
38
- provider?: string;
39
- modelId?: string;
40
- thinkingLevel?: ThinkingLevel;
41
- color?: ModeSpec["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
- type ModesFileJson = {
45
- currentMode?: string;
46
- modes?: Record<string, ModeSpecJson>;
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: ThinkingLevel | undefined): ThinkingLevel | undefined {
157
- if (level === undefined) return undefined;
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) as ModesFileJson;
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 back to thinking-derived colors
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 { getAgentDir } from "@earendil-works/pi-coding-agent";
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
- let cachedShowModeName: boolean | undefined;
21
- let cachedSettingsMtimeMs: number | null | undefined;
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 getSettingsMtimeMs(): number | null {
188
+ function getFileMtimeMs(filePath: string | undefined): number | null {
189
+ if (filePath === undefined) return null;
132
190
  try {
133
- if (!existsSync(getSettingsPath())) return null;
134
- return statSync(getSettingsPath()).mtimeMs;
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
- const parsed = JSON.parse(raw) as unknown;
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
- export function shouldShowModeName(): boolean {
170
- const mtimeMs = getSettingsMtimeMs();
171
- if (cachedShowModeName !== undefined && cachedSettingsMtimeMs === mtimeMs) {
172
- return cachedShowModeName;
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 settings = readSettingsObject();
176
- cachedSettingsMtimeMs = mtimeMs;
177
- cachedShowModeName = settings[SHOW_MODE_NAME_SETTINGS_KEY] === true;
178
- return cachedShowModeName;
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
- cachedSettingsMtimeMs = getSettingsMtimeMs();
187
- cachedShowModeName = show;
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 color is derived from the current thinking level.
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
  };