mulmoclaude 0.3.0 → 0.5.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-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- 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/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- 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/workspace.ts +7 -7
- package/src/App.vue +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -0
- 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-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -1,38 +1,107 @@
|
|
|
1
1
|
// Composable for the server /api/health probe.
|
|
2
2
|
//
|
|
3
|
-
// Owns
|
|
4
|
-
// toggle) plus
|
|
5
|
-
//
|
|
3
|
+
// Owns three refs that the UI reads (gemini key availability +
|
|
4
|
+
// sandbox toggle + server CPU load ratio) plus a one-shot fetch
|
|
5
|
+
// that populates them on mount, plus an optional periodic refresh
|
|
6
|
+
// for the CPU ratio (the favicon's "overloaded" rule needs a live
|
|
7
|
+
// signal, not a boot-time snapshot).
|
|
8
|
+
//
|
|
9
|
+
// On fetch failure we assume Gemini is unavailable so dependent UI
|
|
6
10
|
// (e.g. the "generate image" plugin buttons) falls back gracefully
|
|
7
11
|
// — the sandbox flag keeps its initial `true` so the lock indicator
|
|
8
|
-
// doesn't momentarily flash "sandbox disabled" on a transient error
|
|
12
|
+
// doesn't momentarily flash "sandbox disabled" on a transient error,
|
|
13
|
+
// and the CPU ratio goes to null so the favicon resolver skips the
|
|
14
|
+
// overloaded rule rather than guessing.
|
|
9
15
|
|
|
10
|
-
import { ref, type Ref } from "vue";
|
|
16
|
+
import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from "vue";
|
|
11
17
|
import { API_ROUTES } from "../config/apiRoutes";
|
|
12
18
|
import { apiGet } from "../utils/api";
|
|
13
19
|
|
|
20
|
+
// Once every 15 s is enough for a sustained load spike to light the
|
|
21
|
+
// favicon. Shorter would mostly flap on short-lived spikes that
|
|
22
|
+
// aren't actually user-visible as lag.
|
|
23
|
+
const HEALTH_REFRESH_MS = 15_000;
|
|
24
|
+
|
|
25
|
+
interface CpuPayload {
|
|
26
|
+
load1?: unknown;
|
|
27
|
+
cores?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
interface HealthResponse {
|
|
15
31
|
geminiAvailable?: unknown;
|
|
16
32
|
sandboxEnabled?: unknown;
|
|
33
|
+
cpu?: CpuPayload;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
export function useHealth(): {
|
|
20
37
|
geminiAvailable: Ref<boolean>;
|
|
21
38
|
sandboxEnabled: Ref<boolean>;
|
|
39
|
+
cpuLoadRatio: ComputedRef<number | null>;
|
|
22
40
|
fetchHealth: () => Promise<void>;
|
|
23
41
|
} {
|
|
24
42
|
const geminiAvailable = ref(true);
|
|
25
43
|
const sandboxEnabled = ref(true);
|
|
44
|
+
const cpuLoad1 = ref<number | null>(null);
|
|
45
|
+
const cpuCores = ref<number | null>(null);
|
|
46
|
+
|
|
47
|
+
// Separate flag so transient poll failures don't flip
|
|
48
|
+
// `geminiAvailable` back to false after a successful boot-time
|
|
49
|
+
// fetch. `geminiAvailable` / `sandboxEnabled` are config-derived
|
|
50
|
+
// and don't change at runtime — once we've observed them once,
|
|
51
|
+
// the next 15 s poll's network blip shouldn't mask them.
|
|
52
|
+
let bootFetchCompleted = false;
|
|
26
53
|
|
|
27
54
|
async function fetchHealth(): Promise<void> {
|
|
28
55
|
const result = await apiGet<HealthResponse>(API_ROUTES.health);
|
|
29
56
|
if (!result.ok) {
|
|
30
|
-
|
|
57
|
+
// Only the CPU figures get nulled — the favicon resolver
|
|
58
|
+
// reads null as "skip overloaded" which is the correct fail-
|
|
59
|
+
// closed behaviour. The config flags keep their last-known
|
|
60
|
+
// values, and stay at the initial defaults if we never
|
|
61
|
+
// succeeded (gemini=true → request lands, gets an auth error
|
|
62
|
+
// handled elsewhere; sandbox=true → lock indicator reads on).
|
|
63
|
+
cpuLoad1.value = null;
|
|
64
|
+
cpuCores.value = null;
|
|
65
|
+
if (!bootFetchCompleted) {
|
|
66
|
+
// On the FIRST fetch we do still flip gemini → false so
|
|
67
|
+
// the "Gemini key required" banner can show immediately
|
|
68
|
+
// without waiting for a second attempt. Subsequent poll
|
|
69
|
+
// failures don't re-enter this branch.
|
|
70
|
+
geminiAvailable.value = false;
|
|
71
|
+
}
|
|
31
72
|
return;
|
|
32
73
|
}
|
|
33
74
|
geminiAvailable.value = !!result.data.geminiAvailable;
|
|
34
75
|
sandboxEnabled.value = !!result.data.sandboxEnabled;
|
|
76
|
+
bootFetchCompleted = true;
|
|
77
|
+
const cpu = result.data.cpu;
|
|
78
|
+
if (cpu && typeof cpu.load1 === "number" && Number.isFinite(cpu.load1) && typeof cpu.cores === "number" && cpu.cores > 0) {
|
|
79
|
+
cpuLoad1.value = cpu.load1;
|
|
80
|
+
cpuCores.value = cpu.cores;
|
|
81
|
+
} else {
|
|
82
|
+
cpuLoad1.value = null;
|
|
83
|
+
cpuCores.value = null;
|
|
84
|
+
}
|
|
35
85
|
}
|
|
36
86
|
|
|
37
|
-
|
|
87
|
+
// Refresh the CPU figure periodically. The flag-style booleans
|
|
88
|
+
// (gemini / sandbox) don't change at runtime so re-fetching them
|
|
89
|
+
// is waste; but piggy-backing on the same endpoint keeps the
|
|
90
|
+
// server side to a single route and the client to a single poll.
|
|
91
|
+
const refreshHandle = window.setInterval(() => {
|
|
92
|
+
fetchHealth().catch(() => {
|
|
93
|
+
/* intentionally swallowed — a failed poll just stalls the
|
|
94
|
+
favicon's overloaded rule, not user-visible UI */
|
|
95
|
+
});
|
|
96
|
+
}, HEALTH_REFRESH_MS);
|
|
97
|
+
onScopeDispose(() => window.clearInterval(refreshHandle));
|
|
98
|
+
|
|
99
|
+
// Expose the normalised ratio the favicon resolver expects (load
|
|
100
|
+
// per logical core). Null when either component is missing.
|
|
101
|
+
const cpuLoadRatio = computed<number | null>(() => {
|
|
102
|
+
if (cpuLoad1.value === null || cpuCores.value === null) return null;
|
|
103
|
+
return cpuLoad1.value / cpuCores.value;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { geminiAvailable, sandboxEnabled, cpuLoadRatio, fetchHealth };
|
|
38
107
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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_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
|
+
} {
|
|
17
|
+
localStorage.removeItem(LEGACY_VIEW_MODE_STORAGE_KEY);
|
|
18
|
+
|
|
19
|
+
const layoutMode = ref<LayoutMode>(parseStoredLayoutMode(localStorage.getItem(LAYOUT_MODE_STORAGE_KEY)));
|
|
20
|
+
|
|
21
|
+
function setLayoutMode(mode: LayoutMode): void {
|
|
22
|
+
layoutMode.value = mode;
|
|
23
|
+
localStorage.setItem(LAYOUT_MODE_STORAGE_KEY, mode);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { layoutMode, setLayoutMode };
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Composable: aggregate recent news items via /api/news/items.
|
|
2
|
+
// Mirrors the server-side `NewsItem` shape from
|
|
3
|
+
// `server/workspace/news/reader.ts`. Re-declared here so the
|
|
4
|
+
// frontend doesn't pull a server import.
|
|
5
|
+
|
|
6
|
+
import { ref } from "vue";
|
|
7
|
+
import { API_ROUTES } from "../config/apiRoutes";
|
|
8
|
+
import { apiGet } from "../utils/api";
|
|
9
|
+
|
|
10
|
+
export interface NewsItem {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
url: string;
|
|
14
|
+
publishedAt: string;
|
|
15
|
+
categories: string[];
|
|
16
|
+
sourceSlug: string;
|
|
17
|
+
severity?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useNewsItems() {
|
|
21
|
+
const items = ref<NewsItem[]>([]);
|
|
22
|
+
const loading = ref(false);
|
|
23
|
+
const error = ref<string | null>(null);
|
|
24
|
+
|
|
25
|
+
async function load(days = 30): Promise<void> {
|
|
26
|
+
loading.value = true;
|
|
27
|
+
error.value = null;
|
|
28
|
+
const result = await apiGet<{ items: NewsItem[] }>(`${API_ROUTES.news.items}?days=${days}`);
|
|
29
|
+
loading.value = false;
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
error.value = result.error;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
items.value = result.data.items;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { items, loading, error, load };
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Composable: own the news viewer's per-item read flags. The server
|
|
2
|
+
// persists the list as `config/news-read-state.json`; the composable
|
|
3
|
+
// keeps a `Set<string>` for O(1) lookup and a queue of pending writes
|
|
4
|
+
// so a fast click sequence doesn't pile up overlapping PUTs.
|
|
5
|
+
|
|
6
|
+
import { ref, computed } from "vue";
|
|
7
|
+
import { API_ROUTES } from "../config/apiRoutes";
|
|
8
|
+
import { apiGet, apiPut } from "../utils/api";
|
|
9
|
+
|
|
10
|
+
export function useNewsReadState() {
|
|
11
|
+
const readIds = ref(new Set<string>());
|
|
12
|
+
const error = ref<string | null>(null);
|
|
13
|
+
|
|
14
|
+
// Single in-flight chain — successive markRead / markAllRead calls
|
|
15
|
+
// queue rather than overlap. Keeps the server's view consistent
|
|
16
|
+
// with the most recent intent.
|
|
17
|
+
let inflight: Promise<unknown> = Promise.resolve();
|
|
18
|
+
|
|
19
|
+
async function load(): Promise<void> {
|
|
20
|
+
const result = await apiGet<{ readIds: string[] }>(API_ROUTES.news.readState);
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
error.value = result.error;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
error.value = null;
|
|
26
|
+
readIds.value = new Set(result.data.readIds);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function persist(): Promise<void> {
|
|
30
|
+
const snapshot = Array.from(readIds.value);
|
|
31
|
+
const task = inflight
|
|
32
|
+
.catch(() => undefined)
|
|
33
|
+
.then(async () => {
|
|
34
|
+
const result = await apiPut<{ readIds: string[] }>(API_ROUTES.news.readState, { readIds: snapshot });
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
error.value = result.error;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
error.value = null;
|
|
40
|
+
// Reflect the server's sanitized list so dedupe / cap come back.
|
|
41
|
+
readIds.value = new Set(result.data.readIds);
|
|
42
|
+
});
|
|
43
|
+
inflight = task;
|
|
44
|
+
return task;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function markRead(itemId: string): void {
|
|
48
|
+
if (readIds.value.has(itemId)) return;
|
|
49
|
+
readIds.value.add(itemId);
|
|
50
|
+
// Trigger reactivity — `Set` mutation isn't reactive on its own.
|
|
51
|
+
readIds.value = new Set(readIds.value);
|
|
52
|
+
void persist();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function markAllRead(allIds: readonly string[]): void {
|
|
56
|
+
let changed = false;
|
|
57
|
+
for (const itemId of allIds) {
|
|
58
|
+
if (!readIds.value.has(itemId)) {
|
|
59
|
+
readIds.value.add(itemId);
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!changed) return;
|
|
64
|
+
readIds.value = new Set(readIds.value);
|
|
65
|
+
void persist();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRead(itemId: string): boolean {
|
|
69
|
+
return readIds.value.has(itemId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const readCount = computed(() => readIds.value.size);
|
|
73
|
+
|
|
74
|
+
return { readIds, error, load, markRead, markAllRead, isRead, readCount };
|
|
75
|
+
}
|
|
@@ -4,18 +4,27 @@
|
|
|
4
4
|
// Uses a singleton subscription pattern: the first component that
|
|
5
5
|
// calls useNotifications() subscribes to the pub-sub channel; the
|
|
6
6
|
// last one to unmount unsubscribes. All consumers share the same
|
|
7
|
-
// module-level state (notifications +
|
|
7
|
+
// module-level state (notifications + readIds).
|
|
8
|
+
//
|
|
9
|
+
// Read tracking is per-id via a Set. The unread badge decreases
|
|
10
|
+
// only when the user **interacts** with a notification — either
|
|
11
|
+
// clicking it (markRead) or dismissing it via × (dismiss removes
|
|
12
|
+
// the notification entirely, so it leaves the unread tally as a
|
|
13
|
+
// side effect). Opening the panel does NOT auto-mark everything
|
|
14
|
+
// read; the user has to explicitly act on each item, or hit the
|
|
15
|
+
// "Mark all read" button.
|
|
8
16
|
|
|
9
17
|
import { onUnmounted, ref, computed, type Ref, type ComputedRef } from "vue";
|
|
10
18
|
import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
|
|
11
19
|
import { usePubSub } from "./usePubSub";
|
|
12
|
-
import { NOTIFICATION_KINDS } from "../types/notification";
|
|
20
|
+
import { NOTIFICATION_ACTION_TYPES, NOTIFICATION_KINDS, NOTIFICATION_VIEWS } from "../types/notification";
|
|
13
21
|
import type { NotificationPayload } from "../types/notification";
|
|
14
22
|
import { isRecord } from "../utils/types";
|
|
15
23
|
|
|
16
24
|
const MAX_RECENT = 50;
|
|
17
25
|
|
|
18
26
|
const VALID_KINDS = new Set<string>(Object.values(NOTIFICATION_KINDS));
|
|
27
|
+
const VALID_VIEWS = new Set<string>(Object.values(NOTIFICATION_VIEWS));
|
|
19
28
|
|
|
20
29
|
function isNotificationPayload(value: unknown): value is NotificationPayload {
|
|
21
30
|
if (!isRecord(value)) return false;
|
|
@@ -27,14 +36,25 @@ function isNotificationPayload(value: unknown): value is NotificationPayload {
|
|
|
27
36
|
return true;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
// Tighter than a plain `typeof type === "string"` check — confirms
|
|
40
|
+
// the discriminator is one we know AND, for `navigate`, that the
|
|
41
|
+
// target carries a known view. Stops malformed payloads from
|
|
42
|
+
// landing in the panel and crashing later in the click handler.
|
|
30
43
|
function isValidAction(action: unknown): boolean {
|
|
31
44
|
if (!isRecord(action)) return false;
|
|
32
|
-
|
|
45
|
+
if (action.type === NOTIFICATION_ACTION_TYPES.none) return true;
|
|
46
|
+
if (action.type !== NOTIFICATION_ACTION_TYPES.navigate) return false;
|
|
47
|
+
const target = action.target;
|
|
48
|
+
if (!isRecord(target)) return false;
|
|
49
|
+
return typeof target.view === "string" && VALID_VIEWS.has(target.view);
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
// Module-level state so all components share the same list
|
|
52
|
+
// Module-level state so all components share the same list and the
|
|
53
|
+
// same per-id read state.
|
|
36
54
|
const notifications = ref<NotificationPayload[]>([]);
|
|
37
|
-
|
|
55
|
+
// Set of notification ids the user has explicitly read (clicked or
|
|
56
|
+
// dismissed-as-read). A Set so add/lookup are O(1) per entry.
|
|
57
|
+
const readIds = ref<Set<string>>(new Set());
|
|
38
58
|
|
|
39
59
|
// Singleton subscription — ref-counted across consumers.
|
|
40
60
|
let subscriberCount = 0;
|
|
@@ -45,10 +65,29 @@ function ensureSubscribed(subscribe: ReturnType<typeof usePubSub>["subscribe"]):
|
|
|
45
65
|
if (unsubscribeFn) return; // already listening
|
|
46
66
|
unsubscribeFn = subscribe(PUBSUB_CHANNELS.notifications, (data) => {
|
|
47
67
|
if (!isNotificationPayload(data)) return;
|
|
48
|
-
|
|
68
|
+
const next = [data, ...notifications.value].slice(0, MAX_RECENT);
|
|
69
|
+
notifications.value = next;
|
|
70
|
+
// Drop read-state entries for notifications that just rolled
|
|
71
|
+
// off the end of the bounded list — readIds is otherwise an
|
|
72
|
+
// unbounded leak across a long-lived session.
|
|
73
|
+
pruneReadIds(next);
|
|
49
74
|
});
|
|
50
75
|
}
|
|
51
76
|
|
|
77
|
+
function pruneReadIds(currentList: readonly NotificationPayload[]): void {
|
|
78
|
+
if (readIds.value.size === 0) return;
|
|
79
|
+
const liveIds = new Set(currentList.map((notif) => notif.id));
|
|
80
|
+
const next = new Set<string>();
|
|
81
|
+
for (const readId of readIds.value) {
|
|
82
|
+
if (liveIds.has(readId)) next.add(readId);
|
|
83
|
+
}
|
|
84
|
+
// Only assign when the contents actually changed — avoids
|
|
85
|
+
// unnecessary reactive churn when nothing rolled off.
|
|
86
|
+
if (next.size !== readIds.value.size) {
|
|
87
|
+
readIds.value = next;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
function releaseSubscription(): void {
|
|
53
92
|
subscriberCount--;
|
|
54
93
|
if (subscriberCount <= 0 && unsubscribeFn) {
|
|
@@ -62,6 +101,8 @@ export function useNotifications(): {
|
|
|
62
101
|
notifications: Ref<NotificationPayload[]>;
|
|
63
102
|
latest: ComputedRef<NotificationPayload | null>;
|
|
64
103
|
unreadCount: ComputedRef<number>;
|
|
104
|
+
isRead: (id: string) => boolean;
|
|
105
|
+
markRead: (id: string) => void;
|
|
65
106
|
markAllRead: () => void;
|
|
66
107
|
dismiss: (id: string) => void;
|
|
67
108
|
} {
|
|
@@ -71,20 +112,42 @@ export function useNotifications(): {
|
|
|
71
112
|
|
|
72
113
|
const latest = computed(() => notifications.value[0] ?? null);
|
|
73
114
|
|
|
74
|
-
const unreadCount = computed(() =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
const unreadCount = computed(() => notifications.value.filter((notif) => !readIds.value.has(notif.id)).length);
|
|
116
|
+
|
|
117
|
+
function isRead(notifId: string): boolean {
|
|
118
|
+
return readIds.value.has(notifId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function markRead(notifId: string): void {
|
|
122
|
+
if (readIds.value.has(notifId)) return;
|
|
123
|
+
// Replace the Set so Vue's reactivity fires on consumers that
|
|
124
|
+
// depend on `readIds` via `unreadCount` / `isRead`.
|
|
125
|
+
const next = new Set(readIds.value);
|
|
126
|
+
next.add(notifId);
|
|
127
|
+
readIds.value = next;
|
|
128
|
+
}
|
|
78
129
|
|
|
79
130
|
function markAllRead(): void {
|
|
80
|
-
if (notifications.value.length
|
|
81
|
-
|
|
131
|
+
if (notifications.value.length === 0) return;
|
|
132
|
+
const next = new Set(readIds.value);
|
|
133
|
+
for (const notif of notifications.value) {
|
|
134
|
+
next.add(notif.id);
|
|
82
135
|
}
|
|
136
|
+
readIds.value = next;
|
|
83
137
|
}
|
|
84
138
|
|
|
85
139
|
function dismiss(notifId: string): void {
|
|
86
140
|
notifications.value = notifications.value.filter((notif) => notif.id !== notifId);
|
|
141
|
+
// Drop the matching readIds entry too. Without this, a long
|
|
142
|
+
// session that dismisses thousands of notifications leaks one
|
|
143
|
+
// ~36-char id per dismissal even though the user can't see
|
|
144
|
+
// them — pruneReadIds keeps the Set tied to `notifications`.
|
|
145
|
+
if (readIds.value.has(notifId)) {
|
|
146
|
+
const next = new Set(readIds.value);
|
|
147
|
+
next.delete(notifId);
|
|
148
|
+
readIds.value = next;
|
|
149
|
+
}
|
|
87
150
|
}
|
|
88
151
|
|
|
89
|
-
return { notifications, latest, unreadCount, markAllRead, dismiss };
|
|
152
|
+
return { notifications, latest, unreadCount, isRead, markRead, markAllRead, dismiss };
|
|
90
153
|
}
|
|
@@ -59,7 +59,17 @@ export function usePendingCalls(opts: UsePendingCallsOptions) {
|
|
|
59
59
|
// unused.
|
|
60
60
|
const __tickDep = displayTick.value;
|
|
61
61
|
const now = Date.now();
|
|
62
|
-
|
|
62
|
+
// Project to a narrower shape that carries `elapsedMs` so the
|
|
63
|
+
// consumer doesn't need its own ticker for the per-tool badge —
|
|
64
|
+
// the 50ms re-evaluation here already drives the display
|
|
65
|
+
// (#731 PR2).
|
|
66
|
+
return opts.toolCallHistory.value
|
|
67
|
+
.filter((entry) => __tickDep >= 0 && isCallStillPending(entry, now))
|
|
68
|
+
.map((entry) => ({
|
|
69
|
+
toolUseId: entry.toolUseId,
|
|
70
|
+
toolName: entry.toolName,
|
|
71
|
+
elapsedMs: now - entry.timestamp,
|
|
72
|
+
}));
|
|
63
73
|
});
|
|
64
74
|
|
|
65
75
|
function teardown(): void {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Composable that owns the active role list
|
|
2
|
-
// selected role
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Composable that owns the active role list and its server-merge
|
|
2
|
+
// fetch. The selected role is owned by SessionHeaderControls via
|
|
3
|
+
// useCurrentRole — selection is a UI-local concern and lives next
|
|
4
|
+
// to the dropdown that drives it.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { ref, type Ref } from "vue";
|
|
7
7
|
import { API_ROUTES } from "../config/apiRoutes";
|
|
8
8
|
import { ROLES, type Role } from "../config/roles";
|
|
9
9
|
import { mergeRoles } from "../utils/role/merge";
|
|
@@ -11,13 +11,9 @@ import { apiGet } from "../utils/api";
|
|
|
11
11
|
|
|
12
12
|
export function useRoles(): {
|
|
13
13
|
roles: Ref<Role[]>;
|
|
14
|
-
currentRoleId: Ref<string>;
|
|
15
|
-
currentRole: ComputedRef<Role>;
|
|
16
14
|
refreshRoles: () => Promise<void>;
|
|
17
15
|
} {
|
|
18
16
|
const roles = ref<Role[]>(ROLES);
|
|
19
|
-
const currentRoleId = ref(ROLES[0].id);
|
|
20
|
-
const currentRole = computed(() => roles.value.find((role) => role.id === currentRoleId.value) ?? roles.value[0]);
|
|
21
17
|
|
|
22
18
|
async function refreshRoles(): Promise<void> {
|
|
23
19
|
const result = await apiGet<Role[]>(API_ROUTES.roles.list);
|
|
@@ -30,5 +26,5 @@ export function useRoles(): {
|
|
|
30
26
|
roles.value = mergeRoles(ROLES, result.data);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
return { roles,
|
|
29
|
+
return { roles, refreshRoles };
|
|
34
30
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Tracks how long the active agent run has been going. While
|
|
2
|
+
// `isRunning` is true, `elapsedMs` updates once per second so the
|
|
3
|
+
// rendered string ("12s" / "1m 23s") moves visibly. When the run
|
|
4
|
+
// ends, `elapsedMs` flips back to null and the timer is cleared.
|
|
5
|
+
//
|
|
6
|
+
// Separated from `usePendingCalls` (which ticks every 50ms for the
|
|
7
|
+
// minimum-visible-duration trick) — the run-elapsed display only
|
|
8
|
+
// needs second-granularity, and the consumer renders one badge per
|
|
9
|
+
// run rather than one per pending row, so a tighter tick would just
|
|
10
|
+
// burn re-renders.
|
|
11
|
+
//
|
|
12
|
+
// Why a watcher + setInterval rather than a `requestAnimationFrame`
|
|
13
|
+
// driven computed: tab-throttled rAF freezes when the tab is in the
|
|
14
|
+
// background, and the user expects the elapsed counter to keep
|
|
15
|
+
// running across tab switches.
|
|
16
|
+
|
|
17
|
+
import { computed, ref, watch, type ComputedRef, type Ref, type WatchStopHandle } from "vue";
|
|
18
|
+
|
|
19
|
+
const ONE_SECOND_MS = 1000;
|
|
20
|
+
|
|
21
|
+
interface UseRunElapsedOptions {
|
|
22
|
+
isRunning: ComputedRef<boolean> | Ref<boolean>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useRunElapsed(opts: UseRunElapsedOptions): {
|
|
26
|
+
elapsedMs: ComputedRef<number | null>;
|
|
27
|
+
teardown: () => void;
|
|
28
|
+
} {
|
|
29
|
+
const startedAt = ref<number | null>(null);
|
|
30
|
+
const now = ref(0);
|
|
31
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
32
|
+
let stopWatch: WatchStopHandle | null = null;
|
|
33
|
+
|
|
34
|
+
stopWatch = watch(
|
|
35
|
+
opts.isRunning,
|
|
36
|
+
(running) => {
|
|
37
|
+
if (running) {
|
|
38
|
+
// Guard against double-start: if the watcher fires twice with
|
|
39
|
+
// running=true (e.g. immediate + a synchronous flip), don't
|
|
40
|
+
// stack a second interval.
|
|
41
|
+
if (interval !== null) return;
|
|
42
|
+
startedAt.value = Date.now();
|
|
43
|
+
now.value = startedAt.value;
|
|
44
|
+
interval = setInterval(() => {
|
|
45
|
+
now.value = Date.now();
|
|
46
|
+
}, ONE_SECOND_MS);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (interval !== null) {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
interval = null;
|
|
52
|
+
}
|
|
53
|
+
startedAt.value = null;
|
|
54
|
+
},
|
|
55
|
+
// immediate so a composable created while a run is already in
|
|
56
|
+
// flight (mounted mid-stream) starts ticking right away.
|
|
57
|
+
{ immediate: true },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const elapsedMs = computed<number | null>(() => {
|
|
61
|
+
if (startedAt.value === null) return null;
|
|
62
|
+
return now.value - startedAt.value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function teardown(): void {
|
|
66
|
+
// Stop the watcher first — otherwise an isRunning flip after
|
|
67
|
+
// teardown would recreate the interval (Codex iter-1 #798).
|
|
68
|
+
if (stopWatch !== null) {
|
|
69
|
+
stopWatch();
|
|
70
|
+
stopWatch = null;
|
|
71
|
+
}
|
|
72
|
+
if (interval !== null) {
|
|
73
|
+
clearInterval(interval);
|
|
74
|
+
interval = null;
|
|
75
|
+
}
|
|
76
|
+
startedAt.value = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { elapsedMs, teardown };
|
|
80
|
+
}
|
|
@@ -18,12 +18,27 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
18
18
|
|
|
19
19
|
const currentSummary = computed(() => sessions.value.find((summary) => summary.id === currentSessionId.value));
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// for
|
|
24
|
-
//
|
|
25
|
-
//
|
|
21
|
+
// Global "is anything running" across every known session — in-memory
|
|
22
|
+
// map (which reflects pub/sub events faster than server refetch) and
|
|
23
|
+
// server-side summaries (for sessions not yet hydrated into the map).
|
|
24
|
+
// Used for consumers that must stay true across page navigation:
|
|
25
|
+
// favicon spinner and the FilesView refresh watcher (which would
|
|
26
|
+
// otherwise fire before a background run actually finishes, because
|
|
27
|
+
// leaving /chat drops activeSession to undefined).
|
|
26
28
|
const isRunning = computed(() => {
|
|
29
|
+
for (const session of sessionMap.values()) {
|
|
30
|
+
if (session.isRunning) return true;
|
|
31
|
+
if (Object.keys(session.pendingGenerations).length > 0) return true;
|
|
32
|
+
}
|
|
33
|
+
return sessions.value.some((summary) => summary.isRunning);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// True only when the session on screen has a run in flight. Drives
|
|
37
|
+
// UX touchpoints that should react per-session — ChatInput disable,
|
|
38
|
+
// sendMessage guard, chat-list auto-scroll, pending-call row tick —
|
|
39
|
+
// so a background run in session B doesn't disable the composer
|
|
40
|
+
// while the user is actively chatting in session A.
|
|
41
|
+
const activeSessionRunning = computed(() => {
|
|
27
42
|
const active = activeSession.value;
|
|
28
43
|
const pending = active ? Object.keys(active.pendingGenerations).length > 0 : false;
|
|
29
44
|
return currentSummary.value?.isRunning || active?.isRunning || pending || false;
|
|
@@ -43,6 +58,7 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
43
58
|
sidebarResults,
|
|
44
59
|
currentSummary,
|
|
45
60
|
isRunning,
|
|
61
|
+
activeSessionRunning,
|
|
46
62
|
statusMessage,
|
|
47
63
|
toolCallHistory,
|
|
48
64
|
activeSessionCount,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Composable for the session-history
|
|
1
|
+
// Composable for the session-history view at `/history`.
|
|
2
2
|
//
|
|
3
|
-
// Owns the `sessions` list (what the server knows about)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Owns the `sessions` list (what the server knows about) plus the
|
|
4
|
+
// fetch helper. The view's open/closed state is now URL-backed (see
|
|
5
|
+
// plans/done/feat-history-url-route.md) — callers watch `route.name` and
|
|
6
|
+
// invoke `fetchSessions()` on route enter rather than going through
|
|
7
|
+
// an in-memory toggle flag.
|
|
8
8
|
//
|
|
9
9
|
// Since #205, `fetchSessions()` sends the server's last-issued
|
|
10
10
|
// cursor back as `?since=<cursor>` so the server can reply with
|
|
@@ -26,15 +26,12 @@ interface SessionsResponse {
|
|
|
26
26
|
|
|
27
27
|
export function useSessionHistory(): {
|
|
28
28
|
sessions: Ref<SessionSummary[]>;
|
|
29
|
-
showHistory: Ref<boolean>;
|
|
30
29
|
historyError: Ref<string | null>;
|
|
31
30
|
fetchSessions: () => Promise<SessionSummary[]>;
|
|
32
|
-
toggleHistory: () => Promise<void>;
|
|
33
31
|
} {
|
|
34
32
|
const sessions = ref<SessionSummary[]>([]);
|
|
35
|
-
const showHistory = ref(false);
|
|
36
33
|
// Surfaces the most recent fetch failure. Kept alongside the (stale)
|
|
37
|
-
// sessions list rather than wiping it — a
|
|
34
|
+
// sessions list rather than wiping it — a panel that goes blank
|
|
38
35
|
// the moment the network hiccups is worse UX than one that shows
|
|
39
36
|
// "⚠ using cached list" with the last-known good entries.
|
|
40
37
|
const historyError = ref<string | null>(null);
|
|
@@ -66,16 +63,9 @@ export function useSessionHistory(): {
|
|
|
66
63
|
return sessions.value;
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
async function toggleHistory(): Promise<void> {
|
|
70
|
-
showHistory.value = !showHistory.value;
|
|
71
|
-
if (showHistory.value) await fetchSessions();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
66
|
return {
|
|
75
67
|
sessions,
|
|
76
|
-
showHistory,
|
|
77
68
|
historyError,
|
|
78
69
|
fetchSessions,
|
|
79
|
-
toggleHistory,
|
|
80
70
|
};
|
|
81
71
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// localStorage-backed ref for the session-history side-panel flag.
|
|
2
|
+
// Mirrors the shape of useLayoutMode — one source of truth per chat
|
|
3
|
+
// UI preference, kept out of App.vue's already-large state surface.
|
|
4
|
+
|
|
5
|
+
import { ref, type Ref } from "vue";
|
|
6
|
+
import { SIDE_PANEL_VISIBLE_STORAGE_KEY, parseStoredSidePanelVisible, serializeSidePanelVisible } from "../utils/canvas/sidePanelVisible";
|
|
7
|
+
|
|
8
|
+
export function useSidePanelVisible(): {
|
|
9
|
+
sidePanelVisible: Ref<boolean>;
|
|
10
|
+
setSidePanelVisible: (value: boolean) => void;
|
|
11
|
+
toggleSidePanelVisible: () => void;
|
|
12
|
+
} {
|
|
13
|
+
const sidePanelVisible = ref<boolean>(parseStoredSidePanelVisible(localStorage.getItem(SIDE_PANEL_VISIBLE_STORAGE_KEY)));
|
|
14
|
+
|
|
15
|
+
function setSidePanelVisible(value: boolean): void {
|
|
16
|
+
sidePanelVisible.value = value;
|
|
17
|
+
localStorage.setItem(SIDE_PANEL_VISIBLE_STORAGE_KEY, serializeSidePanelVisible(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toggleSidePanelVisible(): void {
|
|
21
|
+
setSidePanelVisible(!sidePanelVisible.value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { sidePanelVisible, setSidePanelVisible, toggleSidePanelVisible };
|
|
25
|
+
}
|