@underactive/pi-topping 0.6.4 → 0.7.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/src/settings.ts CHANGED
@@ -7,7 +7,20 @@ import { isWordPackEnabled, isWordPackId, type WordPack } from "./word-packs.ts"
7
7
  import { isPlainObject } from "./util.ts";
8
8
 
9
9
  export const SETTING_COLOR_VALUES = ["accent", "border", "borderAccent", "success", "error", "warning"] as const;
10
- export type SettingColor = (typeof SETTING_COLOR_VALUES)[number];
10
+ export const LOADER_COLOR_VALUES = [...SETTING_COLOR_VALUES, "text", "muted"] as const;
11
+ export const THINKING_LEVEL_COLOR_VALUES = ["thinking-level", ...LOADER_COLOR_VALUES] as const;
12
+ export type ThinkingLevelColor = (typeof THINKING_LEVEL_COLOR_VALUES)[number];
13
+ export const THINKING_LEVEL_SETTING_COLOR_VALUES = ["thinking-level", ...SETTING_COLOR_VALUES] as const;
14
+ export type ThinkingLevelSettingColor = (typeof THINKING_LEVEL_SETTING_COLOR_VALUES)[number];
15
+ export const SPINNER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
16
+ export type SpinnerColor = ThinkingLevelSettingColor;
17
+ export const PROMPT_BORDER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
18
+ export type PromptBorderColor = ThinkingLevelSettingColor;
19
+ export const DONE_MARKER_BORDER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
20
+ export type DoneMarkerBorderColor = ThinkingLevelSettingColor;
21
+ const THINKING_LEVEL_COLOR_MIGRATION_VERSION = 2;
22
+ const DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION = 3;
23
+ export const SETTINGS_SCHEMA_VERSION = 3;
11
24
 
12
25
  export const BORDER_STYLE_VALUES = ["double", "single", "rounded", "heavy"] as const;
13
26
  export type BorderStyle = (typeof BORDER_STYLE_VALUES)[number];
@@ -28,10 +41,18 @@ export function isDoneMarkerStyle(value: unknown): value is DoneMarkerStyle {
28
41
  return typeof value === "string" && DONE_MARKER_STYLE_VALUES.some(style => style === value);
29
42
  }
30
43
 
