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.
Files changed (120) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/{index-KNLBjwuh.css → index-Bm70FDU2.css} +1 -1
  3. package/client/assets/{index-D8rhwXLq.js → index-eHWB79u5.js} +3 -3
  4. package/client/index.html +2 -2
  5. package/package.json +1 -1
  6. package/server/agent/config.ts +12 -12
  7. package/server/agent/mcp-server.ts +19 -19
  8. package/server/agent/mcp-tools/x.ts +5 -5
  9. package/server/agent/prompt.ts +9 -4
  10. package/server/agent/sandboxMounts.ts +7 -7
  11. package/server/agent/stream.ts +4 -4
  12. package/server/api/routes/files.ts +9 -9
  13. package/server/api/routes/scheduler.ts +8 -8
  14. package/server/api/routes/schedulerHandlers.ts +12 -12
  15. package/server/api/routes/schedulerTasks.ts +14 -14
  16. package/server/api/routes/sessions.ts +24 -24
  17. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  18. package/server/api/routes/wiki.ts +14 -14
  19. package/server/events/scheduler-adapter.ts +20 -20
  20. package/server/events/session-store/index.ts +10 -10
  21. package/server/events/task-manager/index.ts +7 -7
  22. package/server/index.ts +19 -19
  23. package/server/utils/date.ts +18 -18
  24. package/server/utils/files/atomic.ts +9 -9
  25. package/server/utils/files/html-io.ts +5 -5
  26. package/server/utils/files/image-store.ts +2 -2
  27. package/server/utils/files/journal-io.ts +2 -2
  28. package/server/utils/files/naming.ts +2 -2
  29. package/server/utils/files/roles-io.ts +10 -10
  30. package/server/utils/files/scheduler-io.ts +5 -5
  31. package/server/utils/files/session-io.ts +35 -35
  32. package/server/utils/files/spreadsheet-store.ts +2 -2
  33. package/server/utils/files/todos-io.ts +9 -9
  34. package/server/utils/files/user-tasks-io.ts +5 -5
  35. package/server/workspace/chat-index/indexer.ts +15 -15
  36. package/server/workspace/custom-dirs.ts +11 -11
  37. package/server/workspace/journal/archivist.ts +35 -35
  38. package/server/workspace/journal/dailyPass.ts +31 -28
  39. package/server/workspace/journal/indexFile.ts +29 -25
  40. package/server/workspace/reference-dirs.ts +18 -18
  41. package/server/workspace/roles.ts +6 -6
  42. package/server/workspace/skills/discovery.ts +4 -4
  43. package/server/workspace/skills/user-tasks.ts +34 -34
  44. package/server/workspace/sources/arxivDiscovery.ts +8 -8
  45. package/server/workspace/sources/classifier.ts +7 -7
  46. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  47. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  48. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  49. package/server/workspace/sources/interests.ts +9 -9
  50. package/server/workspace/sources/pipeline/index.ts +6 -6
  51. package/server/workspace/sources/pipeline/plan.ts +5 -5
  52. package/server/workspace/sources/registry.ts +16 -16
  53. package/server/workspace/sources/robots.ts +14 -14
  54. package/server/workspace/sources/sourceState.ts +11 -9
  55. package/server/workspace/tool-trace/index.ts +1 -1
  56. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  57. package/server/workspace/wiki-backlinks/index.ts +8 -8
  58. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  59. package/src/App.vue +30 -30
  60. package/src/components/ChatInput.vue +7 -7
  61. package/src/components/LockStatusPopup.vue +2 -2
  62. package/src/components/NotificationToast.vue +2 -2
  63. package/src/components/RoleSelector.vue +2 -2
  64. package/src/components/SessionHistoryPanel.vue +6 -6
  65. package/src/components/SettingsMcpTab.vue +7 -7
  66. package/src/components/SettingsModal.vue +3 -3
  67. package/src/components/SettingsReferenceDirsTab.vue +10 -10
  68. package/src/components/SettingsWorkspaceDirsTab.vue +5 -5
  69. package/src/components/SuggestionsPanel.vue +2 -2
  70. package/src/components/todo/TodoAddDialog.vue +2 -2
  71. package/src/components/todo/TodoEditPanel.vue +2 -2
  72. package/src/components/todo/TodoListView.vue +5 -5
  73. package/src/composables/useCanvasViewMode.ts +5 -5
  74. package/src/composables/useClickOutside.ts +2 -2
  75. package/src/composables/useFreshPluginData.ts +3 -3
  76. package/src/composables/useKeyNavigation.ts +11 -11
  77. package/src/composables/useMcpTools.ts +2 -2
  78. package/src/composables/useNotifications.ts +3 -3
  79. package/src/composables/usePdfDownload.ts +4 -4
  80. package/src/composables/usePendingCalls.ts +1 -1
  81. package/src/composables/usePubSub.ts +10 -10
  82. package/src/composables/useRoles.ts +1 -1
  83. package/src/composables/useSandboxStatus.ts +1 -1
  84. package/src/composables/useSessionDerived.ts +3 -3
  85. package/src/composables/useSessionSync.ts +8 -8
  86. package/src/composables/useViewLayout.ts +2 -2
  87. package/src/config/roles.ts +2 -2
  88. package/src/plugins/chart/Preview.vue +4 -4
  89. package/src/plugins/manageSkills/View.vue +3 -3
  90. package/src/plugins/manageSource/Preview.vue +1 -1
  91. package/src/plugins/markdown/View.vue +2 -2
  92. package/src/plugins/presentHtml/helpers.ts +8 -8
  93. package/src/plugins/presentMulmoScript/View.vue +4 -4
  94. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  95. package/src/plugins/scheduler/Preview.vue +6 -6
  96. package/src/plugins/scheduler/TasksTab.vue +4 -4
  97. package/src/plugins/textResponse/View.vue +2 -2
  98. package/src/plugins/todo/Preview.vue +2 -2
  99. package/src/plugins/todo/View.vue +11 -11
  100. package/src/plugins/todo/composables/useTodos.ts +5 -5
  101. package/src/plugins/wiki/Preview.vue +5 -5
  102. package/src/plugins/wiki/helpers.ts +4 -4
  103. package/src/router/guards.ts +12 -12
  104. package/src/types/session.ts +4 -3
  105. package/src/utils/agent/request.ts +3 -3
  106. package/src/utils/dom/scrollable.ts +2 -2
  107. package/src/utils/files/expandedDirs.ts +1 -1
  108. package/src/utils/files/sortChildren.ts +6 -6
  109. package/src/utils/format/frontmatter.ts +6 -6
  110. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  111. package/src/utils/markdown/extractFirstH1.ts +2 -2
  112. package/src/utils/path/relativeLink.ts +15 -15
  113. package/src/utils/role/icon.ts +2 -2
  114. package/src/utils/role/merge.ts +2 -2
  115. package/src/utils/role/plugins.ts +1 -1
  116. package/src/utils/session/sessionFactory.ts +2 -2
  117. package/src/utils/session/sessionHelpers.ts +2 -2
  118. package/src/utils/tools/dedup.ts +4 -4
  119. package/src/utils/tools/result.ts +3 -3
  120. 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 id = extractSessionIdFromHref(href);
