mulmoclaude 0.9.3 → 0.9.4

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 (45) hide show
  1. package/client/assets/{index-40ErrJ4a.js → index-B3NxFcEH.js} +36 -36
  2. package/client/assets/{marp-Dh7C24F1.js → marp-C9QDHFAJ.js} +1 -1
  3. package/client/index.html +1 -1
  4. package/package.json +6 -6
  5. package/server/api/routes/agent.ts +5 -1
  6. package/server/api/routes/collections.ts +16 -1
  7. package/server/api/routes/config.ts +12 -0
  8. package/server/remoteHost/firebase.ts +6 -0
  9. package/server/remoteHost/handlers/index.ts +4 -0
  10. package/server/remoteHost/handlers/ingestAttachments.ts +88 -0
  11. package/server/remoteHost/handlers/listAccountingBooks.ts +31 -0
  12. package/server/remoteHost/handlers/listCollections.ts +6 -1
  13. package/server/remoteHost/handlers/listSkills.ts +39 -0
  14. package/server/remoteHost/handlers/startChat.ts +103 -39
  15. package/server/system/config.ts +101 -4
  16. package/server/system/logs/aaa +737 -0
  17. package/server/system/logs/bb +446 -0
  18. package/server/workspace/chat-index/index.ts +21 -2
  19. package/server/workspace/chat-index/indexer.ts +57 -14
  20. package/server/workspace/chat-index/summarizer.ts +18 -10
  21. package/server/workspace/collections/index.ts +1 -1
  22. package/server/workspace/journal/archivist-cli.ts +17 -3
  23. package/server/workspace/journal/dailyPass.ts +52 -2
  24. package/server/workspace/journal/index.ts +20 -4
  25. package/src/App.vue +5 -0
  26. package/src/components/FileTree.vue +21 -1
  27. package/src/components/FileTreePane.vue +41 -25
  28. package/src/components/FilesView.vue +4 -0
  29. package/src/components/RemoteHostControl.vue +18 -0
  30. package/src/components/SessionHistoryPanel.vue +24 -11
  31. package/src/components/SettingsChatIndexTab.vue +119 -0
  32. package/src/components/SettingsJournalTab.vue +117 -0
  33. package/src/components/SettingsModal.vue +12 -2
  34. package/src/composables/collections/useDynamicShortcutIcons.ts +93 -0
  35. package/src/composables/useShowHiddenSystemFiles.ts +23 -0
  36. package/src/config/visibleWorkspaceDirs.ts +21 -0
  37. package/src/lang/de.ts +45 -0
  38. package/src/lang/en.ts +43 -0
  39. package/src/lang/es.ts +44 -0
  40. package/src/lang/fr.ts +45 -0
  41. package/src/lang/ja.ts +44 -0
  42. package/src/lang/ko.ts +43 -0
  43. package/src/lang/pt-BR.ts +44 -0
  44. package/src/lang/zh.ts +43 -0
  45. package/src/utils/session/sessionPreview.ts +29 -0
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <div class="space-y-3" data-testid="settings-chat-index-tab">
3
+ <p class="text-sm text-gray-700">{{ t("settingsModal.chatIndexTab.description") }}</p>
4
+
5
+ <div class="space-y-2">
6
+ <label class="block text-sm font-medium text-gray-800" for="settings-chat-index-mode">{{ t("settingsModal.chatIndexTab.modeLabel") }}</label>
7
+ <select
8
+ id="settings-chat-index-mode"
9
+ v-model="modeDraft"
10
+ class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
11
+ data-testid="settings-chat-index-mode-select"
12
+ @change="save"
13
+ >
14
+ <option v-for="mode in CHAT_INDEX_MODES" :key="mode" :value="mode">{{ t(`settingsModal.chatIndexTab.mode.${mode}`) }}</option>
15
+ </select>
16
+ <p class="text-xs text-gray-500">{{ t("settingsModal.chatIndexTab.helperText") }}</p>
17
+ </div>
18
+
19
+ <div v-if="loaded && !errorMessage" class="flex items-center gap-3 text-xs">
20
+ <span :class="statusColour" data-testid="settings-chat-index-status">
21
+ {{ statusText }}
22
+ </span>
23
+ </div>
24
+
25
+ <p v-if="errorMessage" class="text-sm text-red-700" role="alert" data-testid="settings-chat-index-error">{{ errorMessage }}</p>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { computed, ref, watch } from "vue";
31
+ import { useI18n } from "vue-i18n";
32
+ import { apiGet, apiPut, type ApiResult } from "../utils/api";
33
+ import { API_ROUTES } from "../config/apiRoutes";
34
+
35
+ const CHAT_INDEX_MODES = ["off", "haiku", "sonnet"] as const;
36
+ type ChatIndexMode = (typeof CHAT_INDEX_MODES)[number];
37
+
38
+ const { t } = useI18n();
39
+
40
+ const props = defineProps<{
41
+ reloadToken: number;
42
+ }>();
43
+
44
+ const emit = defineEmits<{
45
+ saved: [];
46
+ }>();
47
+
48
+ interface SettingsResponse {
49
+ settings: { chatIndex?: ChatIndexMode };
50
+ }
51
+
52
+ const modeDraft = ref<ChatIndexMode>("off");
53
+ const storedMode = ref<ChatIndexMode>("off");
54
+ const loaded = ref(false);
55
+ const saving = ref(false);
56
+ const errorMessage = ref("");
57
+
58
+ const statusText = computed(() => {
59
+ if (saving.value) return t("common.saving");
60
+ return t(`settingsModal.chatIndexTab.status.${storedMode.value}`);
61
+ });
62
+
63
+ const statusColour = computed(() => {
64
+ if (saving.value) return "text-gray-500";
65
+ return storedMode.value === "off" ? "text-gray-500" : "text-green-600";
66
+ });
67
+
68
+ async function load(): Promise<void> {
69
+ errorMessage.value = "";
70
+ const response = await apiGet<SettingsResponse>(API_ROUTES.config.base);
71
+ if (!response.ok) {
72
+ errorMessage.value = response.error || t("settingsModal.chatIndexTab.loadError");
73
+ return;
74
+ }
75
+ storedMode.value = response.data.settings.chatIndex ?? "off";
76
+ modeDraft.value = storedMode.value;
77
+ loaded.value = true;
78
+ }
79
+
80
+ async function save(): Promise<void> {
81
+ if (saving.value) return;
82
+ if (modeDraft.value === storedMode.value) return;
83
+ const requested = modeDraft.value;
84
+ saving.value = true;
85
+ errorMessage.value = "";
86
+ // The Settings tab uses the PATCH endpoint (`config.settings`) — the
87
+ // full-shape `config.base` PUT expects `{ settings, mcp }` and would
88
+ // reject a bare `{ chatIndex }` payload. Send null on "off" so the
89
+ // server drops the field and settings.json stays default-clean.
90
+ const payload = { chatIndex: requested === "off" ? null : requested };
91
+ const response = await apiPut<unknown>(API_ROUTES.config.settings, payload);
92
+ saving.value = false;
93
+ applySaveOutcome(response, requested);
94
+ }
95
+
96
+ // On failure with drift, respect the user's newer selection and retry.
97
+ // On failure without drift, revert modeDraft so re-selecting the same
98
+ // option fires @change again — otherwise the select value hasn't
99
+ // changed and the user is stuck with no way to retry.
100
+ function applySaveOutcome(response: ApiResult<unknown>, requested: ChatIndexMode): void {
101
+ if (!response.ok) {
102
+ errorMessage.value = response.error || t("settingsModal.chatIndexTab.saveError");
103
+ if (modeDraft.value !== requested) void save();
104
+ else modeDraft.value = storedMode.value;
105
+ return;
106
+ }
107
+ storedMode.value = requested;
108
+ emit("saved");
109
+ if (modeDraft.value !== requested) void save();
110
+ }
111
+
112
+ watch(
113
+ () => props.reloadToken,
114
+ () => {
115
+ void load();
116
+ },
117
+ { immediate: true },
118
+ );
119
+ </script>
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <div class="space-y-3" data-testid="settings-journal-tab">
3
+ <p class="text-sm text-gray-700">{{ t("settingsModal.journalTab.description") }}</p>
4
+
5
+ <div class="space-y-2">
6
+ <label class="block text-sm font-medium text-gray-800" for="settings-journal-mode">{{ t("settingsModal.journalTab.modeLabel") }}</label>
7
+ <select
8
+ id="settings-journal-mode"
9
+ v-model="modeDraft"
10
+ class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
11
+ data-testid="settings-journal-mode-select"
12
+ @change="save"
13
+ >
14
+ <option v-for="mode in JOURNAL_MODES" :key="mode" :value="mode">{{ t(`settingsModal.journalTab.mode.${mode}`) }}</option>
15
+ </select>
16
+ <p class="text-xs text-gray-500">{{ t("settingsModal.journalTab.helperText") }}</p>
17
+ </div>
18
+
19
+ <div v-if="loaded && !errorMessage" class="flex items-center gap-3 text-xs">
20
+ <span :class="statusColour" data-testid="settings-journal-status">
21
+ {{ statusText }}
22
+ </span>
23
+ </div>
24
+
25
+ <p v-if="errorMessage" class="text-sm text-red-700" role="alert" data-testid="settings-journal-error">{{ errorMessage }}</p>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { computed, ref, watch } from "vue";
31
+ import { useI18n } from "vue-i18n";
32
+ import { apiGet, apiPut, type ApiResult } from "../utils/api";
33
+ import { API_ROUTES } from "../config/apiRoutes";
34
+
35
+ const JOURNAL_MODES = ["off", "haiku", "sonnet"] as const;
36
+ type JournalMode = (typeof JOURNAL_MODES)[number];
37
+
38
+ const { t } = useI18n();
39
+
40
+ const props = defineProps<{
41
+ reloadToken: number;
42
+ }>();
43
+
44
+ const emit = defineEmits<{
45
+ saved: [];
46
+ }>();
47
+
48
+ interface SettingsResponse {
49
+ settings: { journal?: JournalMode };
50
+ }
51
+
52
+ const modeDraft = ref<JournalMode>("off");
53
+ const storedMode = ref<JournalMode>("off");
54
+ const loaded = ref(false);
55
+ const saving = ref(false);
56
+ const errorMessage = ref("");
57
+
58
+ const statusText = computed(() => {
59
+ if (saving.value) return t("common.saving");
60
+ return t(`settingsModal.journalTab.status.${storedMode.value}`);
61
+ });
62
+
63
+ const statusColour = computed(() => {
64
+ if (saving.value) return "text-gray-500";
65
+ return storedMode.value === "off" ? "text-gray-500" : "text-green-600";
66
+ });
67
+
68
+ async function load(): Promise<void> {
69
+ errorMessage.value = "";
70
+ const response = await apiGet<SettingsResponse>(API_ROUTES.config.base);
71
+ if (!response.ok) {
72
+ errorMessage.value = response.error || t("settingsModal.journalTab.loadError");
73
+ return;
74
+ }
75
+ storedMode.value = response.data.settings.journal ?? "off";
76
+ modeDraft.value = storedMode.value;
77
+ loaded.value = true;
78
+ }
79
+
80
+ async function save(): Promise<void> {
81
+ if (saving.value) return;
82
+ if (modeDraft.value === storedMode.value) return;
83
+ const requested = modeDraft.value;
84
+ saving.value = true;
85
+ errorMessage.value = "";
86
+ // PATCH endpoint (not the atomic base) so a bare `{ journal }` body
87
+ // is accepted. Send null on "off" so settings.json stays default-clean.
88
+ const payload = { journal: requested === "off" ? null : requested };
89
+ const response = await apiPut<unknown>(API_ROUTES.config.settings, payload);
90
+ saving.value = false;
91
+ applySaveOutcome(response, requested);
92
+ }
93
+
94
+ // On failure with drift, respect the user's newer selection and retry.
95
+ // On failure without drift, revert modeDraft so re-selecting the same
96
+ // option fires @change again — otherwise the select value hasn't
97
+ // changed and the user is stuck with no way to retry.
98
+ function applySaveOutcome(response: ApiResult<unknown>, requested: JournalMode): void {
99
+ if (!response.ok) {
100
+ errorMessage.value = response.error || t("settingsModal.journalTab.saveError");
101
+ if (modeDraft.value !== requested) void save();
102
+ else modeDraft.value = storedMode.value;
103
+ return;
104
+ }
105
+ storedMode.value = requested;
106
+ emit("saved");
107
+ if (modeDraft.value !== requested) void save();
108
+ }
109
+
110
+ watch(
111
+ () => props.reloadToken,
112
+ () => {
113
+ void load();
114
+ },
115
+ { immediate: true },
116
+ );
117
+ </script>
@@ -167,6 +167,10 @@
167
167
  <SettingsModelTab v-else-if="activeTab === 'model'" :reload-token="modelReloadToken" @saved="emit('saved')" />