31
- export function isSettingColor(value: unknown): value is SettingColor {
32
- return typeof value === "string" && SETTING_COLOR_VALUES.some(color => color === value);
44
+ export function isThinkingLevelColor(value: unknown): value is ThinkingLevelColor {
45
+ return typeof value === "string" && THINKING_LEVEL_COLOR_VALUES.some(color => color === value);
33
46
  }
34
47
 
48
+ export function isThinkingLevelSettingColor(value: unknown): value is ThinkingLevelSettingColor {
49
+ return typeof value === "string" && THINKING_LEVEL_SETTING_COLOR_VALUES.some(color => color === value);
50
+ }
51
+
52
+ export const isSpinnerColor = isThinkingLevelSettingColor;
53
+ export const isPromptBorderColor = isThinkingLevelSettingColor;
54
+ export const isDoneMarkerBorderColor = isThinkingLevelSettingColor;
55
+
35
56
  export interface DecoratorSettings {
36
57
  decorations: {
37
58
  animatedSpinner: boolean;
@@ -45,21 +66,21 @@ export interface DecoratorSettings {
45
66
  meterDirection: "ltr" | "rtl";
46
67
  meterDirectionEnabled: boolean;
47
68
  decorateUserPrompt: boolean;
48
- borderColor: SettingColor;
69
+ borderColor: PromptBorderColor;
49
70
  borderColorEnabled: boolean;
50
71
  borderStyle: BorderStyle;
51
72
  borderStyleEnabled: boolean;
52
73
  doneMarkerBorderStyle: DoneMarkerBorderStyle;
53
- doneMarkerBorderColor: SettingColor;
74
+ doneMarkerBorderColor: DoneMarkerBorderColor;
54
75
  doneMarkerStyle: DoneMarkerStyle;
55
- spinnerColor: SettingColor;
76
+ spinnerColor: SpinnerColor;
56
77
  spinnerColorEnabled: boolean;
57
- meterColor: SettingColor;
78
+ meterColor: ThinkingLevelColor;
58
79
  meterColorEnabled: boolean;
59
80
  meterDimmed: boolean;
60
- tokenRateColor: SettingColor;
81
+ tokenRateColor: ThinkingLevelColor;
61
82
  tokenRateDimmed: boolean;
62
- responseModelColor: SettingColor;
83
+ responseModelColor: ThinkingLevelColor;
63
84
  responseModelDimmed: boolean;
64
85
  promptIcon: boolean;
65
86
  promptTimestamp: boolean;
@@ -84,7 +105,7 @@ export interface DecoratorSettings {
84
105
  }
85
106
 
86
107
  export const DEFAULT_SETTINGS: DecoratorSettings = {
87
- decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "borderAccent", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "borderAccent", doneMarkerStyle: "elite", spinnerColor: "accent", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, responseModelColor: "accent", responseModelDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
108
+ decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "thinking-level", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "thinking-level", doneMarkerStyle: "elite", spinnerColor: "thinking-level", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, responseModelColor: "accent", responseModelDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
88
109
  features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
89
110
  loaderOrder: [...DEFAULT_LOADER_ORDER],
90
111
  wordPacks: {},
@@ -128,7 +149,12 @@ function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, par
128
149
  if (typeof merged[key] === "boolean" && typeof value === "boolean") valid = value;
129
150
  else if ((key === "meterDirection" || key === "shimmerDirection") && (value === "ltr" || value === "rtl")) valid = value;
130
151
  else if (key === "shimmerSpeed" && (value === "slow" || value === "normal" || value === "fast")) valid = value;
131
- else if (key.endsWith("Color") && isSettingColor(value)) valid = value;
152
+ else if (key === "spinnerColor" && value === "default") valid = "thinking-level";
153
+ else if (key === "spinnerColor" && isSpinnerColor(value)) valid = value;
154
+ else if (key === "borderColor" && isPromptBorderColor(value)) valid = value;
155
+ else if (key === "doneMarkerBorderColor" && value === "default") valid = "thinking-level";
156
+ else if (key === "doneMarkerBorderColor" && isDoneMarkerBorderColor(value)) valid = value;
157
+ else if ((key === "meterColor" || key === "tokenRateColor" || key === "responseModelColor") && isThinkingLevelColor(value)) valid = value;
132
158
  else if (key === "borderStyle" && isBorderStyle(value)) valid = value;
133
159
  else if (key === "doneMarkerBorderStyle" && isDoneMarkerBorderStyle(value)) valid = value;
134
160
  else if (key === "doneMarkerStyle" && isDoneMarkerStyle(value)) valid = value;
@@ -137,36 +163,14 @@ function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, par
137
163
  return merged;
138
164
  }
139
165
 
140
- /** Map a persisted setting value to its menu cycle label (directions and speeds only; colors pass through). */
141
- function toCycleValue(stored: unknown): string {
142
- if (stored === "ltr") return "Left to Right";
143
- if (stored === "rtl") return "Right to Left";
144
- if (stored === "slow") return "Slow";
145
- if (stored === "normal") return "Normal";
146
- if (stored === "fast") return "Fast";
147
- return String(stored);
148
- }
149
-
150
- /** Map a menu cycle label back to its persisted value. */
151
- function fromCycleValue(value: string): string {
152
- if (value === "Left to Right") return "ltr";
153
- if (value === "Right to Left") return "rtl";
154
- if (value === "Slow") return "slow";
155
- if (value === "Normal") return "normal";
156
- if (value === "Fast") return "fast";
157
- return value;
158
- }
159
-
160
166
  /** Convert a persisted or menu direction to a safe preview direction. */
161
167
  export function fromCycleDirection(value: unknown): "ltr" | "rtl" {
162
- const stored = typeof value === "string" ? fromCycleValue(value) : "";
163
- return stored === "rtl" ? "rtl" : "ltr";
168
+ return value === "rtl" ? "rtl" : "ltr";
164
169
  }
165
170
 
166
171
  /** Convert a persisted or menu speed to a safe preview speed. */
167
172
  export function fromCycleSpeed(value: unknown): "slow" | "normal" | "fast" {
168
- const stored = typeof value === "string" ? fromCycleValue(value) : "";
169
- return stored === "slow" || stored === "fast" ? stored : "normal";
173
+ return value === "slow" || value === "fast" ? value : "normal";
170
174
  }
171
175
 
172
176
  type MenuSectionName = "User Prompt" | "“Working” Loader" | "Completion Marker" | "Options";
@@ -174,42 +178,45 @@ type DecorationSettings = DecoratorSettings["decorations"];
174
178
  type FeatureSettings = DecoratorSettings["features"];
175
179
  type DecorationBooleanKey = { [Key in keyof DecorationSettings]: DecorationSettings[Key] extends boolean ? Key : never }[keyof DecorationSettings];
176
180
  type MenuEntryBase = { id: string; label: string; section: MenuSectionName };
177
- type DecorationMenuEntry = MenuEntryBase & { group: "decorations"; key: keyof DecorationSettings; cycleValues?: readonly string[]; cycleEnabledBy?: DecorationBooleanKey; cycleDisabledValue?: string };
178
- type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureSettings; cycleValues?: never; cycleEnabledBy?: never; cycleDisabledValue?: never };
181
+ type DecorationMenuEntry = MenuEntryBase & { group: "decorations"; key: keyof DecorationSettings; cycleValues?: readonly string[]; cycleValueLabels?: Readonly<Record<string, string>>; cycleEnabledBy?: DecorationBooleanKey; cycleDisabledValue?: string };
182
+ type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureSettings; cycleValues?: never; cycleValueLabels?: never; cycleEnabledBy?: never; cycleDisabledValue?: never };
179
183
  type MenuEntry = DecorationMenuEntry | FeatureMenuEntry;
184
+ const THINKING_LEVEL_CYCLE_LABELS = { "thinking-level": "thinkingLevel" } as const;
185
+ const DIRECTION_CYCLE_LABELS = { ltr: "Left to Right", rtl: "Right to Left" } as const;
186
+ const SPEED_CYCLE_LABELS = { slow: "Slow", normal: "Normal", fast: "Fast" } as const;
180
187
  export const MENU_ENTRIES: readonly MenuEntry[] = [
181
188
  { id: "decorateUserPrompt", label: "High-vis prompt", section: "User Prompt", group: "decorations", key: "decorateUserPrompt" },
182
189
  { id: "borderStyle", label: "Border style", section: "User Prompt", group: "decorations", key: "borderStyle", cycleValues: BORDER_STYLE_VALUES, cycleEnabledBy: "borderStyleEnabled", cycleDisabledValue: "double" },
183
- { id: "borderColor", label: "Border color", section: "User Prompt", group: "decorations", key: "borderColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "borderColorEnabled", cycleDisabledValue: "borderAccent" },
190
+ { id: "borderColor", label: "Border color", section: "User Prompt", group: "decorations", key: "borderColor", cycleValues: PROMPT_BORDER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "borderColorEnabled", cycleDisabledValue: "thinking-level" },
184
191
  { id: "promptIcon", label: "Pi icon", section: "User Prompt", group: "decorations", key: "promptIcon" },
185
192
  { id: "promptTimestamp", label: "Timestamp", section: "User Prompt", group: "decorations", key: "promptTimestamp" },
186
193
  { id: "promptProvider", label: "Provider", section: "User Prompt", group: "decorations", key: "promptProvider" },
187
194
  { id: "promptModel", label: "Model", section: "User Prompt", group: "decorations", key: "promptModel" },
188
195
  { id: "animatedSpinner", label: "Animated spinner", section: "“Working” Loader", group: "decorations", key: "animatedSpinner" },
189
- { id: "spinnerColor", label: "Animated spinner color", section: "“Working” Loader", group: "decorations", key: "spinnerColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "spinnerColorEnabled", cycleDisabledValue: "accent" },
196
+ { id: "spinnerColor", label: "Animated spinner color", section: "“Working” Loader", group: "decorations", key: "spinnerColor", cycleValues: SPINNER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "spinnerColorEnabled", cycleDisabledValue: "thinking-level" },
190
197
  { id: "substituteDefaultMessage", label: "Randomize “Working” text", section: "“Working” Loader", group: "features", key: "substituteDefaultMessage" },
191
198
  { id: "shimmer", label: "Text shimmer", section: "“Working” Loader", group: "decorations", key: "shimmer" },
192
199
  { id: "shimmerInverted", label: "Invert shimmer", section: "“Working” Loader", group: "decorations", key: "shimmerInverted" },
193
- { id: "shimmerDirection", label: "Text shimmer direction", section: "“Working” Loader", group: "decorations", key: "shimmerDirection", cycleValues: ["Left to Right", "Right to Left"], cycleEnabledBy: "shimmerDirectionEnabled", cycleDisabledValue: "Left to Right" },
194
- { id: "shimmerSpeed", label: "Text shimmer speed", section: "“Working” Loader", group: "decorations", key: "shimmerSpeed", cycleValues: ["Slow", "Normal", "Fast"], cycleEnabledBy: "shimmerSpeedEnabled", cycleDisabledValue: "Normal" },
200
+ { id: "shimmerDirection", label: "Text shimmer direction", section: "“Working” Loader", group: "decorations", key: "shimmerDirection", cycleValues: ["ltr", "rtl"], cycleValueLabels: DIRECTION_CYCLE_LABELS, cycleEnabledBy: "shimmerDirectionEnabled", cycleDisabledValue: "ltr" },
201
+ { id: "shimmerSpeed", label: "Text shimmer speed", section: "“Working” Loader", group: "decorations", key: "shimmerSpeed", cycleValues: ["slow", "normal", "fast"], cycleValueLabels: SPEED_CYCLE_LABELS, cycleEnabledBy: "shimmerSpeedEnabled", cycleDisabledValue: "normal" },
195
202
  { id: "tokenActivityMonitor", label: "Token activity monitor", section: "“Working” Loader", group: "decorations", key: "tokenActivityMonitor" },
196
- { id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "meterColorEnabled", cycleDisabledValue: "accent" },
197
- { id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["Left to Right", "Right to Left"], cycleEnabledBy: "meterDirectionEnabled", cycleDisabledValue: "Right to Left" },
203
+ { id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "meterColorEnabled", cycleDisabledValue: "accent" },
204
+ { id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["ltr", "rtl"], cycleValueLabels: DIRECTION_CYCLE_LABELS, cycleEnabledBy: "meterDirectionEnabled", cycleDisabledValue: "rtl" },
198
205
  { id: "meterDimmed", label: "Token activity monitor dimmed", section: "“Working” Loader", group: "decorations", key: "meterDimmed" },
199
206
  { id: "elapsedTime", label: "Elapsed time since prompt", section: "“Working” Loader", group: "features", key: "elapsedTime" },
200
207
  { id: "outputTokens", label: "Show output tokens", section: "“Working” Loader", group: "features", key: "outputTokens" },
201
208
  // id differs from key: the Elements Order row already owns "tokenRate" in the menu's shared value namespace.
202
209
  { id: "showTokenRate", label: "Token rate", section: "“Working” Loader", group: "features", key: "tokenRate" },
203
- { id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues: SETTING_COLOR_VALUES },
210
+ { id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
204
211
  { id: "tokenRateDimmed", label: "Token rate dimmed", section: "“Working” Loader", group: "decorations", key: "tokenRateDimmed" },
205
212
  // id differs from key: the Elements Order row already owns "responseModel" in the menu's shared value namespace.
206
213
  { id: "showResponseModel", label: "Response model", section: "“Working” Loader", group: "features", key: "responseModel" },
207
- { id: "responseModelColor", label: "Response model color", section: "“Working” Loader", group: "decorations", key: "responseModelColor", cycleValues: SETTING_COLOR_VALUES },
214
+ { id: "responseModelColor", label: "Response model color", section: "“Working” Loader", group: "decorations", key: "responseModelColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
208
215
  { id: "responseModelDimmed", label: "Response model dimmed", section: "“Working” Loader", group: "decorations", key: "responseModelDimmed" },
209
216
  { id: "doneMarker", label: "Show completion marker", section: "Completion Marker", group: "features", key: "doneMarker" },
210
217
  { id: "doneMarkerStyle", label: "Marker style", section: "Completion Marker", group: "decorations", key: "doneMarkerStyle", cycleValues: DONE_MARKER_STYLE_VALUES },
211
218
  { id: "doneMarkerBorderStyle", label: "Border style", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderStyle", cycleValues: DONE_MARKER_BORDER_STYLE_VALUES },
212
- { id: "doneMarkerBorderColor", label: "Border color", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderColor", cycleValues: SETTING_COLOR_VALUES },
219
+ { id: "doneMarkerBorderColor", label: "Border color", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderColor", cycleValues: DONE_MARKER_BORDER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
213
220
  { id: "doneMarkerIcon", label: "Pi icon", section: "Completion Marker", group: "features", key: "doneMarkerIcon" },
214
221
  { id: "randomizeDoneMarker", label: "Randomize “Worked” text", section: "Completion Marker", group: "features", key: "randomizeDoneMarker" },
215
222
  { id: "doneMarkerTokens", label: "Tokens spent", section: "Completion Marker", group: "features", key: "doneMarkerTokens" },
@@ -222,7 +229,7 @@ function menuItem(entry: MenuEntry, settings: DecoratorSettings): MenuSection["i
222
229
  const cycleEnabled = entry.group === "decorations" && entry.cycleEnabledBy
223
230
  ? settings.decorations[entry.cycleEnabledBy]
224
231
  : undefined;
225
- return { id: entry.id, label: entry.label, cycleValues: entry.cycleValues, cycleEnabledBy: entry.cycleEnabledBy, cycleDisabledValue: entry.cycleDisabledValue, cycleEnabled, value: entry.cycleValues ? toCycleValue(value) : value };
232
+ return { id: entry.id, label: entry.label, cycleValues: entry.cycleValues, cycleValueLabels: entry.cycleValueLabels, cycleEnabledBy: entry.cycleEnabledBy, cycleDisabledValue: entry.cycleDisabledValue, cycleEnabled, value };
226
233
  }
227
234
 
228
235
  function isDecorationBooleanKey(key: keyof DecorationSettings): key is DecorationBooleanKey {
@@ -230,31 +237,36 @@ function isDecorationBooleanKey(key: keyof DecorationSettings): key is Decoratio
230
237
  }
231
238
 
232
239
  function setDecorationCycleValue(decorations: DecorationSettings, key: keyof DecorationSettings, value: string): void {
233
- const stored = fromCycleValue(value);
234
240
  switch (key) {
235
241
  case "borderColor":
242
+ if (isPromptBorderColor(value)) decorations[key] = value;
243
+ return;
236
244
  case "spinnerColor":
245
+ if (isSpinnerColor(value)) decorations[key] = value;
246
+ return;
237
247
  case "meterColor":
238
248
  case "tokenRateColor":
239
249
  case "responseModelColor":
250
+ if (isThinkingLevelColor(value)) decorations[key] = value;
251
+ return;
240
252
  case "doneMarkerBorderColor":
241
- if (isSettingColor(stored)) decorations[key] = stored;
253
+ if (isDoneMarkerBorderColor(value)) decorations[key] = value;
242
254
  return;
243
255
  case "borderStyle":
244
- if (isBorderStyle(stored)) decorations[key] = stored;
256
+ if (isBorderStyle(value)) decorations[key] = value;
245
257
  return;
246
258
  case "doneMarkerBorderStyle":
247
- if (isDoneMarkerBorderStyle(stored)) decorations[key] = stored;
259
+ if (isDoneMarkerBorderStyle(value)) decorations[key] = value;
248
260
  return;
249
261
  case "doneMarkerStyle":
250
- if (isDoneMarkerStyle(stored)) decorations[key] = stored;
262
+ if (isDoneMarkerStyle(value)) decorations[key] = value;
251
263
  return;
252
264
  case "shimmerDirection":
253
265
  case "meterDirection":
254
- if (stored === "ltr" || stored === "rtl") decorations[key] = stored;
266
+ if (value === "ltr" || value === "rtl") decorations[key] = value;
255
267
  return;
256
268
  case "shimmerSpeed":
257
- if (stored === "slow" || stored === "normal" || stored === "fast") decorations[key] = stored;
269
+ if (value === "slow" || value === "normal" || value === "fast") decorations[key] = value;
258
270
  return;
259
271
  default:
260
272
  // Fail loudly if a MENU_ENTRIES cycle entry is added without a handler here.
@@ -321,6 +333,14 @@ export function loadSettings(): DecoratorSettings {
321
333
  wordPacks.simcity = true;
322
334
  }
323
335
  const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder), wordPacks };
336
+ const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
337
+ if (schemaVersion < THINKING_LEVEL_COLOR_MIGRATION_VERSION) {
338
+ settings.decorations.spinnerColor = "thinking-level";
339
+ if (settings.decorations.borderColor === "borderAccent") settings.decorations.borderColor = "thinking-level";
340
+ }
341
+ if (schemaVersion < DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION) {
342
+ settings.decorations.doneMarkerBorderColor = "thinking-level";
343
+ }
324
344
  for (const entry of MENU_ENTRIES) {
325
345
  if (entry.group === "decorations" && entry.cycleEnabledBy && entry.cycleDisabledValue !== undefined && !settings.decorations[entry.cycleEnabledBy]) {
326
346
  setDecorationCycleValue(settings.decorations, entry.key, entry.cycleDisabledValue);
@@ -343,5 +363,5 @@ export function atomicWriteFile(path: string, contents: string): void {
343
363
  }
344
364
 
345
365
  export function saveSettings(settings: DecoratorSettings): void {
346
- atomicWriteFile(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`);
366
+ atomicWriteFile(settingsPath(), `${JSON.stringify({ schemaVersion: SETTINGS_SCHEMA_VERSION, ...settings }, null, 2)}\n`);
347
367
  }
package/src/util.ts CHANGED
@@ -7,3 +7,12 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
7
7
  export function stripControlChars(text: string): string {
8
8
  return text.replace(/[\p{Cc}\p{Cf}]/gu, "");
9
9
  }
10
+
11
+ /** True when a reported response model appears to be a decorated form of the selected model. */
12
+ export function modelsResemble(selectedModel?: string, responseModel?: string): boolean {
13
+ if (!selectedModel || !responseModel) return false;
14
+ const selectedName = selectedModel.split(/[\\/]/).at(-1)?.toLowerCase() ?? "";
15
+ const selectedTokens = selectedName.match(/[a-z0-9]{2,}/g) ?? [];
16
+ const normalizedResponse = responseModel.toLowerCase();
17
+ return selectedTokens.length > 0 && selectedTokens.every((token) => normalizedResponse.includes(token));
18
+ }
package/src/word-packs.ts CHANGED
@@ -84,7 +84,6 @@ export function loadBundledWordPacks(): WordPack[] {
84
84
  packs.push(pack);
85
85
  }
86
86
  }
87
- if (!packs.some((pack) => pack.id === "simcity")) throw new Error("Missing bundled SimCity word pack");
88
87
  const bundledIds = new Set(packs.map((pack) => pack.id));
89
88
  for (const id of RESERVED_BUNDLED_PACK_IDS) {
90
89
  if (!bundledIds.has(id)) throw new Error(`Missing bundled word pack: ${id}`);
@@ -109,9 +108,11 @@ export function isWordPackEnabled(id: string, enabled: Record<string, boolean>):
109
108
  }
110
109
 
111
110
  export function selectWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[], fraction: number): WorkingTextSelection {
112
- const pool = [...WORDS, ...packs.filter((pack) => isWordPackEnabled(pack.id, enabled)).flatMap((pack) => pack.words)];
113
- const entry = pool[Math.min(pool.length - 1, Math.floor(fraction * pool.length))]!;
114
- return { text: `${entry.present_tense}…`, pastTense: entry.past_tense };
111
+ const enabledWords = packs.filter((pack) => isWordPackEnabled(pack.id, enabled)).flatMap((pack) => pack.words);
112
+ const total = WORDS.length + enabledWords.length;
113
+ const idx = Math.min(total - 1, Math.floor(fraction * total));
114
+ const entry = idx < WORDS.length ? WORDS[idx] : enabledWords[idx - WORDS.length];
115
+ return { text: entry.present_tense, pastTense: entry.past_tense };
115
116
  }
116
117
 
117
118
  export function pickWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[]): WorkingTextSelection {
package/src/words.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Activity word list for the randomized "Working..." indicator.
2
+ * Activity word list for the randomized "Working" indicator.
3
3
  *
4
4
  * A random word is picked on prompt and on tool execution start
5
- * (see index.ts).
5
+ * (see SessionManager in src/session.ts).
6
6
  */
7
7
 
8
8
  export interface WordEntry {
@@ -10,14 +10,6 @@ export interface WordEntry {
10
10
  past_tense: string;
11
11
  }
12
12
 
13
- export function pickRawWord(): WordEntry {
14
- return WORDS[Math.floor(Math.random() * WORDS.length)]!;
15
- }
16
-
17
- export function pickRandomWord(): string {
18
- return `${pickRawWord().present_tense}…`;
19
- }
20
-
21
13
  export const WORDS: WordEntry[] = [
22
14
  { present_tense: "Accomplishing", past_tense: "Accomplished" },
23
15
  { present_tense: "Actioning", past_tense: "Actioned" },