mulmoclaude 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mulmoclaude.js +1 -1
- package/client/assets/{index-KNLBjwuh.css → index-Bm70FDU2.css} +1 -1
- package/client/assets/{index-D8rhwXLq.js → index-eHWB79u5.js} +3 -3
- package/client/index.html +2 -2
- package/package.json +1 -1
- package/server/agent/config.ts +12 -12
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/x.ts +5 -5
- package/server/agent/prompt.ts +9 -4
- package/server/agent/sandboxMounts.ts +7 -7
- package/server/agent/stream.ts +4 -4
- package/server/api/routes/files.ts +9 -9
- package/server/api/routes/scheduler.ts +8 -8
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +14 -14
- package/server/api/routes/sessions.ts +24 -24
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/wiki.ts +14 -14
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +19 -19
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +9 -9
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +2 -2
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/roles-io.ts +10 -10
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +2 -2
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/custom-dirs.ts +11 -11
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/reference-dirs.ts +18 -18
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/sources/arxivDiscovery.ts +8 -8
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/interests.ts +9 -9
- package/server/workspace/sources/pipeline/index.ts +6 -6
- package/server/workspace/sources/pipeline/plan.ts +5 -5
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +11 -9
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/src/App.vue +30 -30
- package/src/components/ChatInput.vue +7 -7
- package/src/components/LockStatusPopup.vue +2 -2
- package/src/components/NotificationToast.vue +2 -2
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +6 -6
- package/src/components/SettingsMcpTab.vue +7 -7
- package/src/components/SettingsModal.vue +3 -3
- package/src/components/SettingsReferenceDirsTab.vue +10 -10
- package/src/components/SettingsWorkspaceDirsTab.vue +5 -5
- package/src/components/SuggestionsPanel.vue +2 -2
- package/src/components/todo/TodoAddDialog.vue +2 -2
- package/src/components/todo/TodoEditPanel.vue +2 -2
- package/src/components/todo/TodoListView.vue +5 -5
- package/src/composables/useCanvasViewMode.ts +5 -5
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +2 -2
- package/src/config/roles.ts +2 -2
- package/src/plugins/chart/Preview.vue +4 -4
- package/src/plugins/manageSkills/View.vue +3 -3
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +2 -2
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/View.vue +4 -4
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +6 -6
- package/src/plugins/scheduler/TasksTab.vue +4 -4
- package/src/plugins/textResponse/View.vue +2 -2
- package/src/plugins/todo/Preview.vue +2 -2
- package/src/plugins/todo/View.vue +11 -11
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/wiki/Preview.vue +5 -5
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/router/guards.ts +12 -12
- package/src/types/session.ts +4 -3
- package/src/utils/agent/request.ts +3 -3
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
|
@@ -73,8 +73,8 @@ function extractSessionIdsFromAppendix(appendix: string): Set<string> {
|
|
|
73
73
|
if (!line.startsWith("- ") && !line.startsWith("* ")) continue;
|
|
74
74
|
const href = extractHrefFromBullet(line);
|
|
75
75
|
if (!href) continue;
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
76
|
+
const sessionId = extractSessionIdFromHref(href);
|
|
77
|
+
if (sessionId) ids.add(sessionId);
|
|
78
78
|
}
|
|
79
79
|
return ids;
|
|
80
80
|
}
|
|
@@ -105,25 +105,25 @@ function extractSessionIdFromHref(href: string): string | null {
|
|
|
105
105
|
if (lastSlash === -1) return null;
|
|
106
106
|
const parentSegment = findPrecedingSegment(cleanHref, lastSlash);
|
|
107
107
|
if (parentSegment !== "chat") return null;
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
108
|
+
const sessionId = cleanHref.slice(lastSlash + 1, cleanHref.length - JSONL_SUFFIX.length);
|
|
109
|
+
if (sessionId.length === 0 || sessionId.includes("/")) return null;
|
|
110
|
+
return sessionId;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function stripFragmentAndQuery(
|
|
114
|
-
let end =
|
|
115
|
-
const hash =
|
|
113
|
+
function stripFragmentAndQuery(href: string): string {
|
|
114
|
+
let end = href.length;
|
|
115
|
+
const hash = href.indexOf("#");
|
|
116
116
|
if (hash !== -1) end = hash;
|
|
117
|
-
const query =
|
|
117
|
+
const query = href.indexOf("?");
|
|
118
118
|
if (query !== -1 && query < end) end = query;
|
|
119
|
-
return
|
|
119
|
+
return href.slice(0, end);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Given `.../chat/abc.jsonl` and the index of the last `/`, return
|
|
123
123
|
// the segment immediately before the filename (here: `"chat"`).
|
|
124
|
-
function findPrecedingSegment(
|
|
125
|
-
const prevSlash =
|
|
126
|
-
return
|
|
124
|
+
function findPrecedingSegment(href: string, lastSlash: number): string {
|
|
125
|
+
const prevSlash = href.lastIndexOf("/", lastSlash - 1);
|
|
126
|
+
return href.slice(prevSlash + 1, lastSlash);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Insert `newBullet` at the end of the History list inside the
|
|
@@ -135,9 +135,9 @@ function appendBulletToAppendix(appendix: string, newBullet: string): string {
|
|
|
135
135
|
// Hand-rolled rtrim so sonarjs/slow-regex stays quiet.
|
|
136
136
|
let end = appendix.length;
|
|
137
137
|
while (end > 0) {
|
|
138
|
-
const
|
|
138
|
+
const charCode = appendix.charCodeAt(end - 1);
|
|
139
139
|
// Whitespace codepoints we expect here: \n, \r, \t, space.
|
|
140
|
-
if (
|
|
140
|
+
if (charCode !== 10 && charCode !== 13 && charCode !== 9 && charCode !== 32) break;
|
|
141
141
|
end--;
|
|
142
142
|
}
|
|
143
143
|
return `${appendix.slice(0, end)}\n${newBullet}\n`;
|
package/src/App.vue
CHANGED
|
@@ -253,17 +253,17 @@ const router = useRouter();
|
|
|
253
253
|
|
|
254
254
|
// Omit ?role= for the default role to keep URLs clean.
|
|
255
255
|
function buildRoleQuery(): Record<string, string> {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
258
|
-
return { role:
|
|
256
|
+
const roleId = currentRoleId.value;
|
|
257
|
+
if (!roleId || roles.value.length === 0 || roleId === roles.value[0]?.id) return {};
|
|
258
|
+
return { role: roleId };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
function navigateToSession(
|
|
262
|
-
currentSessionId.value =
|
|
261
|
+
function navigateToSession(sessionId: string, replace = false): void {
|
|
262
|
+
currentSessionId.value = sessionId;
|
|
263
263
|
const method = replace ? router.replace : router.push;
|
|
264
264
|
method({
|
|
265
265
|
name: "chat",
|
|
266
|
-
params: { sessionId
|
|
266
|
+
params: { sessionId },
|
|
267
267
|
query: { ...buildViewQuery(), ...buildRoleQuery() },
|
|
268
268
|
}).catch((err) => {
|
|
269
269
|
if (err?.type !== 16) {
|
|
@@ -382,9 +382,9 @@ const { isStackLayout, restoreChatViewForSession, displayedCurrentSessionId } =
|
|
|
382
382
|
// Not wired into the internal `loadSession` call path because that
|
|
383
383
|
// also fires on initial mount with `?view=plugin` URLs, which must
|
|
384
384
|
// be honoured as-is.
|
|
385
|
-
function handleSessionSelect(
|
|
385
|
+
function handleSessionSelect(sessionId: string): void {
|
|
386
386
|
restoreChatViewForSession();
|
|
387
|
-
loadSession(
|
|
387
|
+
loadSession(sessionId);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
function handleNewSessionClick(): void {
|
|
@@ -425,8 +425,8 @@ const { mergedSessions, tabSessions } = useMergedSessions({
|
|
|
425
425
|
// when switching away (running sessions keep their subscription so they
|
|
426
426
|
// continue receiving events — session_finished will clean them up).
|
|
427
427
|
let previousSessionId: string | null = null;
|
|
428
|
-
watch(currentSessionId, (
|
|
429
|
-
const session = sessionMap.get(
|
|
428
|
+
watch(currentSessionId, (sessionId) => {
|
|
429
|
+
const session = sessionMap.get(sessionId);
|
|
430
430
|
// Subscribe to the new session's channel
|
|
431
431
|
if (session) {
|
|
432
432
|
ensureSessionSubscription(session);
|
|
@@ -435,23 +435,23 @@ watch(currentSessionId, (id) => {
|
|
|
435
435
|
// no in-flight background generations. Tearing down the subscription
|
|
436
436
|
// while a generation is still running would orphan its completion
|
|
437
437
|
// event, leaving the session's busy indicator stuck on.
|
|
438
|
-
if (previousSessionId && previousSessionId !==
|
|
438
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
439
439
|
const prevSession = sessionMap.get(previousSessionId);
|
|
440
440
|
const prevBusy = !!prevSession && (prevSession.isRunning || Object.keys(prevSession.pendingGenerations ?? {}).length > 0);
|
|
441
441
|
if (prevSession && !prevBusy) {
|
|
442
442
|
unsubscribeSession(previousSessionId);
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
|
-
previousSessionId =
|
|
445
|
+
previousSessionId = sessionId;
|
|
446
446
|
|
|
447
447
|
// Clear unread in both sessionMap and sessions list (for badge count),
|
|
448
448
|
// then tell the server so other tabs see it too.
|
|
449
|
-
const summary = sessions.value.find((entry) => entry.id ===
|
|
449
|
+
const summary = sessions.value.find((entry) => entry.id === sessionId);
|
|
450
450
|
const wasUnread = (session && session.hasUnread) || (summary && summary.hasUnread);
|
|
451
451
|
if (wasUnread) {
|
|
452
452
|
if (session) session.hasUnread = false;
|
|
453
453
|
if (summary) summary.hasUnread = false;
|
|
454
|
-
markSessionRead(
|
|
454
|
+
markSessionRead(sessionId);
|
|
455
455
|
}
|
|
456
456
|
});
|
|
457
457
|
|
|
@@ -484,11 +484,11 @@ const needsGeminiForRole = (roleId: string) => needsGemini(roles.value, roleId);
|
|
|
484
484
|
// router.replace instead of router.push to keep the empty session out
|
|
485
485
|
// of browser navigation history.
|
|
486
486
|
function removeCurrentIfEmpty(): boolean {
|
|
487
|
-
const
|
|
488
|
-
if (!
|
|
489
|
-
const session = sessionMap.get(
|
|
487
|
+
const sessionId = currentSessionId.value;
|
|
488
|
+
if (!sessionId) return false;
|
|
489
|
+
const session = sessionMap.get(sessionId);
|
|
490
490
|
if (session && session.toolResults.length === 0) {
|
|
491
|
-
sessionMap.delete(
|
|
491
|
+
sessionMap.delete(sessionId);
|
|
492
492
|
return true;
|
|
493
493
|
}
|
|
494
494
|
return false;
|
|
@@ -515,40 +515,40 @@ function onRoleChange() {
|
|
|
515
515
|
maybeSeedRoleDefault(session);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
function activateSession(
|
|
519
|
-
const reactiveSession = sessionMap.get(
|
|
518
|
+
function activateSession(sessionId: string, roleId: string, replace: boolean): void {
|
|
519
|
+
const reactiveSession = sessionMap.get(sessionId);
|
|
520
520
|
if (reactiveSession) ensureSessionSubscription(reactiveSession);
|
|
521
521
|
// Set role before navigating: buildRoleQuery() reads currentRoleId to
|
|
522
522
|
// build ?role=, and the route.query.role watcher would otherwise fire
|
|
523
523
|
// after navigation and revert currentRoleId to the previous session's role.
|
|
524
524
|
currentRoleId.value = roleId;
|
|
525
|
-
navigateToSession(
|
|
525
|
+
navigateToSession(sessionId, replace);
|
|
526
526
|
showHistory.value = false;
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
async function loadSession(
|
|
530
|
-
if (
|
|
529
|
+
async function loadSession(sessionId: string) {
|
|
530
|
+
if (sessionId === currentSessionId.value && sessionMap.has(sessionId)) return;
|
|
531
531
|
const replaced = removeCurrentIfEmpty();
|
|
532
532
|
|
|
533
|
-
const live = sessionMap.get(
|
|
533
|
+
const live = sessionMap.get(sessionId);
|
|
534
534
|
if (live) {
|
|
535
|
-
activateSession(
|
|
535
|
+
activateSession(sessionId, live.roleId, replaced);
|
|
536
536
|
return;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(
|
|
539
|
+
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(sessionId)));
|
|
540
540
|
if (!response.ok) return;
|
|
541
541
|
|
|
542
542
|
const newSession = buildLoadedSession({
|
|
543
|
-
id,
|
|
543
|
+
id: sessionId,
|
|
544
544
|
entries: response.data,
|
|
545
545
|
defaultRoleId: currentRoleId.value,
|
|
546
546
|
urlResult: typeof route.query.result === "string" ? route.query.result : null,
|
|
547
|
-
serverSummary: sessions.value.find((
|
|
547
|
+
serverSummary: sessions.value.find((summary) => summary.id === sessionId),
|
|
548
548
|
nowIso: new Date().toISOString(),
|
|
549
549
|
});
|
|
550
|
-
sessionMap.set(
|
|
551
|
-
activateSession(
|
|
550
|
+
sessionMap.set(sessionId, newSession);
|
|
551
|
+
activateSession(sessionId, newSession.roleId, replaced);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// Re-fetch the transcript from the server and patch any entries the
|
|
@@ -130,7 +130,7 @@ const ACCEPTED_MIME_EXACT = new Set([
|
|
|
130
130
|
]);
|
|
131
131
|
|
|
132
132
|
function isAcceptedType(mime: string): boolean {
|
|
133
|
-
return ACCEPTED_MIME_PREFIXES.some((
|
|
133
|
+
return ACCEPTED_MIME_PREFIXES.some((prefix) => mime.startsWith(prefix)) || ACCEPTED_MIME_EXACT.has(mime);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function readAttachmentFile(file: File): void {
|
|
@@ -154,14 +154,14 @@ function readAttachmentFile(file: File): void {
|
|
|
154
154
|
reader.readAsDataURL(file);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function onPasteFile(
|
|
158
|
-
const items =
|
|
157
|
+
function onPasteFile(event: ClipboardEvent): void {
|
|
158
|
+
const items = event.clipboardData?.items;
|
|
159
159
|
if (!items) return;
|
|
160
160
|
for (const item of items) {
|
|
161
161
|
if (isAcceptedType(item.type)) {
|
|
162
162
|
const file = item.getAsFile();
|
|
163
163
|
if (file) {
|
|
164
|
-
|
|
164
|
+
event.preventDefault();
|
|
165
165
|
readAttachmentFile(file);
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
@@ -169,9 +169,9 @@ function onPasteFile(e: ClipboardEvent): void {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
function onDropFile(
|
|
173
|
-
|
|
174
|
-
const file =
|
|
172
|
+
function onDropFile(event: DragEvent): void {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
const file = event.dataTransfer?.files[0];
|
|
175
175
|
if (file) readAttachmentFile(file);
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -28,8 +28,8 @@ function dismiss(): void {
|
|
|
28
28
|
visible.value = null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function iconName(
|
|
32
|
-
return
|
|
31
|
+
function iconName(notif: NotificationPayload): string {
|
|
32
|
+
return notif.icon ?? NOTIFICATION_ICONS[notif.kind] ?? "notifications";
|
|
33
33
|
}
|
|
34
34
|
</script>
|
|
35
35
|
|
|
@@ -47,8 +47,8 @@ const dropdown = ref<HTMLDivElement | null>(null);
|
|
|
47
47
|
|
|
48
48
|
const currentRoleName = computed(() => roleName(props.roles, props.currentRoleId));
|
|
49
49
|
|
|
50
|
-
function selectRole(
|
|
51
|
-
emit("update:currentRoleId",
|
|
50
|
+
function selectRole(roleId: string): void {
|
|
51
|
+
emit("update:currentRoleId", roleId);
|
|
52
52
|
open.value = false;
|
|
53
53
|
emit("change");
|
|
54
54
|
}
|
|
@@ -128,12 +128,12 @@ function originOf(session: SessionSummary): SessionOrigin {
|
|
|
128
128
|
|
|
129
129
|
const filteredSessions = computed(() => {
|
|
130
130
|
if (activeFilter.value === "all") return props.sessions;
|
|
131
|
-
return props.sessions.filter((
|
|
131
|
+
return props.sessions.filter((session) => originOf(session) === activeFilter.value);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
function countByOrigin(origin: string): number {
|
|
135
135
|
if (origin === "all") return props.sessions.length;
|
|
136
|
-
return props.sessions.filter((
|
|
136
|
+
return props.sessions.filter((session) => originOf(session) === origin).length;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function originIcon(origin: string): string {
|
|
@@ -146,11 +146,11 @@ function originColor(origin: string): string {
|
|
|
146
146
|
|
|
147
147
|
// ── Role helpers ────────────────────────────────────────────
|
|
148
148
|
|
|
149
|
-
function roleIconFor(
|
|
150
|
-
return roleIcon(props.roles,
|
|
149
|
+
function roleIconFor(roleId: string): string {
|
|
150
|
+
return roleIcon(props.roles, roleId);
|
|
151
151
|
}
|
|
152
|
-
function roleNameFor(
|
|
153
|
-
return roleName(props.roles,
|
|
152
|
+
function roleNameFor(roleId: string): string {
|
|
153
|
+
return roleName(props.roles, roleId);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function isSessionRunning(session: SessionSummary): boolean {
|
|
@@ -265,21 +265,21 @@ function ensureUniqueId(base: string): string {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
function commitAdd(): void {
|
|
268
|
-
let
|
|
269
|
-
if (!
|
|
268
|
+
let mcpId = draft.value.id.trim();
|
|
269
|
+
if (!mcpId) {
|
|
270
270
|
const suggested = ensureUniqueId(suggestIdFromDraft(draft.value));
|
|
271
271
|
if (!suggested) {
|
|
272
272
|
draftError.value = "Please provide a Name, or enter a URL / args we can derive one from.";
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
|
-
|
|
275
|
+
mcpId = suggested;
|
|
276
276
|
}
|
|
277
|
-
if (!ID_RE.test(
|
|
277
|
+
if (!ID_RE.test(mcpId)) {
|
|
278
278
|
draftError.value = "Name must start with a lowercase letter and contain only [a-z0-9_-].";
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
if (props.servers.some((server) => server.id ===
|
|
282
|
-
draftError.value = `Server id "${
|
|
281
|
+
if (props.servers.some((server) => server.id === mcpId)) {
|
|
282
|
+
draftError.value = `Server id "${mcpId}" already exists.`;
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
let spec: ServerSpec;
|
|
@@ -302,7 +302,7 @@ function commitAdd(): void {
|
|
|
302
302
|
enabled: true,
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
|
-
emit("add", { id, spec });
|
|
305
|
+
emit("add", { id: mcpId, spec });
|
|
306
306
|
adding.value = false;
|
|
307
307
|
draftError.value = "";
|
|
308
308
|
}
|
|
@@ -177,11 +177,11 @@ let loadToken = 0;
|
|
|
177
177
|
const parsedToolNames = computed(() =>
|
|
178
178
|
toolsText.value
|
|
179
179
|
.split("\n")
|
|
180
|
-
.map((
|
|
181
|
-
.filter((
|
|
180
|
+
.map((line) => line.trim())
|
|
181
|
+
.filter((line) => line.length > 0),
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
-
const invalidToolNames = computed(() => parsedToolNames.value.filter((
|
|
184
|
+
const invalidToolNames = computed(() => parsedToolNames.value.filter((name) => !name.startsWith("mcp__") && !isBuiltIn(name)));
|
|
185
185
|
|
|
186
186
|
function isBuiltIn(name: string): boolean {
|
|
187
187
|
return ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"].includes(name);
|
|
@@ -48,33 +48,33 @@ async function save(): Promise<void> {
|
|
|
48
48
|
|
|
49
49
|
function addEntry(): void {
|
|
50
50
|
draftError.value = "";
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
51
|
+
const path = draftPath.value.trim();
|
|
52
|
+
if (!path) {
|
|
53
53
|
draftError.value = "Path required";
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
if (!
|
|
56
|
+
if (!path.startsWith("/") && !path.startsWith("~/")) {
|
|
57
57
|
draftError.value = "Must be an absolute path or start with ~/";
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
// Normalize: trim trailing slashes for consistent comparison
|
|
61
|
-
let normalized =
|
|
61
|
+
let normalized = path;
|
|
62
62
|
while (normalized.length > 1 && normalized.endsWith("/")) {
|
|
63
63
|
normalized = normalized.slice(0, -1);
|
|
64
64
|
}
|
|
65
|
-
const stripSlash = (
|
|
66
|
-
let
|
|
67
|
-
while (
|
|
68
|
-
return
|
|
65
|
+
const stripSlash = (str: string): string => {
|
|
66
|
+
let cleaned = str;
|
|
67
|
+
while (cleaned.length > 1 && cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
|
|
68
|
+
return cleaned;
|
|
69
69
|
};
|
|
70
|
-
if (dirs.value.some((
|
|
70
|
+
if (dirs.value.some((dir) => stripSlash(dir.hostPath) === normalized)) {
|
|
71
71
|
draftError.value = "Already exists";
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
const lastSeg = normalized.split("/").pop();
|
|
75
75
|
const label = draftLabel.value.trim() || lastSeg || normalized;
|
|
76
76
|
// Reject duplicate labels — @ref/<label> routing requires uniqueness
|
|
77
|
-
if (dirs.value.some((
|
|
77
|
+
if (dirs.value.some((dir) => dir.label === label)) {
|
|
78
78
|
draftError.value = `Label "${label}" already exists`;
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
@@ -51,21 +51,21 @@ async function save(): Promise<void> {
|
|
|
51
51
|
|
|
52
52
|
function addEntry(): void {
|
|
53
53
|
draftError.value = "";
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
54
|
+
const path = draftPath.value.trim();
|
|
55
|
+
if (!path) {
|
|
56
56
|
draftError.value = "Path required";
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
if (!
|
|
59
|
+
if (!path.startsWith("data/") && !path.startsWith("artifacts/")) {
|
|
60
60
|
draftError.value = "Must start with data/ or artifacts/";
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
if (dirs.value.some((
|
|
63
|
+
if (dirs.value.some((dir) => dir.path === path)) {
|
|
64
64
|
draftError.value = "Already exists";
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
dirs.value.push({
|
|
68
|
-
path
|
|
68
|
+
path,
|
|
69
69
|
description: draftDescription.value.trim(),
|
|
70
70
|
structure: draftStructure.value,
|
|
71
71
|
});
|
|
@@ -48,9 +48,9 @@ watch(expanded, (isExpanded) => {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
function onClick(
|
|
51
|
+
function onClick(event: MouseEvent, query: string): void {
|
|
52
52
|
expanded.value = false;
|
|
53
|
-
if (
|
|
53
|
+
if (event.shiftKey) {
|
|
54
54
|
emit("edit", query);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
@@ -123,8 +123,8 @@ function submit(): void {
|
|
|
123
123
|
if (dueDate.value !== "") input.dueDate = dueDate.value;
|
|
124
124
|
const labels = labelsText.value
|
|
125
125
|
.split(",")
|
|
126
|
-
.map((
|
|
127
|
-
.filter((
|
|
126
|
+
.map((item) => item.trim())
|
|
127
|
+
.filter((item) => item.length > 0);
|
|
128
128
|
if (labels.length > 0) input.labels = labels;
|
|
129
129
|
emit("create", input);
|
|
130
130
|
}
|
|
@@ -85,8 +85,8 @@ const labelsText = ref((props.item.labels ?? []).join(", "));
|
|
|
85
85
|
function parseLabels(raw: string): string[] {
|
|
86
86
|
return raw
|
|
87
87
|
.split(",")
|
|
88
|
-
.map((
|
|
89
|
-
.filter((
|
|
88
|
+
.map((item) => item.trim())
|
|
89
|
+
.filter((item) => item.length > 0);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function save(): void {
|
|
@@ -58,22 +58,22 @@ const emit = defineEmits<{
|
|
|
58
58
|
|
|
59
59
|
const expandedId = ref<string | null>(null);
|
|
60
60
|
|
|
61
|
-
function toggleExpand(
|
|
62
|
-
expandedId.value = expandedId.value ===
|
|
61
|
+
function toggleExpand(itemId: string): void {
|
|
62
|
+
expandedId.value = expandedId.value === itemId ? null : itemId;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function toggleComplete(item: TodoItem): void {
|
|
66
66
|
emit("toggleComplete", item);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function onSave(
|
|
70
|
-
emit("patch",
|
|
69
|
+
function onSave(itemId: string, input: PatchItemInput): void {
|
|
70
|
+
emit("patch", itemId, input);
|
|
71
71
|
expandedId.value = null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function statusLabel(item: TodoItem): string {
|
|
75
75
|
if (!item.status) return "";
|
|
76
|
-
const col = props.columns.find((
|
|
76
|
+
const col = props.columns.find((column) => column.id === item.status);
|
|
77
77
|
return col?.label ?? "";
|
|
78
78
|
}
|
|
79
79
|
</script>
|
|
@@ -94,13 +94,13 @@ export function useCanvasViewMode(opts: UseCanvasViewModeOptions): {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
function handleViewModeShortcut(
|
|
98
|
-
if (!(
|
|
99
|
-
if (
|
|
100
|
-
const target = viewModeForShortcutKey(
|
|
97
|
+
function handleViewModeShortcut(event: KeyboardEvent): void {
|
|
98
|
+
if (!(event.metaKey || event.ctrlKey)) return;
|
|
99
|
+
if (event.altKey || event.shiftKey) return;
|
|
100
|
+
const target = viewModeForShortcutKey(event.key);
|
|
101
101
|
if (target === null) return;
|
|
102
102
|
setCanvasViewMode(target);
|
|
103
|
-
|
|
103
|
+
event.preventDefault();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** Plugin-launcher click: switch canvas to the matching view mode. */
|
|
@@ -16,9 +16,9 @@ interface UseClickOutsideOptions {
|
|
|
16
16
|
export function useClickOutside(opts: UseClickOutsideOptions): {
|
|
17
17
|
handler: (e: MouseEvent) => void;
|
|
18
18
|
} {
|
|
19
|
-
function handler(
|
|
19
|
+
function handler(event: MouseEvent): void {
|
|
20
20
|
if (!opts.isOpen.value) return;
|
|
21
|
-
if (isClickOutside(
|
|
21
|
+
if (isClickOutside(event.target as Node | null, opts.buttonRef.value, opts.popupRef.value)) {
|
|
22
22
|
opts.isOpen.value = false;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -70,9 +70,9 @@ export function useFreshPluginData<T>(opts: UseFreshPluginDataOptions<T>): UseFr
|
|
|
70
70
|
|
|
71
71
|
async function refresh(): Promise<boolean> {
|
|
72
72
|
controller?.abort();
|
|
73
|
-
const
|
|
74
|
-
controller =
|
|
75
|
-
return refreshOnce(opts,
|
|
73
|
+
const ctrl = new AbortController();
|
|
74
|
+
controller = ctrl;
|
|
75
|
+
return refreshOnce(opts, ctrl.signal);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function abort(): void {
|
|
@@ -18,7 +18,7 @@ function isVerticalArrow(key: string): key is "ArrowUp" | "ArrowDown" {
|
|
|
18
18
|
|
|
19
19
|
function resolveNextUuid(results: ToolResultComplete[], currentUuid: string | null, direction: "ArrowUp" | "ArrowDown"): string | null {
|
|
20
20
|
if (results.length === 0) return null;
|
|
21
|
-
const idx = results.findIndex((
|
|
21
|
+
const idx = results.findIndex((result) => result.uuid === currentUuid);
|
|
22
22
|
if (idx === -1) {
|
|
23
23
|
return direction === "ArrowDown" ? results[0].uuid : results[results.length - 1].uuid;
|
|
24
24
|
}
|
|
@@ -36,23 +36,23 @@ export function useKeyNavigation(opts: {
|
|
|
36
36
|
}) {
|
|
37
37
|
const { canvasRef, activePane, sidebarResults, selectedResultUuid } = opts;
|
|
38
38
|
|
|
39
|
-
function handleCanvasKeydown(
|
|
40
|
-
if (!isVerticalArrow(
|
|
41
|
-
if (isEditableTarget(
|
|
39
|
+
function handleCanvasKeydown(event: KeyboardEvent): void {
|
|
40
|
+
if (!isVerticalArrow(event.key)) return;
|
|
41
|
+
if (isEditableTarget(event.target)) return;
|
|
42
42
|
if (!canvasRef.value) return;
|
|
43
43
|
const scrollable = findScrollableChild(canvasRef.value);
|
|
44
44
|
if (!scrollable) return;
|
|
45
|
-
|
|
46
|
-
const delta =
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
const delta = event.key === "ArrowDown" ? SCROLL_AMOUNT : -SCROLL_AMOUNT;
|
|
47
47
|
scrollable.scrollBy({ top: delta, behavior: "smooth" });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function handleKeyNavigation(
|
|
50
|
+
function handleKeyNavigation(event: KeyboardEvent): void {
|
|
51
51
|
if (activePane.value !== "sidebar") return;
|
|
52
|
-
if (isEditableTarget(
|
|
53
|
-
if (!isVerticalArrow(
|
|
54
|
-
|
|
55
|
-
const nextUuid = resolveNextUuid(sidebarResults.value, selectedResultUuid.value,
|
|
52
|
+
if (isEditableTarget(event.target)) return;
|
|
53
|
+
if (!isVerticalArrow(event.key)) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
const nextUuid = resolveNextUuid(sidebarResults.value, selectedResultUuid.value, event.key);
|
|
56
56
|
if (nextUuid) selectedResultUuid.value = nextUuid;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -56,8 +56,8 @@ export function useMcpTools(opts: UseMcpToolsOptions) {
|
|
|
56
56
|
}
|
|
57
57
|
mcpToolsError.value = null;
|
|
58
58
|
const tools = result.data;
|
|
59
|
-
disabledMcpTools.value = new Set(tools.filter((
|
|
60
|
-
mcpToolDescriptions.value = Object.fromEntries(tools.filter(hasPrompt).map((
|
|
59
|
+
disabledMcpTools.value = new Set(tools.filter((tool) => !tool.enabled).map((tool) => tool.name));
|
|
60
|
+
mcpToolDescriptions.value = Object.fromEntries(tools.filter(hasPrompt).map((tool) => [tool.name, tool.prompt]));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
return {
|
|
@@ -73,7 +73,7 @@ export function useNotifications(): {
|
|
|
73
73
|
|
|
74
74
|
const unreadCount = computed(() => {
|
|
75
75
|
if (!readAt.value) return notifications.value.length;
|
|
76
|
-
return notifications.value.filter((
|
|
76
|
+
return notifications.value.filter((notif) => notif.firedAt > readAt.value!).length;
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
function markAllRead(): void {
|
|
@@ -82,8 +82,8 @@ export function useNotifications(): {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function dismiss(
|
|
86
|
-
notifications.value = notifications.value.filter((
|
|
85
|
+
function dismiss(notifId: string): void {
|
|
86
|
+
notifications.value = notifications.value.filter((notif) => notif.id !== notifId);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return { notifications, latest, unreadCount, markAllRead, dismiss };
|
|
@@ -42,10 +42,10 @@ export function usePdfDownload(): UsePdfDownloadHandle {
|
|
|
42
42
|
}
|
|
43
43
|
const blob = await response.blob();
|
|
44
44
|
url = URL.createObjectURL(blob);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const anchor = document.createElement("a");
|
|
46
|
+
anchor.href = url;
|
|
47
|
+
anchor.download = filename;
|
|
48
|
+
anchor.click();
|
|
49
49
|
} catch (err) {
|
|
50
50
|
pdfError.value = errorMessage(err);
|
|
51
51
|
} finally {
|