mulmoclaude 0.1.2 → 0.4.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 +7 -24
- package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +22 -21
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- 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 +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- 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/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- 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/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- 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/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +5 -5
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- 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/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- 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/path/workspaceLinkRouter.ts +81 -0
- 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
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-KNLBjwuh.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
package/src/router/guards.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// doesn't push a history entry).
|
|
8
8
|
|
|
9
9
|
import type { Router } from "vue-router";
|
|
10
|
-
import {
|
|
10
|
+
import { readPathMatch } from "../composables/useFileSelection";
|
|
11
|
+
import { readWikiRouteTarget } from "../plugins/wiki/route";
|
|
11
12
|
|
|
12
13
|
// Basic sanity check for a session ID. Real existence verification
|
|
13
14
|
// happens in App.vue's onMounted / loadSession — we can't do async
|
|
@@ -18,43 +19,60 @@ import { VALID_VIEW_MODES } from "../utils/canvas/viewMode";
|
|
|
18
19
|
// doesn't exist on the server" gracefully.
|
|
19
20
|
const SESSION_ID_RE = /^[\w-]{1,128}$/;
|
|
20
21
|
|
|
21
|
-
function isValidSessionId(
|
|
22
|
-
return typeof
|
|
22
|
+
function isValidSessionId(value: unknown): boolean {
|
|
23
|
+
return typeof value === "string" && SESSION_ID_RE.test(value);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function installGuards(router: Router): void {
|
|
26
|
-
router.beforeEach((
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const sessionId = to.params.sessionId;
|
|
33
|
-
if (typeof sessionId === "string" && sessionId.length > 0 && !isValidSessionId(sessionId)) {
|
|
34
|
-
// Garbage sessionId → strip it and go to /chat (new session).
|
|
35
|
-
return { name: "chat", params: {}, query: {}, replace: true };
|
|
27
|
+
router.beforeEach((dest) => {
|
|
28
|
+
if (dest.name === "chat") {
|
|
29
|
+
const sessionId = dest.params.sessionId;
|
|
30
|
+
if (typeof sessionId === "string" && sessionId.length > 0 && !isValidSessionId(sessionId)) {
|
|
31
|
+
return { name: "chat", params: {}, query: {}, replace: true };
|
|
32
|
+
}
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
if (dest.name === "wiki") {
|
|
36
|
+
// Vue Router decodes `%2F` back to `/` in `route.params.slug`,
|
|
37
|
+
// so `/wiki/pages/..%2Fsecrets` arrives here as
|
|
38
|
+
// `{ section: "pages", slug: "../secrets" }`. `readWikiRouteTarget`
|
|
39
|
+
// returns `null` for unsafe slugs, a missing slug on the `pages`
|
|
40
|
+
// section, or an unknown section. In any of those cases, bounce
|
|
41
|
+
// to `/wiki` with `replace: true` so no broken URL lands in
|
|
42
|
+
// history. Legal targets fall through to the view, where the
|
|
43
|
+
// route watcher drives the fetch.
|
|
44
|
+
if (readWikiRouteTarget(dest.params) === null) {
|
|
45
|
+
return { name: "wiki", params: {}, query: dest.query, replace: true };
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if (dest.name === "files") {
|
|
50
|
+
// Back-compat: old query-string form `/files?path=foo.md` →
|
|
51
|
+
// rewrite to the new path form `/files/foo.md`. Silent
|
|
52
|
+
// replace so bookmarks / log links keep working. Do this
|
|
53
|
+
// before the traversal check so `?path=../bad` also lands in
|
|
54
|
+
// the `..` rejection below.
|
|
55
|
+
const legacyPath = dest.query.path;
|
|
56
|
+
if (typeof legacyPath === "string" && legacyPath.length > 0) {
|
|
57
|
+
const cleaned = { ...dest.query };
|
|
51
58
|
delete cleaned.path;
|
|
52
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
name: "files",
|
|
61
|
+
params: { pathMatch: legacyPath.split("/") },
|
|
62
|
+
query: cleaned,
|
|
63
|
+
replace: true,
|
|
64
|
+
};
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
// Traversal / absolute-path rejection against the new param.
|
|
68
|
+
const filePath = readPathMatch(dest.params.pathMatch);
|
|
69
|
+
if (typeof filePath === "string" && (filePath.includes("..") || filePath.startsWith("/"))) {
|
|
70
|
+
return {
|
|
71
|
+
name: "files",
|
|
72
|
+
params: { pathMatch: [] },
|
|
73
|
+
query: dest.query,
|
|
74
|
+
replace: true,
|
|
75
|
+
};
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
});
|
package/src/router/index.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// Vue-router setup (history mode — clean URLs without #).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// The "/" → "/chat" redirect ensures a fresh browser tab always lands
|
|
8
|
-
// on the chat view with the default (new) session, matching the
|
|
9
|
-
// current pre-router behaviour.
|
|
3
|
+
// Each page has its own route: /chat, /files, /todos, /scheduler,
|
|
4
|
+
// /wiki, /skills, /roles. Layout preference (single vs. stack) is a
|
|
5
|
+
// separate concern persisted in localStorage — it is not part of the
|
|
6
|
+
// URL.
|
|
10
7
|
//
|
|
11
8
|
// History mode requires the server to serve index.html for any path
|
|
12
9
|
// that doesn't match an API route or static file. In production the
|
|
@@ -18,28 +15,46 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"
|
|
|
18
15
|
|
|
19
16
|
// Stub component that renders nothing. Required by vue-router (every
|
|
20
17
|
// route needs a component) but never actually mounted because App.vue
|
|
21
|
-
//
|
|
18
|
+
// renders based on `route.name` rather than using <router-view>.
|
|
22
19
|
const Stub = defineComponent({ render: () => h("div") });
|
|
23
20
|
|
|
21
|
+
export const PAGE_ROUTES = {
|
|
22
|
+
chat: "chat",
|
|
23
|
+
files: "files",
|
|
24
|
+
todos: "todos",
|
|
25
|
+
scheduler: "scheduler",
|
|
26
|
+
wiki: "wiki",
|
|
27
|
+
skills: "skills",
|
|
28
|
+
roles: "roles",
|
|
29
|
+
history: "history",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type PageRouteName = (typeof PAGE_ROUTES)[keyof typeof PAGE_ROUTES];
|
|
33
|
+
|
|
24
34
|
const routes: RouteRecordRaw[] = [
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
{ path: "/", redirect: "/chat" },
|
|
36
|
+
{ path: "/chat/:sessionId?", name: PAGE_ROUTES.chat, component: Stub },
|
|
37
|
+
// Files view uses a repeatable catch-all so `/files/a/b/c.md` maps
|
|
38
|
+
// to `params.pathMatch = ["a", "b", "c.md"]`. Joining on `/` at read
|
|
39
|
+
// time keeps each segment URL-encoded independently — passing a
|
|
40
|
+
// string-form catch-all (`:pathMatch(.*)`) would collapse slashes
|
|
41
|
+
// to `%2F` at push time and mangle deep paths. An empty segment
|
|
42
|
+
// (`/files`) yields an empty array, which we treat as "no file
|
|
43
|
+
// selected". See plans/feat-files-path-url.md.
|
|
44
|
+
{ path: "/files/:pathMatch(.*)*", name: PAGE_ROUTES.files, component: Stub },
|
|
45
|
+
{ path: "/todos", name: PAGE_ROUTES.todos, component: Stub },
|
|
46
|
+
{ path: "/scheduler", name: PAGE_ROUTES.scheduler, component: Stub },
|
|
47
|
+
// Wiki sub-views live on the path rather than in query params so
|
|
48
|
+
// URLs mirror the filesystem layout (`data/wiki/pages/<slug>.md`)
|
|
49
|
+
// and stay sibling-safe (no query-key bleed from other routes).
|
|
50
|
+
// `section` is a closed enum; unknown sections fall through to the
|
|
51
|
+
// catch-all redirect below. `slug` only applies when `section ===
|
|
52
|
+
// "pages"`. See plans/feat-wiki-path-urls.md.
|
|
53
|
+
{ path: "/wiki/:section(pages|log|lint-report)?/:slug?", name: PAGE_ROUTES.wiki, component: Stub },
|
|
54
|
+
{ path: "/skills", name: PAGE_ROUTES.skills, component: Stub },
|
|
55
|
+
{ path: "/roles", name: PAGE_ROUTES.roles, component: Stub },
|
|
56
|
+
{ path: "/history", name: PAGE_ROUTES.history, component: Stub },
|
|
57
|
+
{ path: "/:pathMatch(.*)*", redirect: "/chat" },
|
|
43
58
|
];
|
|
44
59
|
|
|
45
60
|
const router = createRouter({
|
package/src/types/session.ts
CHANGED
|
@@ -70,10 +70,11 @@ export interface ToolResultEntry extends SessionEntry {
|
|
|
70
70
|
result: ToolResultComplete;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export const isTextEntry = (
|
|
74
|
-
(
|
|
73
|
+
export const isTextEntry = (entry: SessionEntry): entry is TextEntry =>
|
|
74
|
+
(entry.source === "user" || entry.source === "assistant") && entry.type === EVENT_TYPES.text && typeof entry.message === "string";
|
|
75
75
|
|
|
76
|
-
export const isToolResultEntry = (
|
|
76
|
+
export const isToolResultEntry = (entry: SessionEntry): entry is ToolResultEntry =>
|
|
77
|
+
entry.source === "tool" && entry.type === EVENT_TYPES.toolResult && entry.result !== undefined;
|
|
77
78
|
|
|
78
79
|
// In-memory session held in `sessionMap`. PR #88 introduced this so
|
|
79
80
|
// multiple chats can run concurrently — `id` matches the `chatSessionId`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Module augmentation for vue-i18n. Surfaces the English dictionary
|
|
2
|
+
// shape as `DefineLocaleMessage` so IDEs autocomplete key paths on
|
|
3
|
+
// `$t("…")` and `i18n.global.t("…")`. The schema generic on
|
|
4
|
+
// createI18n (src/lib/vue-i18n.ts) handles the `useI18n()` side.
|
|
5
|
+
//
|
|
6
|
+
// Caveat: vue-i18n v11's `t()` overload has a `Key extends string`
|
|
7
|
+
// fallback, so a typo in `useI18n().t("…")` at a deeply-typed call
|
|
8
|
+
// site may still compile. Autocomplete is the main value; strict
|
|
9
|
+
// rejection of unknown keys would require a bespoke wrapper.
|
|
10
|
+
|
|
11
|
+
import enMessages from "../lang/en";
|
|
12
|
+
|
|
13
|
+
// Alias so `extends` has an interface-shaped base (typeof in extends
|
|
14
|
+
// position is a syntax error).
|
|
15
|
+
type EnMessages = typeof enMessages;
|
|
16
|
+
|
|
17
|
+
declare module "vue-i18n" {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
19
|
+
export interface DefineLocaleMessage extends EnMessages {}
|
|
20
|
+
}
|
|
@@ -16,6 +16,24 @@ export interface AgentRequestBody {
|
|
|
16
16
|
roleId: string;
|
|
17
17
|
chatSessionId: string;
|
|
18
18
|
selectedImageData: string | undefined;
|
|
19
|
+
// IANA identifier (e.g. "Asia/Tokyo", "America/New_York"). The
|
|
20
|
+
// server uses this to interpret bare time expressions in the user's
|
|
21
|
+
// message without asking for clarification every turn. Undefined if
|
|
22
|
+
// the browser can't resolve a timezone — the server then falls back
|
|
23
|
+
// to its own local time and asks as before.
|
|
24
|
+
userTimezone: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// `Intl.DateTimeFormat().resolvedOptions().timeZone` can, in theory,
|
|
28
|
+
// throw in some locked-down environments; wrap so a broken Intl
|
|
29
|
+
// doesn't take down the send path.
|
|
30
|
+
function resolveBrowserTimezone(): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
const zoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
33
|
+
return typeof zoneId === "string" && zoneId.length > 0 ? zoneId : undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
export function buildAgentRequestBody(params: AgentRequestBodyParams): AgentRequestBody {
|
|
@@ -24,6 +42,7 @@ export function buildAgentRequestBody(params: AgentRequestBodyParams): AgentRequ
|
|
|
24
42
|
roleId: params.role.id,
|
|
25
43
|
chatSessionId: params.chatSessionId,
|
|
26
44
|
selectedImageData: params.selectedImageData,
|
|
45
|
+
userTimezone: resolveBrowserTimezone(),
|
|
27
46
|
};
|
|
28
47
|
}
|
|
29
48
|
|
|
@@ -45,11 +64,11 @@ export async function postAgentRun(body: AgentRequestBody): Promise<{ ok: true }
|
|
|
45
64
|
};
|
|
46
65
|
}
|
|
47
66
|
return { ok: true };
|
|
48
|
-
} catch (
|
|
49
|
-
console.error("[agent] fetch error:",
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error("[agent] fetch error:", err);
|
|
50
69
|
return {
|
|
51
70
|
ok: false,
|
|
52
|
-
error:
|
|
71
|
+
error: err instanceof Error ? err.message : "Connection error.",
|
|
53
72
|
};
|
|
54
73
|
}
|
|
55
74
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Pure helpers for the canvas layout mode.
|
|
2
|
+
//
|
|
3
|
+
// Layout is a user preference — single (sidebar + canvas) vs. stack
|
|
4
|
+
// (no sidebar, full-width canvas). It applies only to the /chat page
|
|
5
|
+
// and persists in localStorage.
|
|
6
|
+
//
|
|
7
|
+
// Pages like /files, /todos, /wiki, /skills, /roles, /scheduler are
|
|
8
|
+
// distinct routes, not layout variants. They live in the router, not
|
|
9
|
+
// here.
|
|
10
|
+
|
|
11
|
+
export const LAYOUT_MODES = {
|
|
12
|
+
single: "single",
|
|
13
|
+
stack: "stack",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type LayoutMode = (typeof LAYOUT_MODES)[keyof typeof LAYOUT_MODES];
|
|
17
|
+
|
|
18
|
+
export const LAYOUT_MODE_STORAGE_KEY = "canvas_layout_mode";
|
|
19
|
+
|
|
20
|
+
// Legacy key from before layout/page were split. Deleted on first
|
|
21
|
+
// read of useLayoutMode — not migrated (fresh start).
|
|
22
|
+
export const LEGACY_VIEW_MODE_STORAGE_KEY = "canvas_view_mode";
|
|
23
|
+
|
|
24
|
+
export function parseStoredLayoutMode(stored: string | null): LayoutMode {
|
|
25
|
+
return stored === LAYOUT_MODES.stack ? LAYOUT_MODES.stack : LAYOUT_MODES.single;
|
|
26
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// real DOM.
|
|
12
12
|
export function findScrollableChild(container: HTMLElement): HTMLElement | null {
|
|
13
13
|
const children = container.querySelectorAll("*");
|
|
14
|
-
for (const
|
|
15
|
-
const html =
|
|
14
|
+
for (const elem of children) {
|
|
15
|
+
const html = elem as HTMLElement;
|
|
16
16
|
if (html.scrollHeight > html.clientHeight) {
|
|
17
17
|
const style = getComputedStyle(html);
|
|
18
18
|
if (style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll") {
|
|
@@ -13,7 +13,7 @@ export function parseStoredExpandedDirs(raw: string | null): Set<string> {
|
|
|
13
13
|
try {
|
|
14
14
|
const parsed: unknown = JSON.parse(raw);
|
|
15
15
|
if (!Array.isArray(parsed)) return new Set(DEFAULT_EXPANDED);
|
|
16
|
-
const strings = parsed.filter((
|
|
16
|
+
const strings = parsed.filter((val): val is string => typeof val === "string");
|
|
17
17
|
return new Set(strings);
|
|
18
18
|
} catch {
|
|
19
19
|
return new Set(DEFAULT_EXPANDED);
|
|
@@ -7,14 +7,14 @@ import type { FileSortMode } from "../../composables/useFileSortMode";
|
|
|
7
7
|
// on name so the order is deterministic).
|
|
8
8
|
export function sortChildren(children: readonly TreeNode[], mode: FileSortMode): TreeNode[] {
|
|
9
9
|
const copy = children.slice();
|
|
10
|
-
copy.sort((
|
|
11
|
-
if (
|
|
10
|
+
copy.sort((nodeA, nodeB) => {
|
|
11
|
+
if (nodeA.type !== nodeB.type) return nodeA.type === "dir" ? -1 : 1;
|
|
12
12
|
if (mode === "recent") {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
13
|
+
const modTimeA = nodeA.modifiedMs ?? -Infinity;
|
|
14
|
+
const modTimeB = nodeB.modifiedMs ?? -Infinity;
|
|
15
|
+
if (modTimeA !== modTimeB) return modTimeB - modTimeA;
|
|
16
16
|
}
|
|
17
|
-
return
|
|
17
|
+
return nodeA.name.localeCompare(nodeB.name);
|
|
18
18
|
});
|
|
19
19
|
return copy;
|
|
20
20
|
}
|
|
@@ -66,15 +66,15 @@ function parseValue(raw: string): FrontmatterValue {
|
|
|
66
66
|
if (arrayMatch) {
|
|
67
67
|
return arrayMatch[1]
|
|
68
68
|
.split(",")
|
|
69
|
-
.map((
|
|
70
|
-
.filter((
|
|
69
|
+
.map((item) => unquote(item.trim()))
|
|
70
|
+
.filter((item) => item.length > 0);
|
|
71
71
|
}
|
|
72
72
|
return unquote(raw);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function unquote(
|
|
76
|
-
if ((
|
|
77
|
-
return
|
|
75
|
+
function unquote(str: string): string {
|
|
76
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
|
77
|
+
return str.slice(1, -1);
|
|
78
78
|
}
|
|
79
|
-
return
|
|
79
|
+
return str;
|
|
80
80
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { reactive } from "vue";
|
|
2
|
+
|
|
3
|
+
// Keyed by workspace-relative image path (e.g. "artifacts/images/abc.png").
|
|
4
|
+
// `resolveImageSrc` reads this to append `?v=<bump>` to the URL so consumers
|
|
5
|
+
// (View, Preview) re-fetch when the file on disk has been overwritten in
|
|
6
|
+
// place. The canvas plugin is the current producer — it bumps after each
|
|
7
|
+
// autosave PUT.
|
|
8
|
+
const imageBumps = reactive<Record<string, number>>({});
|
|
9
|
+
|
|
10
|
+
export function getImageBump(imagePath: string): number {
|
|
11
|
+
return imageBumps[imagePath] ?? 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function bumpImage(imagePath: string): void {
|
|
15
|
+
imageBumps[imagePath] = Date.now();
|
|
16
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
2
|
+
import { getImageBump } from "./cacheBust";
|
|
2
3
|
|
|
3
4
|
/** Convert an imageData value to a displayable URL.
|
|
4
5
|
* Handles both legacy data URIs and workspace-relative file paths. */
|
|
@@ -6,3 +7,18 @@ export function resolveImageSrc(imageData: string): string {
|
|
|
6
7
|
if (imageData.startsWith("data:")) return imageData;
|
|
7
8
|
return `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
|
|
8
9
|
}
|
|
10
|
+
|
|
11
|
+
/** Same as `resolveImageSrc` but appends the current cache-bust token
|
|
12
|
+
* so the browser re-fetches when the file has been overwritten in
|
|
13
|
+
* place (e.g. the canvas plugin rewrote it).
|
|
14
|
+
*
|
|
15
|
+
* Use this from display-only consumers (Preview, thumbnail list).
|
|
16
|
+
* Avoid inside the canvas View's own `backgroundImage` — changing
|
|
17
|
+
* that URL mid-session makes `vue-drawing-canvas` re-fetch on every
|
|
18
|
+
* redraw, which races with stroke painting and blanks the canvas. */
|
|
19
|
+
export function resolveImageSrcFresh(imageData: string): string {
|
|
20
|
+
if (imageData.startsWith("data:")) return imageData;
|
|
21
|
+
const base = `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
|
|
22
|
+
const bump = getImageBump(imageData);
|
|
23
|
+
return bump > 0 ? `${base}&v=${bump}` : base;
|
|
24
|
+
}
|
|
@@ -51,7 +51,7 @@ function shouldSkip(url: string): boolean {
|
|
|
51
51
|
function resolveWorkspacePath(basePath: string, url: string): string | null {
|
|
52
52
|
// Absolute-within-workspace (e.g. "/images/foo.png") — reset base.
|
|
53
53
|
const isAbsolute = url.startsWith("/");
|
|
54
|
-
const baseSegs = isAbsolute ? [] : basePath.split("/").filter((
|
|
54
|
+
const baseSegs = isAbsolute ? [] : basePath.split("/").filter((seg) => seg !== "" && seg !== ".");
|
|
55
55
|
const segs = [...baseSegs];
|
|
56
56
|
|
|
57
57
|
const urlSegs = (isAbsolute ? url.slice(1) : url).split("/");
|
|
@@ -76,9 +76,9 @@ function extractBracketedAlt(raw: string): string | null {
|
|
|
76
76
|
if (!raw.startsWith("![")) return null;
|
|
77
77
|
let depth = 1;
|
|
78
78
|
for (let i = 2; i < raw.length; i++) {
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
else if (
|
|
79
|
+
const char = raw[i];
|
|
80
|
+
if (char === "[") depth++;
|
|
81
|
+
else if (char === "]") {
|
|
82
82
|
depth--;
|
|
83
83
|
if (depth === 0) return raw.slice(2, i);
|
|
84
84
|
}
|
|
@@ -123,7 +123,7 @@ function getContainerChildren(token: Token): Token[] | null {
|
|
|
123
123
|
// Returns true if the container was rendered via its children, false
|
|
124
124
|
// if the caller should fall back to emitting the parent's raw.
|
|
125
125
|
function renderContainerChildren(raw: string, children: Token[], basePath: string, out: string[]): boolean {
|
|
126
|
-
const joined = children.map((
|
|
126
|
+
const joined = children.map((token) => (token as { raw?: string }).raw ?? "").join("");
|
|
127
127
|
if (joined === "") return false;
|
|
128
128
|
const idx = raw.indexOf(joined);
|
|
129
129
|
if (idx < 0) return false;
|
|
@@ -30,8 +30,8 @@ export function extractFirstH1(markdown: string): string | null {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function splitLines(
|
|
34
|
-
return
|
|
33
|
+
function splitLines(str: string): string[] {
|
|
34
|
+
return str.split(/\r\n|\r|\n/);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function isInlineSpace(code: number): boolean {
|
|
@@ -73,13 +73,13 @@ export function resolveWorkspaceLink(currentFilePath: string, href: string): str
|
|
|
73
73
|
|
|
74
74
|
// Drop any trailing #fragment or ?query from a path-like string.
|
|
75
75
|
// Whichever marker comes first wins.
|
|
76
|
-
function stripFragmentAndQuery(
|
|
77
|
-
const hashIdx =
|
|
78
|
-
const queryIdx =
|
|
79
|
-
let end =
|
|
76
|
+
function stripFragmentAndQuery(str: string): string {
|
|
77
|
+
const hashIdx = str.indexOf("#");
|
|
78
|
+
const queryIdx = str.indexOf("?");
|
|
79
|
+
let end = str.length;
|
|
80
80
|
if (hashIdx !== -1 && hashIdx < end) end = hashIdx;
|
|
81
81
|
if (queryIdx !== -1 && queryIdx < end) end = queryIdx;
|
|
82
|
-
return
|
|
82
|
+
return str.slice(0, end);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// If `resolvedPath` points at a chat session log (e.g.
|
|
@@ -95,26 +95,26 @@ export function extractSessionIdFromPath(resolvedPath: string): string | null {
|
|
|
95
95
|
const JSONL_SUFFIX = ".jsonl";
|
|
96
96
|
if (!resolvedPath.startsWith(CHAT_PREFIX)) return null;
|
|
97
97
|
if (!resolvedPath.endsWith(JSONL_SUFFIX)) return null;
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
101
|
-
return
|
|
98
|
+
const sessionId = resolvedPath.slice(CHAT_PREFIX.length, resolvedPath.length - JSONL_SUFFIX.length);
|
|
99
|
+
if (sessionId.length === 0) return null;
|
|
100
|
+
if (sessionId.includes("/")) return null;
|
|
101
|
+
return sessionId;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// POSIX-style dirname. The file viewer always uses "/" separators
|
|
105
105
|
// so we don't need to worry about Windows paths.
|
|
106
|
-
function posixDirname(
|
|
107
|
-
const i =
|
|
108
|
-
return i === -1 ? "" :
|
|
106
|
+
function posixDirname(path: string): string {
|
|
107
|
+
const i = path.lastIndexOf("/");
|
|
108
|
+
return i === -1 ? "" : path.slice(0, i);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Collapse "./" and "../" in a workspace path. Rejects paths that
|
|
112
112
|
// escape above the workspace root. Returns null for the empty-path
|
|
113
113
|
// case so the caller can bail out. Callers are expected to strip
|
|
114
114
|
// #fragment / ?query before invoking this function.
|
|
115
|
-
function normalizeWorkspacePath(
|
|
116
|
-
if (
|
|
117
|
-
const parts =
|
|
115
|
+
function normalizeWorkspacePath(path: string): string | null {
|
|
116
|
+
if (path.length === 0) return null;
|
|
117
|
+
const parts = path.split("/");
|
|
118
118
|
const stack: string[] = [];
|
|
119
119
|
for (const part of parts) {
|
|
120
120
|
if (part === "" || part === ".") continue;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Classify a workspace-relative link from agent Markdown into a
|
|
2
|
+
// navigation target. Used by TextResponse's click handler to route
|
|
3
|
+
// internal links to the appropriate view (Wiki, Files, Session)
|
|
4
|
+
// instead of letting them fall through to the SPA router.
|
|
5
|
+
//
|
|
6
|
+
// Pure function — no DOM or Vue dependencies, fully unit-testable.
|
|
7
|
+
|
|
8
|
+
import { isExternalHref, extractSessionIdFromPath } from "./relativeLink";
|
|
9
|
+
|
|
10
|
+
export type WorkspaceLinkTarget = { kind: "wiki"; slug: string } | { kind: "file"; path: string } | { kind: "session"; sessionId: string };
|
|
11
|
+
|
|
12
|
+
// Match `data/wiki/pages/<slug>.md` or `wiki/pages/<slug>.md`.
|
|
13
|
+
const WIKI_PAGE_PATTERN = /(?:data\/)?wiki\/pages\/([^/]+)\.md$/;
|
|
14
|
+
|
|
15
|
+
// Match `conversations/chat/<id>.jsonl` (delegates to extractSessionIdFromPath).
|
|
16
|
+
const CHAT_LOG_PREFIX = "conversations/";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Given a raw href attribute from agent Markdown, return a typed
|
|
20
|
+
* navigation target, or null if the link is external, anchor-only,
|
|
21
|
+
* or unresolvable.
|
|
22
|
+
*
|
|
23
|
+
* Agent links are typically workspace-root-relative (e.g.
|
|
24
|
+
* `data/wiki/pages/foo.md`). Relative paths with `../` that escape
|
|
25
|
+
* the workspace root return null.
|
|
26
|
+
*/
|
|
27
|
+
export function classifyWorkspacePath(href: string): WorkspaceLinkTarget | null {
|
|
28
|
+
if (!href || isExternalHref(href)) return null;
|
|
29
|
+
if (href.startsWith("#")) return null;
|
|
30
|
+
|
|
31
|
+
// Strip fragment and query
|
|
32
|
+
const cleaned = stripFragmentAndQuery(href);
|
|
33
|
+
if (cleaned.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
// Normalize path (collapse ./ and ../, reject root-escape)
|
|
36
|
+
const normalized = normalizePath(cleaned);
|
|
37
|
+
if (!normalized) return null;
|
|
38
|
+
|
|
39
|
+
// Wiki page: data/wiki/pages/<slug>.md
|
|
40
|
+
const wikiMatch = normalized.match(WIKI_PAGE_PATTERN);
|
|
41
|
+
if (wikiMatch) {
|
|
42
|
+
return { kind: "wiki", slug: wikiMatch[1] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Chat session log: conversations/chat/<id>.jsonl
|
|
46
|
+
if (normalized.startsWith(CHAT_LOG_PREFIX)) {
|
|
47
|
+
const chatPath = normalized.slice(CHAT_LOG_PREFIX.length);
|
|
48
|
+
const sessionId = extractSessionIdFromPath(chatPath);
|
|
49
|
+
if (sessionId) {
|
|
50
|
+
return { kind: "session", sessionId };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Everything else: open in Files view
|
|
55
|
+
return { kind: "file", path: normalized };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripFragmentAndQuery(str: string): string {
|
|
59
|
+
const hashIdx = str.indexOf("#");
|
|
60
|
+
const queryIdx = str.indexOf("?");
|
|
61
|
+
let end = str.length;
|
|
62
|
+
if (hashIdx !== -1 && hashIdx < end) end = hashIdx;
|
|
63
|
+
if (queryIdx !== -1 && queryIdx < end) end = queryIdx;
|
|
64
|
+
return str.slice(0, end);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizePath(raw: string): string | null {
|
|
68
|
+
if (raw.length === 0) return null;
|
|
69
|
+
const parts = raw.split("/");
|
|
70
|
+
const stack: string[] = [];
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
if (part === "" || part === ".") continue;
|
|
73
|
+
if (part === "..") {
|
|
74
|
+
if (stack.length === 0) return null;
|
|
75
|
+
stack.pop();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
stack.push(part);
|
|
79
|
+
}
|
|
80
|
+
return stack.length === 0 ? null : stack.join("/");
|
|
81
|
+
}
|
package/src/utils/role/icon.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type { Role } from "../../config/roles";
|
|
|
11
11
|
const MATERIAL_ICON_RE = /^[a-z_]+$/;
|
|
12
12
|
|
|
13
13
|
export function roleIcon(roles: Role[], roleId: string): string {
|
|
14
|
-
const icon = roles.find((
|
|
14
|
+
const icon = roles.find((role) => role.id === roleId)?.icon ?? "star";
|
|
15
15
|
return MATERIAL_ICON_RE.test(icon) ? icon : "smart_toy";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function roleName(roles: Role[], roleId: string): string {
|
|
19
|
-
return roles.find((
|
|
19
|
+
return roles.find((role) => role.id === roleId)?.name ?? roleId;
|
|
20
20
|
}
|
package/src/utils/role/merge.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
import type { Role } from "../../config/roles";
|
|
6
6
|
|
|
7
7
|
export function mergeRoles(builtin: Role[], custom: Role[]): Role[] {
|
|
8
|
-
const customIds = new Set(custom.map((
|
|
9
|
-
return [...builtin.filter((
|
|
8
|
+
const customIds = new Set(custom.map((role) => role.id));
|
|
9
|
+
return [...builtin.filter((role) => !customIds.has(role.id)), ...custom];
|
|
10
10
|
}
|
|
@@ -8,5 +8,5 @@ const GEMINI_PLUGINS = new Set<ToolName>([TOOL_NAMES.generateImage, TOOL_NAMES.p
|
|
|
8
8
|
|
|
9
9
|
/** Whether the given role uses any plugin that requires a Gemini API key. */
|
|
10
10
|
export function needsGemini(roles: Role[], roleId: string): boolean {
|
|
11
|
-
return (roles.find((
|
|
11
|
+
return (roles.find((role) => role.id === roleId)?.availablePlugins ?? []).some((plugin) => GEMINI_PLUGINS.has(plugin));
|
|
12
12
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ActiveSession } from "../../types/session";
|
|
4
4
|
|
|
5
|
-
export function createEmptySession(
|
|
5
|
+
export function createEmptySession(sessionId: string, roleId: string): ActiveSession {
|
|
6
6
|
const now = new Date().toISOString();
|
|
7
7
|
return {
|
|
8
|
-
id,
|
|
8
|
+
id: sessionId,
|
|
9
9
|
roleId,
|
|
10
10
|
toolResults: [],
|
|
11
11
|
resultTimestamps: new Map(),
|