168
168
 
169
169
  <SettingsVoiceTab v-else-if="activeTab === 'voice'" :reload-token="voiceReloadToken" />
170
+
171
+ <SettingsChatIndexTab v-else-if="activeTab === 'chatIndex'" :reload-token="chatIndexReloadToken" @saved="emit('saved')" />
172
+
173
+ <SettingsJournalTab v-else-if="activeTab === 'journal'" :reload-token="journalReloadToken" @saved="emit('saved')" />
170
174
  </template>
171
175
  </div>
172
176
  </div>
@@ -197,6 +201,8 @@ import SettingsMapTab from "./SettingsMapTab.vue";
197
201
  import SettingsPhotosTab from "./SettingsPhotosTab.vue";
198
202
  import SettingsModelTab from "./SettingsModelTab.vue";
199
203
  import SettingsVoiceTab from "./SettingsVoiceTab.vue";
204
+ import SettingsChatIndexTab from "./SettingsChatIndexTab.vue";
205
+ import SettingsJournalTab from "./SettingsJournalTab.vue";
200
206
  import SkillsView from "../plugins/manageSkills/View.vue";
201
207
  import RolesView from "./RolesView.vue";
202
208
  import PluginScopedRoot from "./PluginScopedRoot.vue";
@@ -245,7 +251,7 @@ const emit = defineEmits<{
245
251
  // update / remove persist immediately).
246
252
  const mcpTabRef = ref<{ flushDraft: () => boolean; hasPendingDraft: () => boolean } | null>(null);
247
253
 
248
- type TabId = "gemini" | "tools" | "mcp" | "dirs" | "refs" | "map" | "photos" | "model" | "voice" | "skills" | "roles";
254
+ type TabId = "gemini" | "tools" | "mcp" | "dirs" | "refs" | "map" | "photos" | "model" | "voice" | "chatIndex" | "journal" | "skills" | "roles";
249
255
 
250
256
  const activeTab = ref<TabId>("tools");
251
257
 
@@ -261,7 +267,7 @@ const isFullTab = computed(() => FULL_TABS.includes(activeTab.value));
261
267
  // item is filtered out by `visibleGroups` when geminiAvailable === true
262
268
  // (env var present → user has nothing to configure).
263
269
  const GROUPS: readonly { key: string; items: readonly TabId[] }[] = [
264
- { key: "llm", items: ["model", "voice", "tools", "gemini"] },
270
+ { key: "llm", items: ["model", "voice", "chatIndex", "journal", "tools", "gemini"] },
265
271
  { key: "servers", items: ["mcp"] },
266
272
  { key: "workspace", items: ["dirs", "refs"] },
267
273
  { key: "plugins", items: ["map", "photos"] },
@@ -294,6 +300,8 @@ function onMapSaved(): void {
294
300
  const photosReloadToken = ref(0);
295
301
  const modelReloadToken = ref(0);
296
302
  const voiceReloadToken = ref(0);
303
+ const chatIndexReloadToken = ref(0);
304
+ const journalReloadToken = ref(0);
297
305
  const toolsText = ref("");
298
306
  // Server truth for tools — updated on load and on a successful Save
299
307
  // from the Tools tab. `toolsDirty` compares this against `toolsText`
@@ -501,6 +509,8 @@ watch(
501
509
  photosReloadToken.value += 1;
502
510
  modelReloadToken.value += 1;
503
511
  voiceReloadToken.value += 1;
512
+ chatIndexReloadToken.value += 1;
513
+ journalReloadToken.value += 1;
504
514
  statusMessage.value = "";
505
515
  statusError.value = false;
506
516
  }
@@ -0,0 +1,93 @@
1
+ // Keeps pinned launcher shortcuts for collections that declare a
2
+ // `dynamicIcon` (see `CollectionSchema.dynamicIcon`) live: subscribes to
3
+ // each dynamic collection's source collection(s) and re-reconciles the
4
+ // shortcut cache whenever their records change, so the launcher icon
5
+ // tracks the data state live instead of waiting for the next
6
+ // Collections-index visit — `CollectionsIndexView`'s own reconcile-on-mount
7
+ // remains the fallback for a cold boot, before this composable's first
8
+ // fetch resolves.
9
+ //
10
+ // Mounted once at app level (src/App.vue) so it runs regardless of
11
+ // whether the Collections index is open. Module-singleton state, like
12
+ // `useShortcuts` — there's only ever one launcher to keep live.
13
+
14
+ import { onMounted, onUnmounted, ref } from "vue";
15
+ import { collectionUi } from "@mulmoclaude/collection-plugin/vue";
16
+ import { useShortcuts } from "../useShortcuts";
17
+ import type { CollectionSummary } from "@mulmoclaude/core/collection";
18
+
19
+ /** Wait for a burst of source-collection changes to settle (e.g. a feed
20
+ * refresh writing several records) before re-fetching, rather than
21
+ * re-fetching per record. */
22
+ const SOURCE_CHANGE_DEBOUNCE_MS = 300;
23
+
24
+ type Unsubscribe = () => void;
25
+
26
+ const summaries = ref<CollectionSummary[]>([]);
27
+ let subscriptions: Unsubscribe[] = [];
28
+ let debounceHandle: ReturnType<typeof setTimeout> | null = null;
29
+ let refreshSeq = 0;
30
+
31
+ /** Slugs of every source collection a dynamic-icon shortcut watches,
32
+ * deduped (two dynamic collections can share a source). */
33
+ function dynamicSourceSlugs(): string[] {
34
+ const slugs = new Set<string>();
35
+ for (const summary of summaries.value) {
36
+ for (const source of summary.iconSources ?? []) slugs.add(source);
37
+ }
38
+ return [...slugs];
39
+ }
40
+
41
+ function clearSubscriptions(): void {
42
+ subscriptions.forEach((unsubscribe) => unsubscribe());
43
+ subscriptions = [];
44
+ }
45
+
46
+ /** Re-derive the watched source-collection set from the current
47
+ * `summaries` and (re)subscribe — so a newly-added dynamic collection
48
+ * gets watched on the next refresh. A host without `subscribeChanges`
49
+ * (no pub/sub transport) leaves this a no-op; v1's reconcile-on-visit
50
+ * still applies. */
51
+ function resubscribe(): void {
52
+ clearSubscriptions();
53
+ const subscribe = collectionUi().subscribeChanges;
54
+ if (!subscribe) return;
55
+ subscriptions = dynamicSourceSlugs().map((slug) => subscribe(slug, scheduleRefresh));
56
+ }
57
+
58
+ function scheduleRefresh(): void {
59
+ if (debounceHandle) clearTimeout(debounceHandle);
60
+ debounceHandle = setTimeout(() => {
61
+ debounceHandle = null;
62
+ void refresh();
63
+ }, SOURCE_CHANGE_DEBOUNCE_MS);
64
+ }
65
+
66
+ /** Fetch the authoritative collections list, reconcile pinned
67
+ * `"collection"` shortcuts against it, and re-derive which source
68
+ * collections to watch. Feed-source entries are excluded — they're
69
+ * reconciled separately (kind `"feed"`) by `FeedsView`. A failed fetch
70
+ * leaves the prior summaries (and subscriptions) untouched. A monotonic
71
+ * `refreshSeq` drops a superseded fetch so a slow older `listCollections`
72
+ * can't resolve last and overwrite fresher summaries / resubscribe stale. */
73
+ async function refresh(): Promise<void> {
74
+ const seq = ++refreshSeq;
75
+ const result = await collectionUi().listCollections();
76
+ if (seq !== refreshSeq) return;
77
+ if (!result.ok) return;
78
+ summaries.value = result.data.collections.filter((summary) => summary.source !== "feed");
79
+ resubscribe();
80
+ await useShortcuts().reconcile(
81
+ "collection",
82
+ summaries.value.map((summary) => ({ slug: summary.slug, title: summary.title, icon: summary.icon })),
83
+ );
84
+ }
85
+
86
+ /** Mount once at app level. No template — side effects only. */
87
+ export function useDynamicShortcutIcons(): void {
88
+ onMounted(() => void refresh());
89
+ onUnmounted(() => {
90
+ if (debounceHandle) clearTimeout(debounceHandle);
91
+ clearSubscriptions();
92
+ });
93
+ }
@@ -0,0 +1,23 @@
1
+ // Composable: "show system files" toggle for the Files Explorer,
2
+ // persisted to localStorage. Default false — the Files pane hides
3
+ // agent-internal top-level dirs (`conversations/`, `feeds/`, etc.)
4
+ // until the user opts in. See #1896.
5
+
6
+ import { ref } from "vue";
7
+
8
+ const STORAGE_KEY = "filesView.showHiddenSystem";
9
+
10
+ function readStored(): boolean {
11
+ return localStorage.getItem(STORAGE_KEY) === "true";
12
+ }
13
+
14
+ export function useShowHiddenSystemFiles() {
15
+ const showHiddenSystem = ref<boolean>(readStored());
16
+
17
+ function setShowHiddenSystem(next: boolean): void {
18
+ showHiddenSystem.value = next;
19
+ localStorage.setItem(STORAGE_KEY, next ? "true" : "false");
20
+ }
21
+
22
+ return { showHiddenSystem, setShowHiddenSystem };
23
+ }
@@ -0,0 +1,21 @@
1
+ // Top-level workspace directories that the Files Explorer surfaces by
2
+ // default. Everything else at the root (`conversations/`, `feeds/`,
3
+ // `.git/`, `.github/`, ad-hoc automation buckets, …) is treated as
4
+ // "system" and stays hidden until the user flips the "show system
5
+ // files" toggle. See #1896.
6
+ //
7
+ // The whitelist covers the buckets MulmoClaude WRITES USER-FACING
8
+ // CONTENT into:
9
+ // - data/ — wiki, calendar, contacts, attachments, feed records, ...
10
+ // - artifacts/ — charts, documents, html, svg, images, spreadsheets, ...
11
+ // - config/ — settings.json, mcp.json, roles/, helps/, marp themes
12
+ // The filter is applied only at the root — once a whitelisted dir is
13
+ // shown, everything beneath it is visible without further filtering.
14
+ // That way a plugin that lands `data/foo-plugin/` shows up naturally
15
+ // without needing a code change here.
16
+
17
+ export const VISIBLE_TOP_LEVEL_DIRS: readonly string[] = ["data", "artifacts", "config"];
18
+
19
+ export function isVisibleTopLevel(name: string): boolean {
20
+ return VISIBLE_TOP_LEVEL_DIRS.includes(name);
21
+ }
package/src/lang/de.ts CHANGED
@@ -136,6 +136,9 @@ const deMessages = {
136
136
  disconnectFailed: "Trennen fehlgeschlagen",
137
137
  signInFailed: "Google-Anmeldung fehlgeschlagen",
138
138
  statusFailed: "Status konnte nicht geladen werden",
139
+ description: "Mit Remote-Zugriff kann ein Mobilgerät auf die Sammlungen und Feeds dieser MulmoClaude-Instanz zugreifen.",
140
+ howTo: "Öffne auf deinem Smartphone {url} und melde dich mit demselben Google-Konto an.",
141
+ customViewHint: "Für eine mobiltaugliche Ansicht bitte Claude, statt einer normalen Custom View eine {keyword} zu bauen.",
139
142
  },
140
143
  sidebarHeader: {
141
144
  home: "Zum neuesten Chat",
@@ -180,6 +183,9 @@ const deMessages = {
180
183
  // "RO" = Read-Only. Englische Abkürzung bleibt erhalten, um als
181
184
  // kompaktes Badge neben dem Label "Referenz" zu passen.
182
185
  readOnlyBadge: "RO",
186
+ showSystemFiles: "Systemdateien anzeigen",
187
+ showSystemFilesTitle:
188
+ "Zeigt agent-interne Top-Level-Verzeichnisse (conversations/, feeds/ usw.) zusätzlich zu den Nutzer-Daten (data/, artifacts/, config/) an.",
183
189
  },
184
190
  fileTree: {
185
191
  workspace: "(Arbeitsbereich)",
@@ -231,6 +237,8 @@ const deMessages = {
231
237
  photos: "Fotos",
232
238
  model: "Modell",
233
239
  voice: "Sprache",
240
+ chatIndex: "Chat-Index",
241
+ journal: "Journal",
234
242
  skills: "Skills",
235
243
  roles: "Rollen",
236
244
  },
@@ -293,6 +301,43 @@ const deMessages = {
293
301
  loadError: "Einstellungen konnten nicht geladen werden",
294
302
  saveError: "Speichern fehlgeschlagen",
295
303
  },
304
+ chatIndexTab: {
305
+ description:
306
+ "Automatische KI-Titel und Zusammenfassungen für den Chat-Verlauf. Standardmäßig aus — Automatisierungs-Sessions (Scheduler / System-Worker) werden auch bei aktivierter Option immer übersprungen; menschliche Sessions kosten pro beendetem Turn genau einen Summarizer-Aufruf.",
307
+ modeLabel: "Chat-Index-Modell",
308
+ helperText: "Haiku ist günstiger; Sonnet liefert schärfere Titel bei langen, themenwechselnden Sessions.",
309
+ mode: {
310
+ off: "Aus",
311
+ haiku: "Haiku",
312
+ sonnet: "Sonnet",
313
+ },
314
+ status: {
315
+ off: "Indizierung ist AUS",
316
+ haiku: "Indizierung läuft mit Haiku",
317
+ sonnet: "Indizierung läuft mit Sonnet",
318
+ },
319
+ loadError: "Einstellungen konnten nicht geladen werden",
320
+ saveError: "Speichern fehlgeschlagen",
321
+ },
322
+ journalTab: {
323
+ description:
324
+ "Automatisiertes Tages-Journal — fasst kürzliche Chat-Sessions in journal/*.md zusammen und extrahiert dauerhafte Memory-Notizen. Standardmäßig aus. Automatisierungs-Sessions (Scheduler / System-Worker) werden unabhängig von dieser Einstellung immer ausgeschlossen.",
325
+ modeLabel: "Journal-Modell",
326
+ helperText:
327
+ "Haiku ist günstiger; Sonnet liefert reichhaltigere Tages- und Themen-Zusammenfassungen. Der stündliche Lauf startet nur, wenn dies gesetzt ist.",
328
+ mode: {
329
+ off: "Aus",
330
+ haiku: "Haiku",
331
+ sonnet: "Sonnet",
332
+ },
333
+ status: {
334
+ off: "Journal ist AUS",
335
+ haiku: "Journal läuft mit Haiku",
336
+ sonnet: "Journal läuft mit Sonnet",
337
+ },
338
+ loadError: "Einstellungen konnten nicht geladen werden",
339
+ saveError: "Speichern fehlgeschlagen",
340
+ },
296
341
  // `<i18n-t>`-Slots — die Namen `envKey` / `envFile` werden in
297
342
  // SettingsModal.vue als Inline-`<code>` gerendert, sodass die
298
343
  // literalen Variablen- und Dateinamen unübersetzt bleiben.
package/src/lang/en.ts CHANGED
@@ -154,6 +154,9 @@ const enMessages = {
154
154
  disconnectFailed: "Disconnect failed",
155
155
  signInFailed: "Google sign-in failed",
156
156
  statusFailed: "Failed to load status",
157
+ description: "Remote access lets a mobile device connect to this MulmoClaude's collections and feeds.",
158
+ howTo: "On your phone, open {url} and sign in with the same Google account.",
159
+ customViewHint: "For a mobile-friendly view, ask Claude to build a {keyword} (not a regular custom view).",
157
160
  },
158
161
  sidebarHeader: {
159
162
  home: "Go to latest chat",
@@ -198,6 +201,8 @@ const enMessages = {
198
201
  // "RO" = Read-Only. Kept short on purpose — rendered as a compact
199
202
  // badge next to the Reference label.
200
203
  readOnlyBadge: "RO",
204
+ showSystemFiles: "Show system files",
205
+ showSystemFilesTitle: "Show agent-internal top-level dirs (conversations/, feeds/, etc.) in addition to your user content (data/, artifacts/, config/).",
201
206
  },
202
207
  fileTree: {
203
208
  workspace: "(workspace)",
@@ -249,6 +254,8 @@ const enMessages = {
249
254
  photos: "Photos",
250
255
  model: "Model",
251
256
  voice: "Voice",
257
+ chatIndex: "Chat index",
258
+ journal: "Journal",
252
259
  skills: "Skills",
253
260
  roles: "Roles",
254
261
  },
@@ -309,6 +316,42 @@ const enMessages = {
309
316
  loadError: "Failed to load settings",
310
317
  saveError: "Failed to save",
311
318
  },
319
+ chatIndexTab: {
320
+ description:
321
+ "Background AI titles + summaries for your chat history. Off by default — automation sessions (scheduler / system workers) are always skipped even when on, and human sessions only pay one summarizer call each when a turn ends.",
322
+ modeLabel: "Chat index model",
323
+ helperText: "Haiku is cheaper; Sonnet gives sharper titles for long, topic-shifting sessions.",
324
+ mode: {
325
+ off: "Off",
326
+ haiku: "Haiku",
327
+ sonnet: "Sonnet",
328
+ },
329
+ status: {
330
+ off: "Indexing is OFF",
331
+ haiku: "Indexing with Haiku",
332
+ sonnet: "Indexing with Sonnet",
333
+ },
334
+ loadError: "Failed to load settings",
335
+ saveError: "Failed to save",
336
+ },
337
+ journalTab: {
338
+ description:
339
+ "Automated daily journal — summarises recent chat sessions into journal/*.md and extracts durable memory notes. Off by default. Automation sessions (scheduler / system workers) are always excluded regardless of this setting.",
340
+ modeLabel: "Journal model",
341
+ helperText: "Haiku is cheaper; Sonnet produces richer daily / topic summaries. The hourly pass runs only when this is set.",
342
+ mode: {
343
+ off: "Off",
344
+ haiku: "Haiku",
345
+ sonnet: "Sonnet",
346
+ },
347
+ status: {
348
+ off: "Journal is OFF",
349
+ haiku: "Journal running with Haiku",
350
+ sonnet: "Journal running with Sonnet",
351
+ },
352
+ loadError: "Failed to load settings",
353
+ saveError: "Failed to save",
354
+ },
312
355
  // `<i18n-t>` slots — named `envKey` / `envFile` render as inline
313
356
  // `<code>` in SettingsModal.vue, so the literal variable and file
314
357
  // names stay untranslated while the surrounding copy is localised.
package/src/lang/es.ts CHANGED
@@ -139,6 +139,9 @@ const esMessages = {
139
139
  disconnectFailed: "Error al desconectar",
140
140
  signInFailed: "Error al iniciar sesión con Google",
141
141
  statusFailed: "Error al cargar el estado",
142
+ description: "El acceso remoto permite que un dispositivo móvil se conecte a las colecciones y feeds de este MulmoClaude.",
143
+ howTo: "En tu teléfono, abre {url} e inicia sesión con la misma cuenta de Google.",
144
+ customViewHint: "Para una vista compatible con móviles, pide a Claude que cree una {keyword} (no una custom view normal).",
142
145
  },
143
146
  sidebarHeader: {
144
147
  home: "Ir al chat más reciente",
@@ -183,6 +186,9 @@ const esMessages = {
183
186
  // "RO" = Read-Only. Se mantiene la abreviatura en inglés para
184
187
  // renderizarse como una insignia compacta junto al label Referencia.
185
188
  readOnlyBadge: "RO",
189
+ showSystemFiles: "Mostrar archivos del sistema",
190
+ showSystemFilesTitle:
191
+ "Muestra los directorios raíz internos del agente (conversations/, feeds/, etc.) además del contenido del usuario (data/, artifacts/, config/).",
186
192
  },
187
193
  fileTree: {
188
194
  workspace: "(área de trabajo)",
@@ -234,6 +240,8 @@ const esMessages = {
234
240
  photos: "Fotos",
235
241
  model: "Modelo",
236
242
  voice: "Voz",
243
+ chatIndex: "Índice de chat",
244
+ journal: "Diario",
237
245
  skills: "Skills",
238
246
  roles: "Roles",
239
247
  },
@@ -294,6 +302,42 @@ const esMessages = {
294
302
  loadError: "Error al cargar los ajustes",
295
303
  saveError: "Error al guardar",
296
304
  },
305
+ chatIndexTab: {
306
+ description:
307
+ "Títulos y resúmenes automáticos generados por IA para tu historial de chat. Sale desactivado por defecto — las sesiones de automatización (scheduler / trabajadores del sistema) siempre se omiten aunque esté activado; las sesiones humanas solo pagan una llamada al resumidor al terminar cada turno.",
308
+ modeLabel: "Modelo del índice de chat",
309
+ helperText: "Haiku es más económico; Sonnet ofrece títulos más precisos en sesiones largas con cambios de tema.",
310
+ mode: {
311
+ off: "Desactivado",
312
+ haiku: "Haiku",
313
+ sonnet: "Sonnet",
314
+ },
315
+ status: {
316
+ off: "La indexación está DESACTIVADA",
317
+ haiku: "Indexando con Haiku",
318
+ sonnet: "Indexando con Sonnet",
319
+ },
320
+ loadError: "Error al cargar los ajustes",
321
+ saveError: "Error al guardar",
322
+ },
323
+ journalTab: {
324
+ description:
325
+ "Diario automático — resume las sesiones de chat recientes en journal/*.md y extrae notas de memoria duradera. Sale desactivado. Las sesiones de automatización (scheduler / trabajadores del sistema) se excluyen siempre, independientemente de esta configuración.",
326
+ modeLabel: "Modelo del diario",
327
+ helperText: "Haiku es más económico; Sonnet produce resúmenes diarios y por tema más ricos. El pase horario solo se ejecuta cuando esto está activado.",
328
+ mode: {
329
+ off: "Desactivado",
330
+ haiku: "Haiku",
331
+ sonnet: "Sonnet",
332
+ },
333
+ status: {
334
+ off: "El diario está DESACTIVADO",
335
+ haiku: "Diario en ejecución con Haiku",
336
+ sonnet: "Diario en ejecución con Sonnet",
337
+ },
338
+ loadError: "Error al cargar los ajustes",
339
+ saveError: "Error al guardar",
340
+ },
297
341
  // Slots `<i18n-t>` — los nombres `envKey` / `envFile` se renderizan
298
342
  // como `<code>` en línea en SettingsModal.vue, por lo que los
299
343
  // literales de variable y nombre de archivo se mantienen sin