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
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
// Dynamic favicon that changes color based on agent state (#470).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Renders the MulmoClaude mascot on a colored rounded square. The
|
|
4
|
+
// background color reflects state, and the mascot floats on top:
|
|
5
5
|
// idle (gray) → running (blue, pulse) → done (green) → error (red)
|
|
6
6
|
// notification badge (orange dot) overlaid when unread count > 0.
|
|
7
|
+
//
|
|
8
|
+
// The logo PNG has an opaque white background, which would otherwise
|
|
9
|
+
// hide the state color. On first load we pre-process the pixels,
|
|
10
|
+
// punching out near-white pixels to transparency so the colored
|
|
11
|
+
// backing shows through. The processed image is cached as an
|
|
12
|
+
// offscreen canvas for the lifetime of the page.
|
|
13
|
+
//
|
|
14
|
+
// If the logo fails to load we fall back to the earlier "M"-letter
|
|
15
|
+
// variant so the tab icon never disappears entirely.
|
|
7
16
|
|
|
8
17
|
import { watch, type Ref, type ComputedRef } from "vue";
|
|
18
|
+
import logoUrl from "../assets/mulmo_bw.png";
|
|
9
19
|
|
|
10
20
|
export const FAVICON_STATES = {
|
|
11
21
|
idle: "idle",
|
|
@@ -23,9 +33,88 @@ const STATE_COLORS: Record<FaviconState, string> = {
|
|
|
23
33
|
error: "#EF4444", // red-500
|
|
24
34
|
};
|
|
25
35
|
|
|
26
|
-
const NOTIFICATION_DOT_COLOR = "#
|
|
36
|
+
const NOTIFICATION_DOT_COLOR = "#DC2626"; // red-600 — stands out against the gray/blue/green state backgrounds
|
|
27
37
|
const SIZE = 32;
|
|
28
38
|
const RADIUS = 6;
|
|
39
|
+
// How much of the inner rounded square the mascot fills. 2 px of
|
|
40
|
+
// padding on each side keeps it off the rounded corners and leaves
|
|
41
|
+
// room for the colored backing to peek around the outline.
|
|
42
|
+
const MASCOT_INSET = 2;
|
|
43
|
+
|
|
44
|
+
// Pixels whose RGB channels are all above this are treated as the
|
|
45
|
+
// PNG's white backing and punched to transparent. The PNG uses a soft
|
|
46
|
+
// pastel palette so the mascot itself never hits all three channels
|
|
47
|
+
// this high.
|
|
48
|
+
const WHITE_TO_ALPHA_THRESHOLD = 235;
|
|
49
|
+
// Pixels in the [FEATHER_LOW, WHITE_TO_ALPHA_THRESHOLD] band get a
|
|
50
|
+
// partial-alpha ramp so the mascot's anti-aliased outline blends with
|
|
51
|
+
// the colored background instead of showing a hard seam.
|
|
52
|
+
const FEATHER_LOW = 205;
|
|
53
|
+
|
|
54
|
+
// ── Asset loading ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
let logoCanvas: HTMLCanvasElement | null = null;
|
|
57
|
+
let logoLoadFailed = false;
|
|
58
|
+
let logoLoadPromise: Promise<HTMLCanvasElement> | null = null;
|
|
59
|
+
|
|
60
|
+
function loadLogo(): Promise<HTMLCanvasElement> {
|
|
61
|
+
if (logoCanvas) return Promise.resolve(logoCanvas);
|
|
62
|
+
if (logoLoadPromise) return logoLoadPromise;
|
|
63
|
+
logoLoadPromise = decodeAndPunchOutWhite();
|
|
64
|
+
return logoLoadPromise;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function decodeAndPunchOutWhite(): Promise<HTMLCanvasElement> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const img = new Image();
|
|
70
|
+
img.onload = () => {
|
|
71
|
+
try {
|
|
72
|
+
logoCanvas = buildTransparentLogoCanvas(img);
|
|
73
|
+
resolve(logoCanvas);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logoLoadFailed = true;
|
|
76
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
img.onerror = (err) => {
|
|
80
|
+
logoLoadFailed = true;
|
|
81
|
+
reject(err instanceof Error ? err : new Error("favicon logo failed to load"));
|
|
82
|
+
};
|
|
83
|
+
img.src = logoUrl;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Copy the decoded <img> into an offscreen canvas and scan every
|
|
88
|
+
// pixel, replacing near-white with transparency. The PNG is opaque so
|
|
89
|
+
// the background would otherwise cover the state color backing.
|
|
90
|
+
function buildTransparentLogoCanvas(img: HTMLImageElement): HTMLCanvasElement {
|
|
91
|
+
const canvas = document.createElement("canvas");
|
|
92
|
+
canvas.width = img.naturalWidth;
|
|
93
|
+
canvas.height = img.naturalHeight;
|
|
94
|
+
const ctx = canvas.getContext("2d");
|
|
95
|
+
if (!ctx) throw new Error("2d context unavailable");
|
|
96
|
+
ctx.drawImage(img, 0, 0);
|
|
97
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
98
|
+
const pixels = imageData.data;
|
|
99
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
100
|
+
const red = pixels[i];
|
|
101
|
+
const green = pixels[i + 1];
|
|
102
|
+
const blue = pixels[i + 2];
|
|
103
|
+
const minChannel = Math.min(red, green, blue);
|
|
104
|
+
if (minChannel >= WHITE_TO_ALPHA_THRESHOLD) {
|
|
105
|
+
pixels[i + 3] = 0; // fully transparent
|
|
106
|
+
} else if (minChannel >= FEATHER_LOW) {
|
|
107
|
+
// Linear ramp across the feather band. At minChannel = FEATHER_LOW
|
|
108
|
+
// alpha stays 255; at threshold it drops to 0.
|
|
109
|
+
const ratio = (minChannel - FEATHER_LOW) / (WHITE_TO_ALPHA_THRESHOLD - FEATHER_LOW);
|
|
110
|
+
pixels[i + 3] = Math.round(255 * (1 - ratio));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
ctx.putImageData(imageData, 0, 0);
|
|
114
|
+
return canvas;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Drawing primitives ─────────────────────────────────────────
|
|
29
118
|
|
|
30
119
|
function drawRoundedRect(ctx: CanvasRenderingContext2D, posX: number, posY: number, width: number, height: number, radius: number): void {
|
|
31
120
|
ctx.beginPath();
|
|
@@ -41,54 +130,94 @@ function drawRoundedRect(ctx: CanvasRenderingContext2D, posX: number, posY: numb
|
|
|
41
130
|
ctx.closePath();
|
|
42
131
|
}
|
|
43
132
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
133
|
+
// Aspect-preserving letterbox: scale the logo to fit the inner area
|
|
134
|
+
// without distorting the mascot, then center the leftover space.
|
|
135
|
+
function drawLogoCentered(ctx: CanvasRenderingContext2D, source: HTMLCanvasElement, inset: number): void {
|
|
136
|
+
const available = SIZE - inset * 2;
|
|
137
|
+
const aspect = source.width / source.height;
|
|
138
|
+
const drawW = aspect >= 1 ? available : available * aspect;
|
|
139
|
+
const drawH = aspect >= 1 ? available / aspect : available;
|
|
140
|
+
const drawX = inset + (available - drawW) / 2;
|
|
141
|
+
const drawY = inset + (available - drawH) / 2;
|
|
142
|
+
ctx.drawImage(source, drawX, drawY, drawW, drawH);
|
|
143
|
+
}
|
|
50
144
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
145
|
+
function drawNotificationDot(ctx: CanvasRenderingContext2D): void {
|
|
146
|
+
const dotR = 5;
|
|
147
|
+
const dotX = SIZE - dotR - 1;
|
|
148
|
+
const dotY = dotR + 1;
|
|
149
|
+
ctx.beginPath();
|
|
150
|
+
ctx.arc(dotX, dotY, dotR, 0, Math.PI * 2);
|
|
151
|
+
ctx.fillStyle = NOTIFICATION_DOT_COLOR;
|
|
55
152
|
ctx.fill();
|
|
153
|
+
ctx.strokeStyle = "white";
|
|
154
|
+
ctx.lineWidth = 1.5;
|
|
155
|
+
ctx.stroke();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Composition ────────────────────────────────────────────────
|
|
56
159
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
160
|
+
// Fallback for when the logo PNG fails to decode (or before the first
|
|
161
|
+
// decode completes). Mirrors the earlier "M"-on-colored-square design
|
|
162
|
+
// so the favicon always has a valid first paint.
|
|
163
|
+
function renderFallbackFavicon(ctx: CanvasRenderingContext2D, state: FaviconState, hasNotification: boolean): void {
|
|
60
164
|
drawRoundedRect(ctx, 1, 1, SIZE - 2, SIZE - 2, RADIUS);
|
|
61
|
-
ctx.
|
|
165
|
+
ctx.fillStyle = STATE_COLORS[state];
|
|
166
|
+
ctx.fill();
|
|
62
167
|
|
|
63
|
-
// "M" letter
|
|
64
168
|
ctx.fillStyle = "white";
|
|
65
169
|
ctx.font = "bold 20px -apple-system, BlinkMacSystemFont, sans-serif";
|
|
66
170
|
ctx.textAlign = "center";
|
|
67
171
|
ctx.textBaseline = "middle";
|
|
68
172
|
ctx.fillText("M", SIZE / 2, SIZE / 2 + 1);
|
|
69
173
|
|
|
70
|
-
|
|
174
|
+
if (hasNotification) drawNotificationDot(ctx);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderLogoFavicon(ctx: CanvasRenderingContext2D, logo: HTMLCanvasElement, state: FaviconState, hasNotification: boolean): void {
|
|
178
|
+
// Colored rounded-square backing — the dynamic cue.
|
|
179
|
+
drawRoundedRect(ctx, 0, 0, SIZE, SIZE, RADIUS);
|
|
180
|
+
ctx.fillStyle = STATE_COLORS[state];
|
|
181
|
+
ctx.fill();
|
|
182
|
+
|
|
183
|
+
// Clip subsequent draws to the rounded square so the mascot's
|
|
184
|
+
// anti-aliased edges don't spill past the corners.
|
|
185
|
+
ctx.save();
|
|
186
|
+
drawRoundedRect(ctx, 0, 0, SIZE, SIZE, RADIUS);
|
|
187
|
+
ctx.clip();
|
|
188
|
+
drawLogoCentered(ctx, logo, MASCOT_INSET);
|
|
189
|
+
ctx.restore();
|
|
190
|
+
|
|
191
|
+
// Running state: subtle inner glow ring reinforces the pulse cue
|
|
192
|
+
// without overpowering the colored backing.
|
|
71
193
|
if (state === FAVICON_STATES.running) {
|
|
72
|
-
ctx.strokeStyle = "rgba(255,255,255,0.
|
|
73
|
-
ctx.lineWidth =
|
|
74
|
-
drawRoundedRect(ctx,
|
|
194
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.55)";
|
|
195
|
+
ctx.lineWidth = 1.5;
|
|
196
|
+
drawRoundedRect(ctx, 2.25, 2.25, SIZE - 4.5, SIZE - 4.5, Math.max(RADIUS - 1, 2));
|
|
75
197
|
ctx.stroke();
|
|
76
198
|
}
|
|
77
199
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
200
|
+
if (hasNotification) drawNotificationDot(ctx);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function renderFavicon(state: FaviconState, hasNotification: boolean): Promise<string> {
|
|
204
|
+
const canvas = document.createElement("canvas");
|
|
205
|
+
canvas.width = SIZE;
|
|
206
|
+
canvas.height = SIZE;
|
|
207
|
+
const ctx = canvas.getContext("2d");
|
|
208
|
+
if (!ctx) return "";
|
|
209
|
+
|
|
210
|
+
if (!logoLoadFailed) {
|
|
211
|
+
try {
|
|
212
|
+
const logo = await loadLogo();
|
|
213
|
+
renderLogoFavicon(ctx, logo, state, hasNotification);
|
|
214
|
+
return canvas.toDataURL("image/png");
|
|
215
|
+
} catch {
|
|
216
|
+
// fall through — renderFallbackFavicon below handles it.
|
|
217
|
+
}
|
|
90
218
|
}
|
|
91
219
|
|
|
220
|
+
renderFallbackFavicon(ctx, state, hasNotification);
|
|
92
221
|
return canvas.toDataURL("image/png");
|
|
93
222
|
}
|
|
94
223
|
|
|
@@ -106,10 +235,16 @@ function applyFavicon(dataUrl: string): void {
|
|
|
106
235
|
}
|
|
107
236
|
|
|
108
237
|
export function useDynamicFavicon(opts: { state: Ref<FaviconState> | ComputedRef<FaviconState>; hasNotification: Ref<boolean> | ComputedRef<boolean> }): void {
|
|
109
|
-
function update(): void {
|
|
110
|
-
const dataUrl = renderFavicon(opts.state.value, opts.hasNotification.value);
|
|
238
|
+
async function update(): Promise<void> {
|
|
239
|
+
const dataUrl = await renderFavicon(opts.state.value, opts.hasNotification.value);
|
|
111
240
|
applyFavicon(dataUrl);
|
|
112
241
|
}
|
|
113
242
|
|
|
114
|
-
watch(
|
|
243
|
+
watch(
|
|
244
|
+
[opts.state, opts.hasNotification],
|
|
245
|
+
() => {
|
|
246
|
+
update().catch((err) => console.warn("[favicon] render failed", err));
|
|
247
|
+
},
|
|
248
|
+
{ immediate: true },
|
|
249
|
+
);
|
|
115
250
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Composable that wires the window-level event listeners used by
|
|
2
|
-
// App.vue (
|
|
3
|
-
//
|
|
2
|
+
// App.vue (global keydown for navigation + view-mode shortcuts) and
|
|
3
|
+
// tears them down on unmount.
|
|
4
4
|
//
|
|
5
5
|
// Plugin → App.vue communication used to live here too via
|
|
6
6
|
// `roles-updated` / `skill-run` CustomEvents on `window`. That now
|
|
7
7
|
// flows through `useAppApi` (provide/inject) — see #227. Anything
|
|
8
8
|
// remaining in this composable is genuinely a window-level concern
|
|
9
|
-
// (keyboard
|
|
10
|
-
//
|
|
9
|
+
// (keyboard events that don't have a single "owning" component).
|
|
10
|
+
//
|
|
11
|
+
// The click-outside handler for the history popup was dropped when
|
|
12
|
+
// the popup became a real page at /history (see
|
|
13
|
+
// plans/feat-history-url-route.md).
|
|
11
14
|
//
|
|
12
15
|
// Each listener is supplied as an option so the composable stays
|
|
13
16
|
// independent of App.vue's local state; the caller passes the
|
|
@@ -20,8 +23,6 @@ export interface EventListenerHandlers {
|
|
|
20
23
|
onKeyNavigation: (e: KeyboardEvent) => void;
|
|
21
24
|
/** Global keydown for Cmd/Ctrl+1/2/3 view-mode shortcut. */
|
|
22
25
|
onViewModeShortcut: (e: KeyboardEvent) => void;
|
|
23
|
-
/** mousedown click-outside handlers for each popup. */
|
|
24
|
-
onClickOutsideHistory: (e: MouseEvent) => void;
|
|
25
26
|
/** Called in onUnmounted after all window listeners are removed. */
|
|
26
27
|
onTeardown?: () => void;
|
|
27
28
|
}
|
|
@@ -30,13 +31,11 @@ export function useEventListeners(handlers: EventListenerHandlers): void {
|
|
|
30
31
|
onMounted(() => {
|
|
31
32
|
window.addEventListener("keydown", handlers.onKeyNavigation);
|
|
32
33
|
window.addEventListener("keydown", handlers.onViewModeShortcut);
|
|
33
|
-
window.addEventListener("mousedown", handlers.onClickOutsideHistory);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
onUnmounted(() => {
|
|
37
37
|
window.removeEventListener("keydown", handlers.onKeyNavigation);
|
|
38
38
|
window.removeEventListener("keydown", handlers.onViewModeShortcut);
|
|
39
|
-
window.removeEventListener("mousedown", handlers.onClickOutsideHistory);
|
|
40
39
|
handlers.onTeardown?.();
|
|
41
40
|
});
|
|
42
41
|
}
|
|
@@ -10,8 +10,13 @@ export function useFaviconState(opts: {
|
|
|
10
10
|
isRunning: ComputedRef<boolean>;
|
|
11
11
|
currentSummary: ComputedRef<SessionSummary | undefined>;
|
|
12
12
|
activeSession: ComputedRef<ActiveSession | undefined>;
|
|
13
|
+
// Number of sessions (across all tabs) with unread messages. We
|
|
14
|
+
// light the badge dot when any session is unread, even if it's not
|
|
15
|
+
// the currently-focused one, so background replies still surface in
|
|
16
|
+
// the tab bar.
|
|
17
|
+
sessionsUnreadCount: ComputedRef<number>;
|
|
13
18
|
}) {
|
|
14
|
-
const { isRunning, currentSummary, activeSession } = opts;
|
|
19
|
+
const { isRunning, currentSummary, activeSession, sessionsUnreadCount } = opts;
|
|
15
20
|
|
|
16
21
|
const faviconState = computed<FaviconState>(() => {
|
|
17
22
|
if (isRunning.value) return FAVICON_STATES.running;
|
|
@@ -21,7 +26,13 @@ export function useFaviconState(opts: {
|
|
|
21
26
|
});
|
|
22
27
|
|
|
23
28
|
const { unreadCount: notificationUnreadCount } = useNotifications();
|
|
24
|
-
|
|
29
|
+
// Badge dot covers two independent signals:
|
|
30
|
+
// 1. Pub-sub notifications (scheduled tasks, etc.)
|
|
31
|
+
// 2. Any session with unread chat messages (including background
|
|
32
|
+
// tabs the user isn't currently viewing).
|
|
33
|
+
// Either one flips the dot on — the dot doesn't distinguish source,
|
|
34
|
+
// just tells the user "there's something to look at".
|
|
35
|
+
const hasNotificationBadge = computed(() => notificationUnreadCount.value > 0 || sessionsUnreadCount.value > 0);
|
|
25
36
|
|
|
26
37
|
useDynamicFavicon({
|
|
27
38
|
state: faviconState,
|
|
@@ -32,12 +32,27 @@ export function isValidFilePath(value: unknown): value is string {
|
|
|
32
32
|
return !value.split("/").some((seg) => seg === "..");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Extract the logical file path from a route's `pathMatch` param.
|
|
37
|
+
* Vue Router hands the repeatable catch-all back as an array, a
|
|
38
|
+
* single string, or `undefined` depending on what matched — normalise
|
|
39
|
+
* to a `string | null` so the rest of the composable doesn't care.
|
|
40
|
+
*/
|
|
41
|
+
export function readPathMatch(raw: unknown): string | null {
|
|
42
|
+
if (Array.isArray(raw)) {
|
|
43
|
+
if (raw.length === 0) return null;
|
|
44
|
+
return raw.join("/");
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw === "string" && raw.length > 0) return raw;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
export function useFileSelection() {
|
|
36
51
|
const route = useRoute();
|
|
37
52
|
const router = useRouter();
|
|
38
53
|
|
|
39
|
-
const
|
|
40
|
-
const selectedPath = ref<string | null>(isValidFilePath(
|
|
54
|
+
const pathFromRoute = readPathMatch(route.params.pathMatch);
|
|
55
|
+
const selectedPath = ref<string | null>(isValidFilePath(pathFromRoute) ? pathFromRoute : null);
|
|
41
56
|
const content = ref<FileContent | null>(null);
|
|
42
57
|
const contentLoading = ref(false);
|
|
43
58
|
const contentError = ref<string | null>(null);
|
|
@@ -71,8 +86,12 @@ export function useFileSelection() {
|
|
|
71
86
|
function selectFile(filePath: string): void {
|
|
72
87
|
selectedPath.value = filePath;
|
|
73
88
|
loadContent(filePath);
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
// Pass segments as an array so Vue Router encodes each segment
|
|
90
|
+
// independently (spaces / multi-byte / `?#%` get UTF-8 percent-
|
|
91
|
+
// encoding), while slashes stay as path separators. Passing the
|
|
92
|
+
// joined string would urlencode `/` → `%2F` and collapse the
|
|
93
|
+
// visible path shape.
|
|
94
|
+
router.push({ name: "files", params: { pathMatch: filePath.split("/") }, query: route.query }).catch((err: unknown) => {
|
|
76
95
|
if (!isNavigationFailure(err)) {
|
|
77
96
|
// Frontend composable — server logger not available.
|
|
78
97
|
// console.error is the standard pattern in Vue composables.
|
|
@@ -88,8 +107,7 @@ export function useFileSelection() {
|
|
|
88
107
|
content.value = null;
|
|
89
108
|
contentLoading.value = false;
|
|
90
109
|
contentError.value = null;
|
|
91
|
-
|
|
92
|
-
router.replace({ query: restQuery }).catch((err: unknown) => {
|
|
110
|
+
router.replace({ name: "files", params: { pathMatch: [] }, query: route.query }).catch((err: unknown) => {
|
|
93
111
|
if (!isNavigationFailure(err)) {
|
|
94
112
|
console.error("[deselectFile] navigation failed:", err);
|
|
95
113
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Layout preference (single vs. stack) for the /chat page, persisted
|
|
2
|
+
// in localStorage. Independent of which page the user is on — pages
|
|
3
|
+
// like Files/Todos/Wiki live in the router, not here.
|
|
4
|
+
//
|
|
5
|
+
// One-time cleanup: deletes the legacy `canvas_view_mode` key that
|
|
6
|
+
// conflated layout with page navigation. The value is intentionally
|
|
7
|
+
// not migrated — users land on "single" on first load after the
|
|
8
|
+
// split.
|
|
9
|
+
|
|
10
|
+
import { ref, type Ref } from "vue";
|
|
11
|
+
import { LAYOUT_MODES, LAYOUT_MODE_STORAGE_KEY, LEGACY_VIEW_MODE_STORAGE_KEY, parseStoredLayoutMode, type LayoutMode } from "../utils/canvas/layoutMode";
|
|
12
|
+
|
|
13
|
+
export function useLayoutMode(): {
|
|
14
|
+
layoutMode: Ref<LayoutMode>;
|
|
15
|
+
setLayoutMode: (mode: LayoutMode) => void;
|
|
16
|
+
toggleLayoutMode: () => void;
|
|
17
|
+
} {
|
|
18
|
+
localStorage.removeItem(LEGACY_VIEW_MODE_STORAGE_KEY);
|
|
19
|
+
|
|
20
|
+
const layoutMode = ref<LayoutMode>(parseStoredLayoutMode(localStorage.getItem(LAYOUT_MODE_STORAGE_KEY)));
|
|
21
|
+
|
|
22
|
+
function setLayoutMode(mode: LayoutMode): void {
|
|
23
|
+
layoutMode.value = mode;
|
|
24
|
+
localStorage.setItem(LAYOUT_MODE_STORAGE_KEY, mode);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toggleLayoutMode(): void {
|
|
28
|
+
setLayoutMode(layoutMode.value === LAYOUT_MODES.stack ? LAYOUT_MODES.single : LAYOUT_MODES.stack);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { layoutMode, setLayoutMode, toggleLayoutMode };
|
|
32
|
+
}
|
|
@@ -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 {
|
|
@@ -59,7 +59,7 @@ export function usePendingCalls(opts: UsePendingCallsOptions) {
|
|
|
59
59
|
// unused.
|
|
60
60
|
const __tickDep = displayTick.value;
|
|
61
61
|
const now = Date.now();
|
|
62
|
-
return opts.toolCallHistory.value.filter((
|
|
62
|
+
return opts.toolCallHistory.value.filter((entry) => __tickDep >= 0 && isCallStillPending(entry, now));
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
function teardown(): void {
|
|
@@ -21,16 +21,16 @@ let socket: Socket | null = null;
|
|
|
21
21
|
|
|
22
22
|
const listeners = new Map<string, Set<Callback>>();
|
|
23
23
|
|
|
24
|
-
function resendSubscriptions(
|
|
24
|
+
function resendSubscriptions(sock: Socket): void {
|
|
25
25
|
for (const channel of listeners.keys()) {
|
|
26
|
-
|
|
26
|
+
sock.emit("subscribe", channel);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function connect(): Socket {
|
|
31
31
|
if (socket) return socket;
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const sock = io({
|
|
34
34
|
path: "/ws/pubsub",
|
|
35
35
|
// Match the server. Long-polling is fine as a fallback but
|
|
36
36
|
// the server refuses it, so don't negotiate it here either —
|
|
@@ -38,17 +38,17 @@ function connect(): Socket {
|
|
|
38
38
|
transports: ["websocket"],
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
sock.on("connect", () => resendSubscriptions(sock));
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
sock.on("data", (msg: PubSubMessage) => {
|
|
44
44
|
const cbs = listeners.get(msg.channel);
|
|
45
45
|
if (cbs) {
|
|
46
|
-
for (const
|
|
46
|
+
for (const handler of cbs) handler(msg.data);
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
socket =
|
|
51
|
-
return
|
|
50
|
+
socket = sock;
|
|
51
|
+
return sock;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function maybeDisconnect(): void {
|
|
@@ -63,8 +63,8 @@ export function usePubSub() {
|
|
|
63
63
|
if (!listeners.has(channel)) listeners.set(channel, new Set());
|
|
64
64
|
listeners.get(channel)!.add(callback);
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
66
|
+
const sock = connect();
|
|
67
|
+
if (sock.connected) sock.emit("subscribe", channel);
|
|
68
68
|
// If not yet connected, the "connect" handler replays every
|
|
69
69
|
// listener's subscription, so newly-added channels are
|
|
70
70
|
// covered without extra bookkeeping.
|
|
@@ -17,7 +17,7 @@ export function useRoles(): {
|
|
|
17
17
|
} {
|
|
18
18
|
const roles = ref<Role[]>(ROLES);
|
|
19
19
|
const currentRoleId = ref(ROLES[0].id);
|
|
20
|
-
const currentRole = computed(() => roles.value.find((
|
|
20
|
+
const currentRole = computed(() => roles.value.find((role) => role.id === currentRoleId.value) ?? roles.value[0]);
|
|
21
21
|
|
|
22
22
|
async function refreshRoles(): Promise<void> {
|
|
23
23
|
const result = await apiGet<Role[]>(API_ROUTES.roles.list);
|
|
@@ -29,7 +29,7 @@ function isSandboxStatus(raw: RawResponse): raw is {
|
|
|
29
29
|
} {
|
|
30
30
|
if (typeof raw.sshAgent !== "boolean") return false;
|
|
31
31
|
if (!Array.isArray(raw.mounts)) return false;
|
|
32
|
-
return raw.mounts.every((
|
|
32
|
+
return raw.mounts.every((mount) => typeof mount === "string");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface UseSandboxStatusHandle {
|
|
@@ -16,7 +16,7 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
16
16
|
|
|
17
17
|
const sidebarResults = computed(() => deduplicateResults(toolResults.value));
|
|
18
18
|
|
|
19
|
-
const currentSummary = computed(() => sessions.value.find((
|
|
19
|
+
const currentSummary = computed(() => sessions.value.find((summary) => summary.id === currentSessionId.value));
|
|
20
20
|
|
|
21
21
|
// The server-side summary already merges pendingGenerations into
|
|
22
22
|
// `isRunning` (see server/api/routes/sessions.ts), but pub/sub events
|
|
@@ -33,9 +33,9 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
33
33
|
|
|
34
34
|
const toolCallHistory = computed<ToolCallHistoryItem[]>(() => activeSession.value?.toolCallHistory ?? []);
|
|
35
35
|
|
|
36
|
-
const activeSessionCount = computed(() => sessions.value.filter((
|
|
36
|
+
const activeSessionCount = computed(() => sessions.value.filter((session) => session.isRunning).length);
|
|
37
37
|
|
|
38
|
-
const unreadCount = computed(() => sessions.value.filter((
|
|
38
|
+
const unreadCount = computed(() => sessions.value.filter((session) => session.hasUnread).length);
|
|
39
39
|
|
|
40
40
|
return {
|
|
41
41
|
activeSession,
|