77
- if (id) ids.add(id);
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 id = cleanHref.slice(lastSlash + 1, cleanHref.length - JSONL_SUFFIX.length);
109
- if (id.length === 0 || id.includes("/")) return null;
110
- return id;
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(s: string): string {
114
- let end = s.length;
115
- const hash = s.indexOf("#");
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 = s.indexOf("?");
117
+ const query = href.indexOf("?");
118
118
  if (query !== -1 && query < end) end = query;
119
- return s.slice(0, end);
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(s: string, lastSlash: number): string {
125
- const prevSlash = s.lastIndexOf("/", lastSlash - 1);
126
- return s.slice(prevSlash + 1, lastSlash);
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 ch = appendix.charCodeAt(end - 1);
138
+ const charCode = appendix.charCodeAt(end - 1);
139
139
  // Whitespace codepoints we expect here: \n, \r, \t, space.
140
- if (ch !== 10 && ch !== 13 && ch !== 9 && ch !== 32) break;
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 id = currentRoleId.value;
257
- if (!id || roles.value.length === 0 || id === roles.value[0]?.id) return {};
258
- return { role: id };
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(id: string, replace = false): void {
262
- currentSessionId.value = id;
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: id },
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(id: string): void {
385
+ function handleSessionSelect(sessionId: string): void {
386
386
  restoreChatViewForSession();
387
- loadSession(id);
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, (id) => {
429
- const session = sessionMap.get(id);
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 !== id) {
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 = id;
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 === 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(id);
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 id = currentSessionId.value;
488
- if (!id) return false;
489
- const session = sessionMap.get(id);
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(id);
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(id: string, roleId: string, replace: boolean): void {
519
- const reactiveSession = sessionMap.get(id);
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(id, replace);
525
+ navigateToSession(sessionId, replace);
526
526
  showHistory.value = false;
527
527
  }
528
528
 
529
- async function loadSession(id: string) {
530
- if (id === currentSessionId.value && sessionMap.has(id)) return;
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(id);
533
+ const live = sessionMap.get(sessionId);
534
534
  if (live) {
535
- activateSession(id, live.roleId, replaced);
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(id)));
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((s) => s.id === id),
547
+ serverSummary: sessions.value.find((summary) => summary.id === sessionId),
548
548
  nowIso: new Date().toISOString(),
549
549
  });
550
- sessionMap.set(id, newSession);
551
- activateSession(id, newSession.roleId, replaced);
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((p) => mime.startsWith(p)) || ACCEPTED_MIME_EXACT.has(mime);
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(e: ClipboardEvent): void {
158
- const items = e.clipboardData?.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
- e.preventDefault();
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(e: DragEvent): void {
173
- e.preventDefault();
174
- const file = e.dataTransfer?.files[0];
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
 
@@ -104,8 +104,8 @@ watch(
104
104
  },
105
105
  );
106
106
 
107
- function onTestQuery(q: string): void {
107
+ function onTestQuery(query: string): void {
108
108
  emit("update:open", false);
109
- emit("testQuery", q);
109
+ emit("testQuery", query);
110
110
  }
111
111
  </script>
@@ -28,8 +28,8 @@ function dismiss(): void {
28
28
  visible.value = null;
29
29
  }
30
30
 
31
- function iconName(n: NotificationPayload): string {
32
- return n.icon ?? NOTIFICATION_ICONS[n.kind] ?? "notifications";
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(id: string): void {
51
- emit("update:currentRoleId", id);
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((s) => originOf(s) === activeFilter.value);
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((s) => originOf(s) === origin).length;
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(id: string): string {
150
- return roleIcon(props.roles, id);
149
+ function roleIconFor(roleId: string): string {
150
+ return roleIcon(props.roles, roleId);
151
151
  }
152
- function roleNameFor(id: string): string {
153
- return roleName(props.roles, id);
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 id = draft.value.id.trim();
269
- if (!id) {
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
- id = suggested;
275
+ mcpId = suggested;
276
276
  }
277
- if (!ID_RE.test(id)) {
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 === id)) {
282
- draftError.value = `Server id "${id}" already exists.`;
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((s) => s.trim())
181
- .filter((s) => s.length > 0),
180
+ .map((line) => line.trim())
181
+ .filter((line) => line.length > 0),
182
182
  );
183
183
 
184
- const invalidToolNames = computed(() => parsedToolNames.value.filter((n) => !n.startsWith("mcp__") && !isBuiltIn(n)));
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 p = draftPath.value.trim();
52
- if (!p) {
51
+ const path = draftPath.value.trim();
52
+ if (!path) {
53
53
  draftError.value = "Path required";
54
54
  return;
55
55
  }
56
- if (!p.startsWith("/") && !p.startsWith("~/")) {
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 = p;
61
+ let normalized = path;
62
62
  while (normalized.length > 1 && normalized.endsWith("/")) {
63
63
  normalized = normalized.slice(0, -1);
64
64
  }
65
- const stripSlash = (s: string): string => {
66
- let r = s;
67
- while (r.length > 1 && r.endsWith("/")) r = r.slice(0, -1);
68
- return r;
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((d) => stripSlash(d.hostPath) === normalized)) {
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((d) => d.label === label)) {
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 p = draftPath.value.trim();
55
- if (!p) {
54
+ const path = draftPath.value.trim();
55
+ if (!path) {
56
56
  draftError.value = "Path required";
57
57
  return;
58
58
  }
59
- if (!p.startsWith("data/") && !p.startsWith("artifacts/")) {
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((d) => d.path === p)) {
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: p,
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(e: MouseEvent, query: string): void {
51
+ function onClick(event: MouseEvent, query: string): void {
52
52
  expanded.value = false;
53
- if (e.shiftKey) {
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((s) => s.trim())
127
- .filter((s) => s.length > 0);
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((s) => s.trim())
89
- .filter((s) => s.length > 0);
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(id: string): void {
62
- expandedId.value = expandedId.value === id ? null : id;
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(id: string, input: PatchItemInput): void {
70
- emit("patch", id, input);
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((c) => c.id === item.status);
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(e: KeyboardEvent): void {
98
- if (!(e.metaKey || e.ctrlKey)) return;
99
- if (e.altKey || e.shiftKey) return;
100
- const target = viewModeForShortcutKey(e.key);
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
- e.preventDefault();
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(e: MouseEvent): void {
19
+ function handler(event: MouseEvent): void {
20
20
  if (!opts.isOpen.value) return;
21
- if (isClickOutside(e.target as Node | null, opts.buttonRef.value, opts.popupRef.value)) {
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 c = new AbortController();
74
- controller = c;
75
- return refreshOnce(opts, c.signal);
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((r) => r.uuid === currentUuid);
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(e: KeyboardEvent): void {
40
- if (!isVerticalArrow(e.key)) return;
41
- if (isEditableTarget(e.target)) return;
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
- e.preventDefault();
46
- const delta = e.key === "ArrowDown" ? SCROLL_AMOUNT : -SCROLL_AMOUNT;
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(e: KeyboardEvent): void {
50
+ function handleKeyNavigation(event: KeyboardEvent): void {
51
51
  if (activePane.value !== "sidebar") return;
52
- if (isEditableTarget(e.target)) return;
53
- if (!isVerticalArrow(e.key)) return;
54
- e.preventDefault();
55
- const nextUuid = resolveNextUuid(sidebarResults.value, selectedResultUuid.value, e.key);
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((t) => !t.enabled).map((t) => t.name));
60
- mcpToolDescriptions.value = Object.fromEntries(tools.filter(hasPrompt).map((t) => [t.name, t.prompt]));
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((n) => n.firedAt > readAt.value!).length;
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(id: string): void {
86
- notifications.value = notifications.value.filter((n) => n.id !== id);
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 a = document.createElement("a");
46
- a.href = url;
47
- a.download = filename;
48
- a.click();
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 {