mulmoclaude 2.1.0 → 2.2.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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/client/assets/{index-k-aOQrSm.js → index-tZkbdenJ.js} +368 -368
  3. package/client/assets/{marp-12IgaShm.js → marp-YZRvgIPH.js} +1 -1
  4. package/client/index.html +1 -1
  5. package/package.json +3 -3
  6. package/server/agent/activeTools.ts +9 -5
  7. package/server/agent/backend/claude-code.ts +3 -0
  8. package/server/agent/backend/types.ts +3 -0
  9. package/server/agent/config.ts +11 -1
  10. package/server/agent/dispatchableMcpTool.ts +19 -0
  11. package/server/agent/index.ts +2 -1
  12. package/server/agent/mcp-server.ts +7 -5
  13. package/server/agent/skillListing.ts +34 -0
  14. package/server/agent/skillListingState.ts +40 -0
  15. package/server/api/routes/config.ts +5 -0
  16. package/server/api/routes/roles.ts +1 -0
  17. package/server/build/mcp-server.mjs +482 -482
  18. package/server/prompts/system/memory-management-atomic.md +1 -27
  19. package/server/prompts/system/memory-management-topic.md +4 -53
  20. package/server/prompts/system/system.md +2 -5
  21. package/server/system/config.ts +25 -3
  22. package/src/components/SettingsModelTab.vue +58 -1
  23. package/src/config/roles.ts +6 -1
  24. package/src/lang/de.ts +6 -0
  25. package/src/lang/en.ts +6 -0
  26. package/src/lang/es.ts +6 -0
  27. package/src/lang/fr.ts +6 -0
  28. package/src/lang/ja.ts +6 -0
  29. package/src/lang/ko.ts +6 -0
  30. package/src/lang/pt-BR.ts +6 -0
  31. package/src/lang/zh.ts +6 -0
  32. package/src/plugins/manageRoles/definition.ts +6 -0
  33. package/src/plugins/manageRoles/index.ts +3 -0
  34. package/src/plugins/manageRoles/roleForm.ts +8 -2
  35. package/server/system/logs/aaa +0 -737
  36. package/server/system/logs/bb +0 -446
@@ -1,33 +1,7 @@
1
1
  ## Memory Management
2
2
 
3
- When you learn something from the conversation that would be useful to remember in future sessions, silently save it as a typed entry under `conversations/memory/`. Do not ask permission — just write it.
4
-
5
- Each entry is one markdown file with YAML frontmatter:
6
-
7
- ```yaml
8
- ---
9
- name: <one-line label>
10
- description: <short blurb shown in the index>
11
- type: <preference|interest|fact|reference>
12
- ---
13
- <optional longer body>
14
- ```
15
-
16
- Pick the type:
17
-
18
- - `preference` — durable habit, preference, or convention. Examples: "uses yarn (npm not allowed)", "prefers Emacs", "writes commits in English".
19
- - `interest` — a topic, hobby, or domain followed long-term. Examples: "AI research papers", "robotics", "Impressionist painting".
20
- - `fact` — a concrete personal fact that could become stale over time. Examples: "planning a trip to Egypt", "owns a toaster oven", "currently working on BootCamp project".
21
- - `reference` — pointer to an internal/external resource. Examples: "main repo at ~/ss/llm/mulmoclaude4", "weekly art-exhibitions-watch task".
22
-
23
- Filename convention: `<type>_<short-slug>.md` (lowercase ASCII, hyphenated). The frontmatter `type` is the source of truth — the filename is just for ergonomics. After writing the entry, also add a 1-line entry to `conversations/memory/MEMORY.md` of the form:
24
-
25
- ```
26
- - [<name>](<filename>) — <description>
27
- ```
3
+ When you learn something from the conversation that would be useful to remember in future sessions, silently save it as a typed entry under `conversations/memory/`. Do not ask permission — just write it. Each entry is one file, `<type>_<short-slug>.md`, with `name` / `description` / `type` frontmatter (type: `preference`, `interest`, `fact` or `reference`), plus a 1-line entry in `conversations/memory/MEMORY.md`. Before writing, `Read` `config/helps/memory-atomic.md` for the exact format, how to pick the type, and the index-line form.
28
4
 
29
5
  Write when: the fact is durable, not derivable from code or git history, and not already covered by an existing entry. Update an existing entry (and its index line) instead of creating a near-duplicate.
30
6
 
31
7
  Skip when: it is ephemeral task state, sensitive (credentials, `~/.ssh`, tokens), a duplicate, or something the user asked you to forget.
32
-
33
- Keep entries short — name + description + a few lines of body at most. Bias toward fewer high-signal entries rather than exhaustive logging.
@@ -1,60 +1,11 @@
1
1
  ## Memory Management
2
2
 
