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.
- package/client/assets/{index-40ErrJ4a.js → index-B3NxFcEH.js} +36 -36
- package/client/assets/{marp-Dh7C24F1.js → marp-C9QDHFAJ.js} +1 -1
- package/client/index.html +1 -1
- package/package.json +6 -6
- package/server/api/routes/agent.ts +5 -1
- package/server/api/routes/collections.ts +16 -1
- package/server/api/routes/config.ts +12 -0
- package/server/remoteHost/firebase.ts +6 -0
- package/server/remoteHost/handlers/index.ts +4 -0
- package/server/remoteHost/handlers/ingestAttachments.ts +88 -0
- package/server/remoteHost/handlers/listAccountingBooks.ts +31 -0
- package/server/remoteHost/handlers/listCollections.ts +6 -1
- package/server/remoteHost/handlers/listSkills.ts +39 -0
- package/server/remoteHost/handlers/startChat.ts +103 -39
- package/server/system/config.ts +101 -4
- package/server/system/logs/aaa +737 -0
- package/server/system/logs/bb +446 -0
- package/server/workspace/chat-index/index.ts +21 -2
- package/server/workspace/chat-index/indexer.ts +57 -14
- package/server/workspace/chat-index/summarizer.ts +18 -10
- package/server/workspace/collections/index.ts +1 -1
- package/server/workspace/journal/archivist-cli.ts +17 -3
- package/server/workspace/journal/dailyPass.ts +52 -2
- package/server/workspace/journal/index.ts +20 -4
- package/src/App.vue +5 -0
- package/src/components/FileTree.vue +21 -1
- package/src/components/FileTreePane.vue +41 -25
- package/src/components/FilesView.vue +4 -0
- package/src/components/RemoteHostControl.vue +18 -0
- package/src/components/SessionHistoryPanel.vue +24 -11
- package/src/components/SettingsChatIndexTab.vue +119 -0
- package/src/components/SettingsJournalTab.vue +117 -0
- package/src/components/SettingsModal.vue +12 -2
- package/src/composables/collections/useDynamicShortcutIcons.ts +93 -0
- package/src/composables/useShowHiddenSystemFiles.ts +23 -0
- package/src/config/visibleWorkspaceDirs.ts +21 -0
- package/src/lang/de.ts +45 -0
- package/src/lang/en.ts +43 -0
- package/src/lang/es.ts +44 -0
- package/src/lang/fr.ts +45 -0
- package/src/lang/ja.ts +44 -0
- package/src/lang/ko.ts +43 -0
- package/src/lang/pt-BR.ts +44 -0
- package/src/lang/zh.ts +43 -0
- 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
|