jevstrudel.git / worker / src / mcp-settings.mjs

The settings tab's settings an MCP may read and change in a listener's tab (get_settings and set_settings, shared-tools.json): shared by the Worker, which checks a change before it reaches a tab, and the tab (website/src/jev/tabTools.mjs), which checks it again against its own store (website/src/settings.mjs). Plain JavaScript without imports, like art-rubric.mjs, so the Worker, Vite and Node all load it.

Only how the editor and panel look and behave while typing. Never a setting that runs or routes anything, so none can weaken the sandbox or act as the listener: prebakeScript code the page evaluates before every song: in the page includePrebakeScriptInShare, userPatterns, latestCode the listener's own code isSyncEnabled shares the editor across tabs patternAutoStart, isBlockBasedEvalEnabled when and how code is evaluated audioEngineTarget osc sends every note to a local server audioDeviceName, maxPolyphony, multiChannelOrbits the audio device and engine A key not listed here is refused, so a setting added upstream stays out until someone decides it is harmless.

20const FLAG = { type: 'boolean' };
22export const SETTINGS = {
23  theme: { type: 'string', about: 'the editor theme (get_settings lists them)' },
24  fontFamily: {
25    type: 'enum',
26    values: ['monospace', 'Courier', 'CutiePi', 'JetBrains', 'Hack', 'FiraCode', 'FiraCode-SemiBold', 'teletext', 'tic80', 'mode7', 'BigBlueTerminal', 'x3270', 'Monocraft', 'PressStart', 'we-come-in-peace', 'galactico'],
27  },
28  fontSize: { type: 'number', min: 10, max: 40 },
29  keybindings: { type: 'enum', values: ['codemirror', 'vim', 'emacs', 'helix', 'vscode'] },
30  panelPosition: { type: 'enum', values: ['bottom', 'right'] },
31  activeFooter: { type: 'enum', values: ['patterns', 'sounds', 'reference', 'export', 'console', 'settings'], about: "the right panel's open tab (Strudel's)" },
32  isPanelOpen: FLAG,
33  jevTab: { type: 'enum', values: ['songs', 'activity', 'listening', 'you', 'mcp', 'data'], about: "the jev panel's open tab (everything jevstrudel adds, on the left)" },
34  isJevPanelOpen: FLAG,
35  isZen: FLAG,
36  isBracketMatchingEnabled: FLAG,
37  isBracketClosingEnabled: FLAG,
38  isLineNumbersDisplayed: FLAG,
39  isActiveLineHighlighted: FLAG,
40  isPatternHighlightingEnabled: FLAG,
41  isAutoCompletionEnabled: FLAG,
42  isTooltipEnabled: FLAG,
43  isLineWrappingEnabled: FLAG,
44  isMinimapEnabled: FLAG,
45  isTabIndentationEnabled: FLAG,
46  isMultiCursorEnabled: FLAG,
47  isFlashEnabled: FLAG,
48  isButtonRowHidden: FLAG,
49  isCSSAnimationDisabled: FLAG,
50};
51
52export const MAX_THEME_CHARS = 64;
53
54const isPlain = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
55const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);

Why value is not a valid key, or null. themes, when given, is the tab's own list of themes.

59export function whyNotSetting(key, value, themes) {
60  if (!own(SETTINGS, key)) return `${String(key).slice(0, 40)} is not a setting an MCP may change`;
61  const rule = SETTINGS[key];
62  switch (rule.type) {
63    case 'boolean':
64      return typeof value === 'boolean' ? null : `${key} is true or false`;
65    case 'number':
66      return typeof value === 'number' && Number.isInteger(value) && value >= rule.min && value <= rule.max
67        ? null
68        : `${key} is a whole number from ${rule.min} to ${rule.max}`;
69    case 'enum':
70      return rule.values.includes(value) ? null : `${key} is one of ${rule.values.join(', ')}`;
71    default:
72      if (typeof value !== 'string' || !value || value.length > MAX_THEME_CHARS) return `${key} is a name`;
73      if (themes && !themes.includes(value)) return `no theme ${value}; get_settings lists them`;
74      return null;
75  }
76}

Why changes is not a set of changes, or null.

79export function whyNotChanges(changes, themes) {
80  if (!isPlain(changes)) return 'changes is an object of setting: value';
81  const keys = Object.keys(changes);
82  if (!keys.length) return 'no changes';
83  for (const key of keys) {
84    const why = whyNotSetting(key, changes[key], themes);
85    if (why) return why;
86  }
87  return null;
88}

The allowed settings out of a settings object, typed; anything else dropped.

91export function settingsView(all) {
92  const view = {};
93  if (!isPlain(all)) return view;
94  for (const key of Object.keys(SETTINGS)) {
95    if (!own(all, key)) continue;
96    let value = all[key];
97    const rule = SETTINGS[key];
98    if (rule.type === 'boolean') value = value === true || value === 'true';
99    else if (rule.type === 'number') value = Number(value);
100    if (whyNotSetting(key, value) === null) view[key] = value;
101  }
102  return view;
103}