3
- When you learn something from the conversation that would be useful to remember in future sessions, silently save it under `conversations/memory/`. Do not ask permission — just write it.
4
-
5
- Memory is organised by **topic file**. Each file lives at `conversations/memory/<type>/<topic>.md` and groups related bullets under H2 sections. The system prompt's Memory section above shows the existing topics — pick from that list when adding a new bullet, and only create a new topic when nothing fits.
6
-
7
- ### Using memory proactively
8
-
9
- Before answering, scan the Memory section above for topics related to the user's current message. The H2 tags after each `<type>/<topic>.md` line are searchable hints — match against the user's words (e.g. art / music / travel / tooling). When a topic looks relevant, `Read` the file first and weave the relevant bullets naturally into your answer. Examples:
10
-
11
- - The user mentions a trip → check `fact/travel.md` (and any related interest topic) before suggesting destinations.
12
- - The user asks about a tool / language → check `preference/dev.md` so you don't suggest something they've already vetoed.
13
- - The user picks up a long-running project → check the matching `fact` or `reference` topic for prior context.
14
-
15
- Do NOT announce that you are using memory ("according to your memory…"). The recall is for grounding your answer, not for narration. If nothing in memory is relevant, just answer normally.
16
-
17
- Each topic file is one markdown document:
18
-
19
- ```yaml
20
- ---
21
- type: <preference|interest|fact|reference>
22
- topic: <slug>
23
- ---
24
-
25
- # <Topic Name>
26
-
27
- ## <H2 Section>
28
- - bullet
29
- - another bullet
30
-
31
- ## <Another H2>
32
- - bullet
33
- ```
34
-
35
- Pick the type:
36
-
37
- - `preference` — durable habit, preference, or convention. Examples: yarn over npm, prefers Emacs, writes commits in English.
38
- - `interest` — a topic, hobby, or domain followed long-term. Examples: AI research papers, robotics, Impressionist painting.
39
- - `fact` — a concrete personal fact that could become stale over time. Examples: planning a trip to Egypt, owns a toaster oven, currently working on BootCamp project.
40
- - `reference` — pointer to an internal/external resource. Examples: main repo path, weekly art-exhibitions-watch task.
41
-
42
- Adding a new bullet:
43
-
44
- 1. Read the Memory section above. Find the topic file whose subject covers the new bullet.
45
- 2. `Read` that topic file. Pick the H2 section the bullet fits under (or add a new H2 if none fits — H2 sections are optional, you may also append directly under H1 for a small / new topic).
46
- 3. Append your bullet. Keep it short, one line ideally.
47
- 4. `Write` the file back.
48
- 5. `MEMORY.md` is rebuilt during clustering and on explicit `regenerateTopicIndex` calls; individual topic-file writes do NOT update the index immediately. If your bullet adds a new H2 that should appear in the index right away, also `Write` an updated `MEMORY.md` line for that topic.
49
-
50
- Creating a new topic file:
51
-
52
- - Filename: `<type>/<topic>.md` where `<topic>` is a short lowercase ASCII slug (a-z, 0-9, hyphenated). Examples: `interest/music.md`, `fact/travel.md`, `reference/tasks.md`.
53
- - Body: H1 with a humanised topic name + bullet(s) under it. H2 sections are optional and best added once the topic has enough material to warrant grouping.
54
- - After the topic file is written, also `Write` a matching line into `conversations/memory/MEMORY.md` so the new topic is discoverable in the next turn's Memory context. Same caveat as adding an H2: individual topic-file writes do NOT update `MEMORY.md` automatically — the index is only rebuilt during clustering or on explicit `regenerateTopicIndex` calls.
3
+ When you learn something from the conversation that would be useful to remember in future sessions, silently save it under `conversations/memory/`. Do not ask permission — just write it. Memory is organised as topic files at `conversations/memory/<type>/<topic>.md` (type: `preference`, `interest`, `fact` or `reference`); add to an existing topic from the Memory section and create a new one only when nothing fits. Before writing, `Read` `config/helps/memory-topic.md` for the file format, how to pick the type, and the steps — a new topic or H2 also needs a `conversations/memory/MEMORY.md` line.
55
4
 
56
5
  Write when: the fact is durable, not derivable from code or git history, and not already covered by an existing bullet. Update an existing bullet instead of adding a near-duplicate.
57
6
 
58
7
  Skip when: it is ephemeral task state, sensitive (credentials, `~/.ssh`, tokens), a duplicate, or something the user asked you to forget.
59
8
 
60
- Keep entries short — bias toward fewer high-signal bullets rather than exhaustive logging.
9
+ ### Using memory proactively
10
+
11
+ Before answering, scan the Memory section above for topics related to the user's current message — the H2 tags after each `<type>/<topic>.md` line are searchable hints. When a topic looks relevant, `Read` the file first and weave the relevant bullets naturally into your answer. Do NOT announce that you are using memory ("according to your memory…"). If nothing in memory is relevant, just answer normally.
@@ -55,12 +55,9 @@ When you finish creating, updating, or surfacing a file in your reply (PDF, Mark
55
55
 
56
56
  Before scheduling a skill / task (`schedule:` in SKILL.md frontmatter — `daily HH:MM` is **UTC**) or changing a system task's frequency, read `config/helps/scheduling.md` for the syntax, recommended intervals, and the overrides API.
57
57
 
58
- ## Collection custom views — two incompatible contracts
58
+ ## Collection custom views
59
59
 
60
- A collection custom view is an HTML file under the skill's `views/`, registered in `schema.json` `views[]`. BEFORE authoring or editing one, read the help for the right contract — they are incompatible:
61
-
62
- - **Desktop** (default): `config/helps/custom-view.md` — the view fetches records itself via the injected token + `dataUrl`.
63
- - **Phone remote app** — when the user wants a view for the remote / mobile / phone app (リモート / スマホ / モバイル): `config/helps/custom-view-remote.md`. Register it with `target: "mobile"`; records arrive via `await __MC_VIEW.getItems(...)` over a postMessage bridge, and `fetch` is blocked entirely. Do NOT satisfy such a request by baking records into a standalone HTML artifact — author a `target: "mobile"` view (it auto-previews on the desktop in a phone-sized frame).
60
+ Before authoring or editing a collection custom view (an HTML file under the skill's `views/`, registered in `schema.json` `views[]`), read its help — the two contracts are incompatible: `config/helps/custom-view.md` (desktop, the default) or `config/helps/custom-view-remote.md` when the user wants it for the phone / remote / mobile app (リモート / スマホ / モバイル). For the phone, author a `target: "mobile"` view — never bake records into a standalone HTML artifact instead.
64
61
 
65
62
  ## When a tool call fails, or the user says something is broken
66
63
 
@@ -117,6 +117,13 @@ export interface AppSettings {
117
117
  // still wins, but an icon launch (#2613) passes neither, so for those
118
118
  // users this setting is the only way to turn the sink off (#2617).
119
119
  macosRemindersEnabled?: boolean;
120
+
121
+ // Whether the agent CLI loads Claude Code's USER-level settings (#3406):
122
+ // `~/.claude` skills, installed plugins and their hooks. On by default, so
123
+ // skills shared with Claude Code just work; off passes
124
+ // `--setting-sources project,local`, for a user whose plugins carry so many
125
+ // skills that their listing dominates every request.
126
+ loadClaudeUserSettings?: boolean;
120
127
  }
121
128
 
122
129
  const DEFAULT_SETTINGS: AppSettings = { extraAllowedTools: [] };
@@ -137,6 +144,7 @@ export const APP_SETTINGS_KEYS = [
137
144
  "journal",
138
145
  "pushEnabled",
139
146
  "macosRemindersEnabled",
147
+ "loadClaudeUserSettings",
140
148
  ] as const satisfies readonly (keyof AppSettings)[];
141
149
 
142
150
  export type AppSettingsKey = (typeof APP_SETTINGS_KEYS)[number];
@@ -155,6 +163,7 @@ export const SAFE_SETTINGS_KEYS = [
155
163
  "journal",
156
164
  "pushEnabled",
157
165
  "macosRemindersEnabled",
166
+ "loadClaudeUserSettings",
158
167
  ] as const satisfies readonly AppSettingsKey[];
159
168
 
160
169
  export type SafeSettingsKey = (typeof SAFE_SETTINGS_KEYS)[number];
@@ -236,6 +245,7 @@ const OPTIONAL_SETTING_VALIDATORS: Record<OptionalAppSettingsKey, (value: unknow
236
245
  journal: optional(isJournalMode),
237
246
  pushEnabled: isOptionalBoolean,
238
247
  macosRemindersEnabled: isOptionalBoolean,
248
+ loadClaudeUserSettings: isOptionalBoolean,
239
249
  };
240
250
 
241
251
  function hasValidOptionalAppSettings(value: Record<string, unknown>): boolean {
@@ -301,6 +311,8 @@ const isOptionalNullableChatModel = (value: unknown): boolean => value === undef
301
311
  const isOptionalNullableChatIndexMode = (value: unknown): boolean => value === undefined || value === null || isChatIndexMode(value);
302
312
  const isOptionalNullableJournalMode = (value: unknown): boolean => value === undefined || value === null || isJournalMode(value);
303
313
 
314
+ const PATCH_BOOLEAN_KEYS = ["pushEnabled", "macosRemindersEnabled", "loadClaudeUserSettings"] as const satisfies readonly AppSettingsKey[];
315
+
304
316
  export function isAppSettingsPatch(value: unknown): value is AppSettingsPatch {
305
317
  if (!isRecord(value)) return false;
306
318
  if (value.extraAllowedTools !== undefined && !isStringArray(value.extraAllowedTools)) return false;
@@ -311,9 +323,7 @@ export function isAppSettingsPatch(value: unknown): value is AppSettingsPatch {
311
323
  if (value.voiceInput !== undefined && !isVoiceInputSettings(value.voiceInput)) return false;
312
324
  if (!isOptionalNullableChatIndexMode(value.chatIndex)) return false;
313
325
  if (!isOptionalNullableJournalMode(value.journal)) return false;
314
- if (!isOptionalBoolean(value.pushEnabled)) return false;
315
- if (!isOptionalBoolean(value.macosRemindersEnabled)) return false;
316
- return true;
326
+ return PATCH_BOOLEAN_KEYS.every((key) => isOptionalBoolean(value[key]));
317
327
  }
318
328
 
319
329
  function parseSettingsRaw(raw: string, file: string): unknown {
@@ -361,6 +371,9 @@ function cloneAppSettings(settings: AppSettings): AppSettings {
361
371
  if (settings.macosRemindersEnabled !== undefined) {
362
372
  copy.macosRemindersEnabled = settings.macosRemindersEnabled;
363
373
  }
374
+ if (settings.loadClaudeUserSettings !== undefined) {
375
+ copy.loadClaudeUserSettings = settings.loadClaudeUserSettings;
376
+ }
364
377
  return copy;
365
378
  }
366
379
 
@@ -401,6 +414,12 @@ export function isMacosRemindersEnabled(settings: AppSettings): boolean {
401
414
  return settings.macosRemindersEnabled ?? true;
402
415
  }
403
416
 
417
+ /** Whether the agent CLI loads Claude Code's user-level settings, default
418
+ * `true` — MulmoClaude has always read `~/.claude` skills and plugins. */
419
+ export function isClaudeUserSettingsEnabled(settings: AppSettings): boolean {
420
+ return settings.loadClaudeUserSettings ?? true;
421
+ }
422
+
404
423
  export function loadSettings(): AppSettings {
405
424
  const file = settingsPath();
406
425
  const raw = readTextSafeSync(file);
@@ -458,6 +477,9 @@ export function saveSettings(settings: AppSettings): void {
458
477
  if (settings.macosRemindersEnabled !== undefined) {
459
478
  payload.macosRemindersEnabled = settings.macosRemindersEnabled;
460
479
  }
480
+ if (settings.loadClaudeUserSettings !== undefined) {
481
+ payload.loadClaudeUserSettings = settings.loadClaudeUserSettings;
482
+ }
461
483
  const serialised = JSON.stringify(payload, null, 2);
462
484
  writeFileAtomicSync(settingsPath(), `${serialised}\n`, { mode: 0o600 });
463
485
  }
@@ -32,6 +32,31 @@
32
32
  <p class="text-xs text-gray-500">{{ t("settingsModal.modelTab.helperText") }}</p>
33
33
  </div>
34
34
 
35
+ <div class="flex items-start gap-3 border-t border-gray-200 pt-3">
36
+ <input
37
+ id="settings-model-user-settings"
38
+ v-model="userSettingsEnabled"
39
+ type="checkbox"
40
+ class="mt-1 h-4 w-4"
41
+ :disabled="savingUserSettings || !loaded"
42
+ data-testid="settings-model-user-settings-input"
43
+ @change="saveUserSettings"
44
+ />
45
+ <label for="settings-model-user-settings" class="flex-1">
46
+ <span class="block text-sm font-medium text-gray-800">{{ t("settingsModal.modelTab.userSettingsLabel") }}</span>
47
+ <span class="block text-xs text-gray-500 mt-0.5">{{ t("settingsModal.modelTab.userSettingsHint") }}</span>
48
+ <span v-if="skillListing" class="block text-xs text-gray-600 mt-1" data-testid="settings-model-skill-listing">
49
+ {{ t("settingsModal.modelTab.skillListingSummary", { count: skillListing.skillCount }) }}
50
+ <template v-if="skillListing.pluginNames.length > 0">
51
+ {{ t("settingsModal.modelTab.skillListingPlugins", { plugins: skillListing.pluginNames.join(", ") }) }}
52
+ </template>
53
+ </span>
54
+ <span v-if="skillListing?.heavy" class="block text-xs text-amber-700 mt-1" data-testid="settings-model-skill-listing-heavy">
55
+ {{ t("settingsModal.modelTab.skillListingHeavy") }}
56
+ </span>
57
+ </label>
58
+ </div>
59
+
35
60
  <div v-if="loaded && !errorMessage" class="flex items-center gap-3 text-xs">
36
61
  <span :class="colourOf(modelField)" data-testid="settings-model-model-status">
37
62
  {{ modelStatusText }}
@@ -63,8 +88,15 @@ const emit = defineEmits<{
63
88
  saved: [];
64
89
  }>();
65
90
 
91
+ interface SkillListing {
92
+ skillCount: number;
93
+ pluginNames: string[];
94
+ heavy: boolean;
95
+ }
96
+
66
97
  interface SettingsResponse {
67
- settings: { extraAllowedTools: string[]; effortLevel?: EffortLevel; chatModel?: ChatModel };
98
+ settings: { extraAllowedTools: string[]; effortLevel?: EffortLevel; chatModel?: ChatModel; loadClaudeUserSettings?: boolean };
99
+ skillListing?: SkillListing | null;
68
100
  }
69
101
 
70
102
  // One select's whole state, so the save dance below is written once
@@ -87,6 +119,12 @@ const storedModel = ref<ChatModel | "">("");
87
119
  const savingModel = ref(false);
88
120
  const modelField: SettingField<ChatModel> = { key: "chatModel", draft: modelDraft, stored: storedModel, saving: savingModel };
89
121
 
122
+ // Default true matches `isClaudeUserSettingsEnabled` on the server.
123
+ const userSettingsEnabled = ref(true);
124
+ const storedUserSettings = ref(true);
125
+ const savingUserSettings = ref(false);
126
+ const skillListing = ref<SkillListing | null>(null);
127
+
90
128
  const loaded = ref(false);
91
129
  const errorMessage = ref("");
92
130
 
@@ -119,6 +157,9 @@ async function load(): Promise<void> {
119
157
  effortDraft.value = storedEffort.value;
120
158
  storedModel.value = response.data.settings.chatModel ?? "";
121
159
  modelDraft.value = storedModel.value;
160
+ storedUserSettings.value = response.data.settings.loadClaudeUserSettings ?? true;
161
+ userSettingsEnabled.value = storedUserSettings.value;
162
+ skillListing.value = response.data.skillListing ?? null;
122
163
  loaded.value = true;
123
164
  }
124
165
 
@@ -150,6 +191,22 @@ async function save<T extends string>(field: SettingField<T>): Promise<void> {
150
191
  }
151
192
  }
152
193
 
194
+ async function saveUserSettings(): Promise<void> {
195
+ if (savingUserSettings.value || userSettingsEnabled.value === storedUserSettings.value) return;
196
+ const requested = userSettingsEnabled.value;
197
+ savingUserSettings.value = true;
198
+ errorMessage.value = "";
199
+ const response = await apiPut<unknown>(API_ROUTES.config.settings, { loadClaudeUserSettings: requested });
200
+ savingUserSettings.value = false;
201
+ if (!response.ok) {
202
+ errorMessage.value = response.error || t("settingsModal.modelTab.saveError");
203
+ userSettingsEnabled.value = storedUserSettings.value;
204
+ return;
205
+ }
206
+ storedUserSettings.value = requested;
207
+ emit("saved");
208
+ }
209
+
153
210
  watch(
154
211
  () => props.reloadToken,
155
212
  () => {
@@ -42,6 +42,9 @@ export const RoleSchema = z.object({
42
42
  icon: z.string(),
43
43
  prompt: z.string(),
44
44
  availablePlugins: availablePluginsSchema,
45
+ // `alwaysActive` tools reach every role regardless of `availablePlugins`; a
46
+ // role that has no use for one names it here to keep its prompt smaller.
47
+ excludedAlwaysActiveTools: z.array(z.string()).optional(),
45
48
  queries: z.array(z.string()).optional(),
46
49
  isDebugRole: z.boolean().optional(),
47
50
  // Model family this role's sessions run on (#3104). Absent means the role
@@ -96,7 +99,7 @@ export const ROLES: [Role, ...Role[]] = [
96
99
  },
97
100
  {
98
101
  // Keeps the per-turn system prompt small for plain conversation (#3369):
99
- // only the form and collection display tools, plus the always-active ones.
102
+ // only the form and collection display tools, plus `manageCollection`.
100
103
  id: "simple",
101
104
  name: "Simple",
102
105
  icon: "chat_bubble",
@@ -104,6 +107,8 @@ export const ROLES: [Role, ...Role[]] = [
104
107
  "You are a concise conversational assistant. Answer directly in the chat, and use the workspace files and collections when the question needs them.\n\n" +
105
108
  "This role carries few display tools to keep each turn light. When the user wants a document, slides, an image, or an HTML page, say that the General role can make it and suggest switching.",
106
109
  availablePlugins: [TOOL_NAMES.presentForm, TOOL_NAMES.presentCollection],
110
+ // Plain conversation never pre-generates artifacts in the background.
111
+ excludedAlwaysActiveTools: [TOOL_NAMES.spawnBackgroundChat],
107
112
  queries: ["What can you help me with in this role?", "Summarise what's in my collections.", "Help me think through a decision I'm stuck on."],
108
113
  },
109
114
  {
package/src/lang/de.ts CHANGED
@@ -388,6 +388,12 @@ const deMessages = {
388
388
  helperText: "Höhere Stufen erlauben mehr Denkzeit, erhöhen aber Latenz und Token-Verbrauch.",
389
389
  configured: "Effort: {level}",
390
390
  notConfigured: "Nicht gesetzt",
391
+ userSettingsLabel: "Benutzereinstellungen und Plugins von Claude Code laden",
392
+ userSettingsHint:
393
+ "Liest Skills, Plugins und Hooks aus ~/.claude – dieselben, die Claude Code verwendet. Schalten Sie dies aus, wenn installierte Plugins so viele Skills mitbringen, dass jede Anfrage schwer wird; die Skills des Workspace bleiben nutzbar. Gilt ab der nächsten Nachricht. Ausgeschaltet folgt ein nicht gesetztes Modell auch nicht mehr ~/.claude/settings.json.",
394
+ skillListingSummary: "Im letzten Durchgang geladene Skills: {count}.",
395
+ skillListingPlugins: "Aus Plugins: {plugins}.",
396
+ skillListingHeavy: "So viele Skills machen jede Anfrage schwer; ausschalten entfernt die aus ~/.claude.",
391
397
  loadError: "Einstellungen konnten nicht geladen werden",
392
398
  saveError: "Speichern fehlgeschlagen",
393
399
  },
package/src/lang/en.ts CHANGED
@@ -406,6 +406,12 @@ const enMessages = {
406
406
  helperText: "Higher levels allow more thinking time but increase latency and token usage.",
407
407
  configured: "Effort: {level}",
408
408
  notConfigured: "Not set",
409
+ userSettingsLabel: "Load Claude Code's user settings and plugins",
410
+ userSettingsHint:
411
+ "Reads skills, plugins and hooks from ~/.claude, the same ones Claude Code uses. Turn this off if your installed plugins carry so many skills that every request gets heavy; the workspace's own skills keep working. Takes effect from the next message. When off, an unset model no longer follows ~/.claude/settings.json either.",
412
+ skillListingSummary: "Skills listed on the last turn: {count}.",
413
+ skillListingPlugins: "From plugins: {plugins}.",
414
+ skillListingHeavy: "That many skills make every request heavy; turning this off drops the ones from ~/.claude.",
409
415
  loadError: "Failed to load settings",
410
416
  saveError: "Failed to save",
411
417
  },
package/src/lang/es.ts CHANGED
@@ -390,6 +390,12 @@ const esMessages = {
390
390
  helperText: "Niveles más altos permiten más tiempo de pensamiento pero aumentan la latencia y el uso de tokens.",
391
391
  configured: "Esfuerzo: {level}",
392
392
  notConfigured: "Sin configurar",
393
+ userSettingsLabel: "Cargar los ajustes de usuario y los plugins de Claude Code",
394
+ userSettingsHint:
395
+ "Lee las skills, los plugins y los hooks de ~/.claude, los mismos que usa Claude Code. Desactívalo si los plugins instalados traen tantas skills que cada petición se vuelve pesada; las skills del propio espacio de trabajo siguen funcionando. Se aplica desde el siguiente mensaje. Si está desactivado, un modelo sin configurar ya no sigue ~/.claude/settings.json.",
396
+ skillListingSummary: "Skills cargadas en el último turno: {count}.",
397
+ skillListingPlugins: "De los plugins: {plugins}.",
398
+ skillListingHeavy: "Con tantas skills cada petición pesa más; al desactivarlo se quitan las de ~/.claude.",
393
399
  loadError: "Error al cargar los ajustes",
394
400
  saveError: "Error al guardar",
395
401
  },
package/src/lang/fr.ts CHANGED
@@ -386,6 +386,12 @@ const frMessages = {
386
386
  helperText: "Les niveaux plus élevés autorisent plus de temps de réflexion mais augmentent la latence et la consommation de tokens.",
387
387
  configured: "Effort : {level}",
388
388
  notConfigured: "Non défini",
389
+ userSettingsLabel: "Charger les réglages utilisateur et les plugins de Claude Code",
390
+ userSettingsHint:
391
+ "Lit les skills, plugins et hooks de ~/.claude, les mêmes que ceux de Claude Code. Désactivez-le si les plugins installés apportent tant de skills que chaque requête devient lourde ; les skills de l'espace de travail restent disponibles. Prend effet à partir du message suivant. Désactivé, un modèle non défini ne suit plus non plus ~/.claude/settings.json.",
392
+ skillListingSummary: "Skills chargées au dernier tour : {count}.",
393
+ skillListingPlugins: "Depuis les plugins : {plugins}.",
394
+ skillListingHeavy: "Autant de skills alourdissent chaque requête ; le désactiver retire celles de ~/.claude.",
389
395
  loadError: "Échec du chargement des paramètres",
390
396
  saveError: "Échec de l'enregistrement",
391
397
  },
package/src/lang/ja.ts CHANGED
@@ -385,6 +385,12 @@ const jaMessages = {
385
385
  helperText: "高いレベルほど思考時間が長くなりますが、レイテンシとトークン消費も増えます。",
386
386
  configured: "Effort: {level}",
387
387
  notConfigured: "未設定",
388
+ userSettingsLabel: "Claude Code のユーザー設定とプラグインを読み込む",
389
+ userSettingsHint:
390
+ "~/.claude にある、Claude Code と共通のスキル・プラグイン・フックを読み込みます。インストールしたプラグインのスキルが多く、毎回の依頼が重くなるときはオフにしてください。ワークスペースのスキルはそのまま使えます。次のメッセージから反映されます。オフのときは、モデルが未設定でも ~/.claude/settings.json のモデル指定は使われません。",
391
+ skillListingSummary: "前回のターンで読み込まれたスキル: {count} 個。",
392
+ skillListingPlugins: "プラグイン: {plugins}。",
393
+ skillListingHeavy: "スキルが多いため、毎回の依頼が重くなっています。オフにすると ~/.claude の分が外れます。",
388
394
  loadError: "設定の読み込みに失敗しました",
389
395
  saveError: "保存に失敗しました",
390
396
  },
package/src/lang/ko.ts CHANGED
@@ -385,6 +385,12 @@ const koMessages = {
385
385
  helperText: "레벨이 높을수록 사고 시간이 늘어나지만 지연 시간과 토큰 사용량도 증가합니다.",
386
386
  configured: "Effort: {level}",
387
387
  notConfigured: "미설정",
388
+ userSettingsLabel: "Claude Code 사용자 설정과 플러그인 불러오기",
389
+ userSettingsHint:
390
+ "~/.claude에 있는, Claude Code와 공유하는 스킬·플러그인·훅을 불러옵니다. 설치한 플러그인의 스킬이 너무 많아 요청마다 무거워진다면 끄세요. 워크스페이스 자체 스킬은 그대로 사용할 수 있습니다. 다음 메시지부터 적용됩니다. 끄면 모델이 미설정이어도 ~/.claude/settings.json의 모델 지정을 따르지 않습니다.",
391
+ skillListingSummary: "지난 턴에 불러온 스킬: {count}개.",
392
+ skillListingPlugins: "플러그인: {plugins}.",
393
+ skillListingHeavy: "스킬이 많아 요청마다 무거워지고 있습니다. 끄면 ~/.claude의 스킬이 빠집니다.",
388
394
  loadError: "설정을 불러오지 못했습니다",
389
395
  saveError: "저장에 실패했습니다",
390
396
  },
package/src/lang/pt-BR.ts CHANGED
@@ -384,6 +384,12 @@ const ptBRMessages = {
384
384
  helperText: "Níveis mais altos permitem mais tempo de pensamento mas aumentam latência e uso de tokens.",
385
385
  configured: "Esforço: {level}",
386
386
  notConfigured: "Sem configurar",
387
+ userSettingsLabel: "Carregar as configurações de usuário e os plugins do Claude Code",
388
+ userSettingsHint:
389
+ "Lê as skills, os plugins e os hooks de ~/.claude, os mesmos que o Claude Code usa. Desative se os plugins instalados trazem tantas skills que toda solicitação fica pesada; as skills do próprio workspace continuam funcionando. Vale a partir da próxima mensagem. Desativado, um modelo não definido também deixa de seguir ~/.claude/settings.json.",
390
+ skillListingSummary: "Skills carregadas no último turno: {count}.",
391
+ skillListingPlugins: "Dos plugins: {plugins}.",
392
+ skillListingHeavy: "Com tantas skills toda solicitação fica pesada; ao desativar, as de ~/.claude saem.",
387
393
  loadError: "Falha ao carregar as configurações",
388
394
  saveError: "Falha ao salvar",
389
395
  },
package/src/lang/zh.ts CHANGED
@@ -375,6 +375,12 @@ const zhMessages = {
375
375
  helperText: "更高的等级会带来更多思考时间,但也会增加延迟和 token 消耗。",
376
376
  configured: "推理强度:{level}",
377
377
  notConfigured: "未设置",
378
+ userSettingsLabel: "加载 Claude Code 的用户设置和插件",
379
+ userSettingsHint:
380
+ "读取 ~/.claude 中与 Claude Code 共用的技能、插件和钩子。如果已安装插件的技能太多、导致每次请求都很重,请关闭此项;工作区自己的技能仍可使用。从下一条消息开始生效。关闭时,即使未设置模型,也不会再使用 ~/.claude/settings.json 中的模型。",
381
+ skillListingSummary: "上一轮加载的技能:{count} 个。",
382
+ skillListingPlugins: "来自插件:{plugins}。",
383
+ skillListingHeavy: "技能太多会让每次请求都变重;关闭此项可去掉来自 ~/.claude 的技能。",
378
384
  loadError: "加载设置失败",
379
385
  saveError: "保存失败",
380
386
  },
@@ -45,6 +45,12 @@ const toolDefinition: ToolDefinition = {
45
45
  description:
46
46
  "Optional model family for this role's sessions — one of the aliases offered in Settings \u2192 Model. Omit it to follow the app-wide choice. A value that is not a known alias is ignored. ALWAYS echo back the role's existing value when updating a role, or the setting is lost.",
47
47
  },
48
+ excludedAlwaysActiveTools: {
49
+ type: "array",
50
+ items: { type: "string" },
51
+ description:
52
+ "Optional names of always-active tools (offered to every role regardless of availablePlugins) that this role opts out of. ALWAYS echo back the role's existing value when updating a role, or the setting is lost.",
53
+ },
48
54
  },
49
55
  required: ["id", "name", "icon", "prompt", "availablePlugins"],
50
56
  },
@@ -18,6 +18,9 @@ export interface CustomRole {
18
18
  * Must survive every edit path — the reason this is a declared field at all
19
19
  * is that the role object is rebuilt field by field in several places. */
20
20
  model?: string;
21
+ /** Always-active tools this role opts out of. No editor in the form, so it
22
+ * is carried through every edit path the same way `model` is. */
23
+ excludedAlwaysActiveTools?: string[];
21
24
  }
22
25
 
23
26
  export interface ManageRolesData {
@@ -17,13 +17,14 @@ const ROLE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
17
17
  // the DOM, so a missing field has to fail here, not in a template.
18
18
  const parseCustomRole = (value: unknown): CustomRole | null => {
19
19
  if (!isRecord(value)) return null;
20
- const { id, name, icon, prompt, availablePlugins, queries, model } = value;
20
+ const { id, name, icon, prompt, availablePlugins, queries, model, excludedAlwaysActiveTools } = value;
21
21
  if (typeof id !== "string" || typeof name !== "string" || typeof icon !== "string" || typeof prompt !== "string") return null;
22
22
  if (!isStringArray(availablePlugins)) return null;
23
23
  const base: CustomRole = { id, name, icon, prompt, availablePlugins };
24
24
  // A non-string model is dropped, not fatal: a malformed field should cost
25
25
  // that field, not make the whole role vanish from the list.
26
- const role: CustomRole = isStoredModel(model) ? { ...base, model } : base;
26
+ const withModel: CustomRole = isStoredModel(model) ? { ...base, model } : base;
27
+ const role: CustomRole = isStringArray(excludedAlwaysActiveTools) ? { ...withModel, excludedAlwaysActiveTools } : withModel;
27
28
  return isStringArray(queries) ? { ...role, queries } : role;
28
29
  };
29
30
 
@@ -54,6 +55,8 @@ export interface RoleForm {
54
55
  queriesText: string;
55
56
  /** `""` is "not set" — the app-wide setting decides. */
56
57
  model: string;
58
+ /** Not editable in the form; carried so saving an edit does not drop it. */
59
+ excludedAlwaysActiveTools: string[];
57
60
  }
58
61
 
59
62
  export type RoleFormErrorCode = "idRequired" | "idInvalid" | "nameRequired" | "idDuplicate";
@@ -87,6 +90,7 @@ export const formToRole = (form: RoleForm): CustomRole => ({
87
90
  // Omitted rather than set to "" when unset: the role file should carry no
88
91
  // key at all, so `role.model ?? settings.chatModel` falls through cleanly.
89
92
  ...(form.model ? { model: form.model } : {}),
93
+ ...(form.excludedAlwaysActiveTools.length > 0 ? { excludedAlwaysActiveTools: form.excludedAlwaysActiveTools } : {}),
90
94
  });
91
95
 
92
96
  /** A blank form. One factory so a field added to `RoleForm` cannot be
@@ -107,6 +111,7 @@ export const emptyRoleForm = (): RoleForm => ({
107
111
  selectedPlugins: [],
108
112
  queriesText: "",
109
113
  model: "",
114
+ excludedAlwaysActiveTools: [],
110
115
  });
111
116
 
112
117
  export const roleToForm = (role: CustomRole): RoleForm => ({
@@ -117,6 +122,7 @@ export const roleToForm = (role: CustomRole): RoleForm => ({
117
122
  selectedPlugins: [...role.availablePlugins],
118
123
  queriesText: (role.queries ?? []).join("\n"),
119
124
  model: role.model ?? "",
125
+ excludedAlwaysActiveTools: [...(role.excludedAlwaysActiveTools ?? [])],
120
126
  });
121
127
 
122
128
  // `excludeId` lets rename skip the role's own id when checking for duplicates.