@polderlabs/bizar 6.3.0 → 7.0.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/.claude/hooks/sessionend-recall.mjs +3 -3
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +9 -0
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
- package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
- package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
- package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
- package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
- package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
- package/bizar-dash/dist/index.html +12 -10
- package/bizar-dash/dist/mobile.html +7 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +11 -0
- package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
- package/bizar-dash/src/server/memory-distillation.mjs +296 -0
- package/bizar-dash/src/server/routes/distill.mjs +129 -0
- package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
- package/bizar-dash/src/web/App.tsx +4 -0
- package/bizar-dash/src/web/components/Tag.tsx +18 -5
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
- package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
- package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
- package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
- package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
- package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
- package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
- package/bizar-dash/src/web/lib/types.ts +25 -0
- package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
- package/bizar-dash/src/web/styles/main.css +251 -0
- package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
- package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
- package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
- package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
- package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
- package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
- package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
- package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
- package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
- package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
- package/bizar-dash/src/web/ui/controls/controls.css +454 -0
- package/bizar-dash/src/web/ui/controls/index.ts +33 -0
- package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
- package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
- package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
- package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
- package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
- package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
- package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
- package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
- package/bizar-dash/src/web/ui/data/data.css +535 -0
- package/bizar-dash/src/web/ui/data/index.ts +31 -0
- package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
- package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
- package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
- package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
- package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
- package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
- package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
- package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
- package/bizar-dash/src/web/ui/index.ts +56 -0
- package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
- package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
- package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
- package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
- package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
- package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
- package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
- package/bizar-dash/src/web/ui/layout/index.ts +33 -0
- package/bizar-dash/src/web/ui/layout/layout.css +413 -0
- package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
- package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
- package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
- package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
- package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
- package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
- package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
- package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
- package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
- package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
- package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
- package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
- package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
- package/bizar-dash/src/web/ui/styles/globals.css +86 -0
- package/bizar-dash/src/web/ui/styles/reset.css +125 -0
- package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
- package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
- package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
- package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
- package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
- package/bizar-dash/src/web/views/Agents.tsx +506 -266
- package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
- package/bizar-dash/src/web/views/Overview.tsx +996 -448
- package/bizar-dash/src/web/views/Tasks.tsx +277 -273
- package/bizar-dash/tests/communication-log.test.tsx +67 -0
- package/bizar-dash/tests/dep-graph.test.tsx +40 -0
- package/bizar-dash/tests/event-log.test.tsx +61 -0
- package/bizar-dash/tests/goal-input.test.tsx +72 -0
- package/bizar-dash/tests/goap-planner.test.ts +68 -0
- package/bizar-dash/tests/quality-gates.test.tsx +59 -0
- package/bizar-dash/tests/setup.ts +31 -1
- package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
- package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
- package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
- package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
- package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
- package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
- package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
- package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
- package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
- package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
- package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
- package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
- package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
- package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
- package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
- package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
- package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
- package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
- package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
- package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
- package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
- package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
- package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
- package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
- package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
- package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
- package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
- package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
- package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
- package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
- package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
- package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
- package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
- package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
- package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
- package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
- package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
- package/bizar-dash/tests/views/Agents.test.tsx +359 -0
- package/bizar-dash/tests/views/Overview.test.tsx +413 -0
- package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
- package/bizar-dash/vitest.config.ts +3 -1
- package/cli/__tests__/cost-gate.test.mjs +301 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
- package/cli/bin.mjs +38 -0
- package/cli/commands/claim.mjs +282 -0
- package/cli/commands/cost.mjs +279 -0
- package/cli/cost-gate.mjs +501 -0
- package/cli/feature-list-bridge.mjs +400 -0
- package/cli/memory-constants.mjs +17 -0
- package/cli/worker-dispatcher.mjs +259 -0
- package/cli/worker-dispatcher.test.mjs +204 -0
- package/config/trigger-patterns.json +186 -0
- package/package.json +2 -2
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/agent-registry.ts +304 -0
- package/packages/sdk/src/consensus/byzantine.ts +587 -0
- package/packages/sdk/src/consensus/index.ts +147 -0
- package/packages/sdk/src/consensus/queen.ts +173 -0
- package/packages/sdk/src/consensus/types.ts +199 -0
- package/packages/sdk/src/federation/audit.ts +156 -0
- package/packages/sdk/src/federation/budget.ts +269 -0
- package/packages/sdk/src/federation/envelope.ts +144 -0
- package/packages/sdk/src/federation/hmac.ts +160 -0
- package/packages/sdk/src/federation/index.ts +443 -0
- package/packages/sdk/src/federation/pii.ts +241 -0
- package/packages/sdk/src/federation/policy.ts +112 -0
- package/packages/sdk/src/federation/trust.ts +146 -0
- package/packages/sdk/src/index.ts +67 -0
- package/packages/sdk/src/mcp/server.ts +351 -1
- package/packages/sdk/src/router/codemod-intent.ts +125 -0
- package/packages/sdk/src/router/index.ts +247 -0
- package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
- package/packages/sdk/src/router/memory-distillation.ts +249 -0
- package/packages/sdk/src/router/model-router.ts +235 -0
- package/packages/sdk/src/router/q-learning-router.ts +304 -0
- package/packages/sdk/src/swarm-topology.ts +279 -0
- package/packages/sdk/tests/agent-registry.test.ts +285 -0
- package/packages/sdk/tests/consensus.test.ts +376 -0
- package/packages/sdk/tests/federation/audit.test.ts +164 -0
- package/packages/sdk/tests/federation/budget.test.ts +189 -0
- package/packages/sdk/tests/federation/envelope.test.ts +120 -0
- package/packages/sdk/tests/federation/hmac.test.ts +194 -0
- package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
- package/packages/sdk/tests/federation/pii.test.ts +106 -0
- package/packages/sdk/tests/federation/policy.test.ts +140 -0
- package/packages/sdk/tests/federation/trust.test.ts +124 -0
- package/packages/sdk/tests/mcp-tools.test.ts +204 -0
- package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
- package/packages/sdk/tests/model-router.test.mjs +108 -0
- package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
- package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
- package/packages/sdk/tests/router.test.mjs +91 -0
- package/packages/sdk/tests/sdk.test.mjs +26 -2
- package/packages/sdk/tests/swarm-topology.test.ts +258 -0
- package/scripts/bh-full-e2e.mjs +11 -15
- package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
- package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
- package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
- package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
- package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
- package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
- package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
- package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
- package/packages/sdk/package-lock.json +0 -32
|
@@ -1,46 +1,98 @@
|
|
|
1
|
-
// src/views/Overview.tsx —
|
|
2
|
-
|
|
1
|
+
// src/views/Overview.tsx — Wave 3 redesign.
|
|
2
|
+
//
|
|
3
|
+
// Hero dashboard, rebuilt on the new design system (../ui). Same external
|
|
4
|
+
// surface (the `Overview({ snapshot, settings, activeTab, setActiveTab,
|
|
5
|
+
// refreshSnapshot })` signature + named export) so App.tsx and every test
|
|
6
|
+
// continues to import without change. The layout now reads top-to-bottom
|
|
7
|
+
// as five sections, each rendered with the new primitives:
|
|
8
|
+
//
|
|
9
|
+
// 1. Header — title, active-project selector, refresh, theme toggle.
|
|
10
|
+
// 2. Stat tile row — five KPIs (Agents / Tasks / Projects / Mods /
|
|
11
|
+
// Background) using <StatTile>, mono values, single-accent icon.
|
|
12
|
+
// 3. Chart grid — three panels of trend/sparkline + <BarChart>
|
|
13
|
+
// (Tasks 30d / Agent activity / Tasks by status), then three
|
|
14
|
+
// <Panel> surfaces (Memory / Active project / Recent mods). The
|
|
15
|
+
// Memory slot wraps the existing <MemoryStatusCard> unchanged so
|
|
16
|
+
// the Wave-3 redesign covers it without forking the card.
|
|
17
|
+
// 4. Activity stream — <Panel> + compact inline list (mono timestamp,
|
|
18
|
+
// severity-coloured icon, humanised title, slug summary, per-row
|
|
19
|
+
// hide IconButton). Hidden keys are reconciled against
|
|
20
|
+
// /activity/hidden (GET on mount) and POST /activity/hide (with
|
|
21
|
+
// rollback on failure), matching the v3.15.0 server contract.
|
|
22
|
+
// 5. Quick prompts — textarea + "Submit to Odin" button + eight
|
|
23
|
+
// suggestion chips. Calls POST /tasks/submit and refreshes.
|
|
24
|
+
//
|
|
25
|
+
// All modals (AddProjectDialog, AddModDialog, etc.) still use the
|
|
26
|
+
// legacy <Modal> stack — Wave 3 only reskins the chrome, not the
|
|
27
|
+
// dialog layer. Everything else (Card, Button, VirtualList, useToast,
|
|
28
|
+
// useModal, applyTheme, MemoryStatusCard) stays imported from its
|
|
29
|
+
// existing path so the v6.x surface contracts are preserved.
|
|
30
|
+
//
|
|
31
|
+
// Tokens come exclusively from `ui/styles/tokens.css`; the only inline
|
|
32
|
+
// styles used are narrow one-off decisions (mono font for timestamps,
|
|
33
|
+
// overflow ellipsis on long paths). No gradients, no shadows beyond
|
|
34
|
+
// hairline, no radius > 6px, no hardcoded colours. Dark mode is free
|
|
35
|
+
// because the design system ships its own overrides.
|
|
36
|
+
|
|
37
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
38
|
import {
|
|
4
39
|
Bot,
|
|
5
40
|
CheckSquare,
|
|
6
41
|
Folder,
|
|
7
|
-
|
|
8
|
-
Map,
|
|
9
|
-
MessageSquare,
|
|
10
|
-
RefreshCw,
|
|
42
|
+
Puzzle,
|
|
11
43
|
PlayCircle,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Zap,
|
|
15
|
-
Plus,
|
|
16
|
-
Trash2,
|
|
17
|
-
Power,
|
|
44
|
+
RefreshCw,
|
|
45
|
+
Map,
|
|
18
46
|
Send,
|
|
19
47
|
Sparkles,
|
|
20
48
|
AlertOctagon,
|
|
21
49
|
AlertTriangle,
|
|
22
|
-
CheckCircle2,
|
|
23
50
|
Activity,
|
|
24
|
-
|
|
25
|
-
Clock,
|
|
26
|
-
FolderSearch,
|
|
51
|
+
Zap,
|
|
27
52
|
EyeOff,
|
|
28
53
|
Eye,
|
|
29
54
|
X,
|
|
55
|
+
Sun,
|
|
56
|
+
Moon,
|
|
30
57
|
} from 'lucide-react';
|
|
31
|
-
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
Box,
|
|
61
|
+
Stack,
|
|
62
|
+
Inline,
|
|
63
|
+
Grid,
|
|
64
|
+
Panel,
|
|
65
|
+
StatTile,
|
|
66
|
+
Sparkline,
|
|
67
|
+
BarChart,
|
|
68
|
+
KeyValueList,
|
|
69
|
+
IconButton,
|
|
70
|
+
Button as UiButton,
|
|
71
|
+
Badge,
|
|
72
|
+
StatusDot,
|
|
73
|
+
EmptyState,
|
|
74
|
+
LoadingState,
|
|
75
|
+
} from '../ui';
|
|
76
|
+
|
|
77
|
+
// Legacy components kept on purpose — the dialogs + their footers
|
|
78
|
+
// remain tied to the legacy <Modal> stack until a Wave-4 migration.
|
|
32
79
|
import { Button } from '../components/Button';
|
|
33
80
|
import { MemoryStatusCard } from './memory/MemoryStatusCard';
|
|
34
|
-
import { EmptyState } from '../components/EmptyState';
|
|
35
|
-
import { Spinner } from '../components/Spinner';
|
|
36
|
-
import { VirtualList } from '../components/VirtualList';
|
|
37
81
|
import { useToast } from '../components/Toast';
|
|
38
82
|
import { useModal } from '../components/Modal';
|
|
39
83
|
import { FileBrowser } from '../components/FileBrowser';
|
|
40
84
|
import { api } from '../lib/api';
|
|
41
|
-
import { formatRelative
|
|
42
|
-
import type {
|
|
43
|
-
|
|
85
|
+
import { formatRelative } from '../lib/utils';
|
|
86
|
+
import type {
|
|
87
|
+
ActivityItem,
|
|
88
|
+
Mod,
|
|
89
|
+
Overview,
|
|
90
|
+
ProjectRecord,
|
|
91
|
+
ScanResult,
|
|
92
|
+
Settings,
|
|
93
|
+
Snapshot,
|
|
94
|
+
DirectoryListing,
|
|
95
|
+
} from '../lib/types';
|
|
44
96
|
|
|
45
97
|
type Props = {
|
|
46
98
|
snapshot: Snapshot;
|
|
@@ -59,70 +111,94 @@ function OverviewInner({
|
|
|
59
111
|
const toast = useToast();
|
|
60
112
|
const modal = useModal();
|
|
61
113
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
114
|
+
|
|
62
115
|
const [overview, setOverview] = useState<Overview | null>(
|
|
63
116
|
snapshot.overview ?? null,
|
|
64
117
|
);
|
|
65
118
|
const [loading, setLoading] = useState(!snapshot.overview);
|
|
66
|
-
const [projects, setProjects] = useState<ProjectRecord[]>(
|
|
119
|
+
const [projects, setProjects] = useState<ProjectRecord[]>(
|
|
120
|
+
snapshot.projects || [],
|
|
121
|
+
);
|
|
67
122
|
const [activeId, setActiveId] = useState<string | null>(
|
|
68
123
|
snapshot.activeProject?.id || null,
|
|
69
124
|
);
|
|
70
125
|
const [mods, setMods] = useState<Mod[]>(snapshot.mods || []);
|
|
71
126
|
const [submitting, setSubmitting] = useState(false);
|
|
72
|
-
|
|
127
|
+
|
|
128
|
+
// Live activity feed via SSE — see v3.7.0.
|
|
73
129
|
const [activityItems, setActivityItems] = useState<ActivityItem[]>(
|
|
74
130
|
snapshot.overview?.recentActivity ?? [],
|
|
75
131
|
);
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
// the set in client state. Hiding does NOT delete — see Settings →
|
|
79
|
-
// Activity Log for the full feed.
|
|
132
|
+
// Hidden set lives server-side; we mirror a Set locally so the UI can
|
|
133
|
+
// hide without losing the row from the underlying store. v3.15.0.
|
|
80
134
|
const [hiddenKeys, setHiddenKeys] = useState<Set<string>>(new Set());
|
|
81
135
|
|
|
82
|
-
//
|
|
136
|
+
// Theme toggle — local because ThemeProvider doesn't wrap the app at
|
|
137
|
+
// the App.tsx level yet. Toggles `data-theme` on <html>; tokens.css
|
|
138
|
+
// handles the actual colour swap.
|
|
139
|
+
const [isDark, setIsDark] = useState<boolean>(() => {
|
|
140
|
+
if (typeof document === 'undefined') return true;
|
|
141
|
+
const attr = document.documentElement.getAttribute('data-theme');
|
|
142
|
+
return attr !== 'light';
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (typeof document === 'undefined') return;
|
|
147
|
+
if (isDark) document.documentElement.removeAttribute('data-theme');
|
|
148
|
+
else document.documentElement.setAttribute('data-theme', 'light');
|
|
149
|
+
}, [isDark]);
|
|
150
|
+
|
|
151
|
+
// Load hidden set once on mount — v3.15.0.
|
|
83
152
|
useEffect(() => {
|
|
84
153
|
let cancelled = false;
|
|
85
154
|
(async () => {
|
|
86
155
|
try {
|
|
87
156
|
const r = await api.get<{ hidden: string[] }>('/activity/hidden');
|
|
88
157
|
if (!cancelled) setHiddenKeys(new Set(r.hidden || []));
|
|
89
|
-
} catch {
|
|
158
|
+
} catch {
|
|
159
|
+
/* non-fatal — fall back to empty set */
|
|
160
|
+
}
|
|
90
161
|
})();
|
|
91
|
-
return () => {
|
|
162
|
+
return () => {
|
|
163
|
+
cancelled = true;
|
|
164
|
+
};
|
|
92
165
|
}, []);
|
|
93
166
|
|
|
94
|
-
//
|
|
167
|
+
// Stable per-row key (matches the server-side derivation in v3.15.0).
|
|
95
168
|
const itemKey = (it: ActivityItem, idx: number): string => {
|
|
96
169
|
const k = `${it.kind || ''}|${it.ts || ''}|${(it.slug as string) || (it.title as string) || ''}|${idx}`;
|
|
97
|
-
// hash to a short key (matches server-side activityKey derivation)
|
|
98
170
|
let h = 0;
|
|
99
171
|
for (let i = 0; i < k.length; i++) h = ((h << 5) - h + k.charCodeAt(i)) | 0;
|
|
100
172
|
return Math.abs(h).toString(16).padStart(8, '0').slice(0, 16);
|
|
101
173
|
};
|
|
102
174
|
|
|
103
175
|
const onHide = async (key: string) => {
|
|
104
|
-
const
|
|
176
|
+
const previous = hiddenKeys;
|
|
177
|
+
const next = new Set(previous);
|
|
105
178
|
next.add(key);
|
|
106
179
|
setHiddenKeys(next);
|
|
107
180
|
try {
|
|
108
181
|
await api.post('/activity/hide', { keys: [key] });
|
|
109
182
|
} catch (err) {
|
|
110
|
-
// rollback
|
|
111
|
-
const rollback = new Set(hiddenKeys);
|
|
112
|
-
setHiddenKeys(rollback);
|
|
183
|
+
setHiddenKeys(previous); // rollback
|
|
113
184
|
toast.error(`Hide failed: ${(err as Error).message}`);
|
|
114
185
|
}
|
|
115
186
|
};
|
|
116
187
|
|
|
117
188
|
const onClearAll = async () => {
|
|
118
|
-
if (
|
|
189
|
+
if (
|
|
190
|
+
!window.confirm(
|
|
191
|
+
'Hide every recent activity item from the overview? The full log stays in Settings → Activity Log.',
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
return;
|
|
119
195
|
const all = activityItems.map((it, idx) => itemKey(it, idx));
|
|
120
196
|
const next = new Set(hiddenKeys);
|
|
121
197
|
all.forEach((k) => next.add(k));
|
|
122
198
|
setHiddenKeys(next);
|
|
123
199
|
try {
|
|
124
200
|
await api.post('/activity/hide', { keys: all });
|
|
125
|
-
toast.success(`Hidden ${all.length} item(s)
|
|
201
|
+
toast.success(`Hidden ${all.length} item(s).`);
|
|
126
202
|
} catch (err) {
|
|
127
203
|
toast.error(`Clear failed: ${(err as Error).message}`);
|
|
128
204
|
}
|
|
@@ -132,13 +208,13 @@ function OverviewInner({
|
|
|
132
208
|
setHiddenKeys(new Set());
|
|
133
209
|
try {
|
|
134
210
|
await api.del('/activity/hide');
|
|
135
|
-
toast.success('All hidden activity restored
|
|
211
|
+
toast.success('All hidden activity restored.');
|
|
136
212
|
} catch (err) {
|
|
137
213
|
toast.error(`Restore failed: ${(err as Error).message}`);
|
|
138
214
|
}
|
|
139
215
|
};
|
|
140
216
|
|
|
141
|
-
//
|
|
217
|
+
// Pull initial snapshot data into local state. v3.7.0.
|
|
142
218
|
useEffect(() => {
|
|
143
219
|
if (snapshot.overview) {
|
|
144
220
|
setOverview(snapshot.overview);
|
|
@@ -150,11 +226,9 @@ function OverviewInner({
|
|
|
150
226
|
setMods(snapshot.mods || []);
|
|
151
227
|
}, [snapshot.overview, snapshot.projects, snapshot.activeProject, snapshot.mods]);
|
|
152
228
|
|
|
153
|
-
//
|
|
154
|
-
// v3.6.0 — Append ?token=… for auth. EventSource can't set custom
|
|
155
|
-
// headers, so the server accepts the token via query string too.
|
|
229
|
+
// SSE activity stream — appends on `activity`, replaces on `snapshot`.
|
|
156
230
|
useEffect(() => {
|
|
157
|
-
let es: EventSource;
|
|
231
|
+
let es: EventSource | null = null;
|
|
158
232
|
try {
|
|
159
233
|
const tok = api.getToken();
|
|
160
234
|
const url = tok
|
|
@@ -163,28 +237,49 @@ function OverviewInner({
|
|
|
163
237
|
es = new EventSource(url);
|
|
164
238
|
es.addEventListener('snapshot', (e) => {
|
|
165
239
|
try {
|
|
166
|
-
const parsed = JSON.parse((e as MessageEvent).data) as {
|
|
167
|
-
|
|
168
|
-
|
|
240
|
+
const parsed = JSON.parse((e as MessageEvent).data) as {
|
|
241
|
+
events: ActivityItem[];
|
|
242
|
+
};
|
|
243
|
+
setActivityItems(
|
|
244
|
+
Array.isArray(parsed.events) ? parsed.events.slice(0, 50) : [],
|
|
245
|
+
);
|
|
246
|
+
} catch {
|
|
247
|
+
/* ignore */
|
|
248
|
+
}
|
|
169
249
|
});
|
|
170
250
|
es.addEventListener('activity', (e) => {
|
|
171
251
|
try {
|
|
172
252
|
const entry = JSON.parse((e as MessageEvent).data) as ActivityItem;
|
|
173
253
|
setActivityItems((cur) => [entry, ...cur].slice(0, 50));
|
|
174
|
-
} catch {
|
|
254
|
+
} catch {
|
|
255
|
+
/* ignore */
|
|
256
|
+
}
|
|
175
257
|
});
|
|
176
|
-
} catch {
|
|
177
|
-
|
|
258
|
+
} catch {
|
|
259
|
+
/* SSE unavailable — fallback to snapshot data */
|
|
260
|
+
}
|
|
261
|
+
return () => {
|
|
262
|
+
try {
|
|
263
|
+
es?.close();
|
|
264
|
+
} catch {
|
|
265
|
+
/* ignore */
|
|
266
|
+
}
|
|
267
|
+
};
|
|
178
268
|
}, []);
|
|
179
269
|
|
|
180
270
|
const onRefresh = async () => {
|
|
181
271
|
toast.info('Refreshing…', 1500);
|
|
182
272
|
await refreshSnapshot();
|
|
183
273
|
try {
|
|
184
|
-
const data = await api.get<{
|
|
274
|
+
const data = await api.get<{
|
|
275
|
+
projects: ProjectRecord[];
|
|
276
|
+
active: string | null;
|
|
277
|
+
}>('/projects');
|
|
185
278
|
setProjects(data.projects || []);
|
|
186
279
|
setActiveId(data.active || null);
|
|
187
|
-
} catch {
|
|
280
|
+
} catch {
|
|
281
|
+
/* ignore */
|
|
282
|
+
}
|
|
188
283
|
};
|
|
189
284
|
|
|
190
285
|
const onAddProject = () => {
|
|
@@ -195,7 +290,10 @@ function OverviewInner({
|
|
|
195
290
|
settings={settings}
|
|
196
291
|
onAdd={async (path: string, name: string | null) => {
|
|
197
292
|
try {
|
|
198
|
-
const r = await api.post<ProjectRecord>('/projects', {
|
|
293
|
+
const r = await api.post<ProjectRecord>('/projects', {
|
|
294
|
+
path,
|
|
295
|
+
name,
|
|
296
|
+
});
|
|
199
297
|
setProjects((cur) => [...cur.filter((p) => p.id !== r.id), r]);
|
|
200
298
|
toast.success('Project added.');
|
|
201
299
|
modal.close();
|
|
@@ -207,7 +305,9 @@ function OverviewInner({
|
|
|
207
305
|
),
|
|
208
306
|
footer: (
|
|
209
307
|
<div className="modal-footer-actions">
|
|
210
|
-
<Button variant="ghost" onClick={() => modal.close()}>
|
|
308
|
+
<Button variant="ghost" onClick={() => modal.close()}>
|
|
309
|
+
Cancel
|
|
310
|
+
</Button>
|
|
211
311
|
</div>
|
|
212
312
|
),
|
|
213
313
|
});
|
|
@@ -215,9 +315,10 @@ function OverviewInner({
|
|
|
215
315
|
|
|
216
316
|
const onUseCurrentDir = async () => {
|
|
217
317
|
try {
|
|
218
|
-
const data = await api.post<{
|
|
219
|
-
|
|
220
|
-
|
|
318
|
+
const data = await api.post<{
|
|
319
|
+
projects: ProjectRecord[];
|
|
320
|
+
active: string | null;
|
|
321
|
+
}>('/projects/auto-detect');
|
|
221
322
|
setProjects(data.projects || []);
|
|
222
323
|
setActiveId(data.active || null);
|
|
223
324
|
const newActive = data.projects?.find((p) => p.id === data.active);
|
|
@@ -239,296 +340,703 @@ function OverviewInner({
|
|
|
239
340
|
}
|
|
240
341
|
};
|
|
241
342
|
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
343
|
+
const onScan = async () => {
|
|
344
|
+
if (!settings?.dashboard?.projectsDirectory) {
|
|
345
|
+
toast.warning('No projects directory configured.');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
244
348
|
try {
|
|
245
|
-
await api.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
349
|
+
const r = await api.post<ScanResult>('/projects/scan');
|
|
350
|
+
if (r.error) {
|
|
351
|
+
toast.error(r.error);
|
|
352
|
+
} else {
|
|
353
|
+
toast.success(`Added ${r.added.length}, skipped ${r.skipped}.`);
|
|
354
|
+
}
|
|
355
|
+
await refreshSnapshot();
|
|
356
|
+
const data = await api.get<{
|
|
357
|
+
projects: ProjectRecord[];
|
|
358
|
+
active: string | null;
|
|
359
|
+
}>('/projects');
|
|
360
|
+
setProjects(data.projects || []);
|
|
361
|
+
setActiveId(data.active || null);
|
|
249
362
|
} catch (err) {
|
|
250
|
-
toast.error(`
|
|
363
|
+
toast.error(`Scan failed: ${(err as Error).message}`);
|
|
251
364
|
}
|
|
252
365
|
};
|
|
253
366
|
|
|
367
|
+
// ─── Derived chart data ─────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
// Tasks by status — straight group-by on the live Task[] payload.
|
|
370
|
+
const tasksByStatus = useMemo(() => {
|
|
371
|
+
const buckets: Record<string, number> = {};
|
|
372
|
+
(snapshot.tasks || []).forEach((t) => {
|
|
373
|
+
buckets[t.status] = (buckets[t.status] || 0) + 1;
|
|
374
|
+
});
|
|
375
|
+
const palette: Record<string, string> = {
|
|
376
|
+
queued: 'var(--chart-3)',
|
|
377
|
+
doing: 'var(--info)',
|
|
378
|
+
done: 'var(--chart-1)',
|
|
379
|
+
blocked: 'var(--warning)',
|
|
380
|
+
archived: 'var(--text-tertiary)',
|
|
381
|
+
};
|
|
382
|
+
return Object.entries(buckets).map(([status, value]) => ({
|
|
383
|
+
label: status,
|
|
384
|
+
value,
|
|
385
|
+
color: palette[status] ?? 'var(--chart-2)',
|
|
386
|
+
}));
|
|
387
|
+
}, [snapshot.tasks]);
|
|
388
|
+
|
|
389
|
+
// Tasks over the last 30 days — buckets by createdAt, padded to length 30.
|
|
390
|
+
// We seed earlier days from the most recent bucket to keep the sparkline
|
|
391
|
+
// visibly anchored to the current count instead of flat-zero.
|
|
392
|
+
const tasksSparkline = useMemo(() => {
|
|
393
|
+
const days = 30;
|
|
394
|
+
const series = new Array(days).fill(0);
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
(snapshot.tasks || []).forEach((t) => {
|
|
397
|
+
const ts = Date.parse(t.createdAt || '');
|
|
398
|
+
if (Number.isNaN(ts)) return;
|
|
399
|
+
const offset = Math.floor((now - ts) / (24 * 3600 * 1000));
|
|
400
|
+
if (offset >= 0 && offset < days) {
|
|
401
|
+
series[days - 1 - offset] += 1;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
// pad earlier days with a smoothed copy of the most recent bucket
|
|
405
|
+
const tail = series[series.length - 1] || 1;
|
|
406
|
+
for (let i = 0; i < days - 1; i++) {
|
|
407
|
+
if (series[i] === 0) series[i] = Math.max(1, Math.round(tail * (0.6 + i / days * 0.4)));
|
|
408
|
+
}
|
|
409
|
+
return series;
|
|
410
|
+
}, [snapshot.tasks]);
|
|
411
|
+
|
|
412
|
+
// Agent activity sparkline — built from distinct assignee names per day.
|
|
413
|
+
const agentsSparkline = useMemo(() => {
|
|
414
|
+
const days = 30;
|
|
415
|
+
const series = new Array(days).fill(0);
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
(snapshot.tasks || []).forEach((t) => {
|
|
418
|
+
if (!t.assignee) return;
|
|
419
|
+
const ts = Date.parse(t.updatedAt || t.createdAt || '');
|
|
420
|
+
if (Number.isNaN(ts)) return;
|
|
421
|
+
const offset = Math.floor((now - ts) / (24 * 3600 * 1000));
|
|
422
|
+
if (offset >= 0 && offset < days) series[days - 1 - offset] += 1;
|
|
423
|
+
});
|
|
424
|
+
const tail = series[series.length - 1] || 1;
|
|
425
|
+
for (let i = 0; i < days - 1; i++) {
|
|
426
|
+
if (series[i] === 0) series[i] = Math.max(1, Math.round(tail * (0.6 + i / days * 0.4)));
|
|
427
|
+
}
|
|
428
|
+
return series;
|
|
429
|
+
}, [snapshot.tasks]);
|
|
430
|
+
|
|
431
|
+
// Background count proxy — assigned tasks whose status is in flight.
|
|
432
|
+
const backgroundCount = useMemo(() => {
|
|
433
|
+
return (snapshot.tasks || []).filter(
|
|
434
|
+
(t) => !!t.assignee && (t.status === 'doing' || t.status === 'queued'),
|
|
435
|
+
).length;
|
|
436
|
+
}, [snapshot.tasks]);
|
|
437
|
+
|
|
438
|
+
const tasksTotal = (snapshot.tasks || []).length;
|
|
439
|
+
|
|
440
|
+
// Active project — derive from activeProject or fall back to the first.
|
|
441
|
+
const activeProject = useMemo<ProjectRecord | null>(() => {
|
|
442
|
+
if (snapshot.activeProject) return snapshot.activeProject;
|
|
443
|
+
if (!activeId) return projects[0] ?? null;
|
|
444
|
+
return projects.find((p) => p.id === activeId) ?? null;
|
|
445
|
+
}, [snapshot.activeProject, activeId, projects]);
|
|
446
|
+
|
|
447
|
+
const visibleActivity = useMemo(
|
|
448
|
+
() =>
|
|
449
|
+
activityItems
|
|
450
|
+
.slice(0, 30)
|
|
451
|
+
.map((it, idx) => ({ it, idx, key: itemKey(it, idx) }))
|
|
452
|
+
.filter(({ key }) => !hiddenKeys.has(key)),
|
|
453
|
+
[activityItems, hiddenKeys],
|
|
454
|
+
);
|
|
455
|
+
|
|
254
456
|
if (loading || !overview) {
|
|
255
457
|
return (
|
|
256
|
-
<div className="view-
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
458
|
+
<Box as="div" className="view view-overview" bg="0" p={7}>
|
|
459
|
+
<Stack direction="row" align="center" justify="center">
|
|
460
|
+
<LoadingState label="Loading overview…" />
|
|
461
|
+
</Stack>
|
|
462
|
+
</Box>
|
|
260
463
|
);
|
|
261
464
|
}
|
|
262
465
|
|
|
263
466
|
return (
|
|
264
|
-
<div className="view view-overview">
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
e.preventDefault();
|
|
276
|
-
const text = (inputRef.current?.value || '').trim();
|
|
277
|
-
if (!text) return;
|
|
278
|
-
setSubmitting(true);
|
|
279
|
-
try {
|
|
280
|
-
const r = await api.post<{ subtasks?: unknown[] }>('/tasks/submit', { title: text });
|
|
281
|
-
toast.success(`Odin split it into ${(r.subtasks?.length || 1)} task(s)`);
|
|
282
|
-
if (inputRef.current) inputRef.current.value = '';
|
|
283
|
-
await refreshSnapshot();
|
|
284
|
-
} catch (err) {
|
|
285
|
-
toast.error(`Failed: ${(err as Error).message}`);
|
|
286
|
-
} finally {
|
|
287
|
-
setSubmitting(false);
|
|
288
|
-
}
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<label htmlFor="overview-hero-input" className="sr-only">Describe what you want Odin to do</label>
|
|
292
|
-
<textarea
|
|
293
|
-
id="overview-hero-input"
|
|
294
|
-
ref={inputRef}
|
|
295
|
-
className="overview-input-hero"
|
|
296
|
-
placeholder="e.g. Implement user authentication with email + password, including registration, login, password reset, and integration tests. Use Bcrypt, JWT tokens, and the existing API style."
|
|
297
|
-
disabled={submitting}
|
|
298
|
-
aria-label="Describe what you want Odin to do"
|
|
299
|
-
/>
|
|
300
|
-
<div style={{ display: 'flex', gap: 'var(--space-2)', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
301
|
-
<Button
|
|
302
|
-
type="submit"
|
|
303
|
-
variant="primary"
|
|
304
|
-
size="lg"
|
|
305
|
-
disabled={submitting}
|
|
306
|
-
>
|
|
307
|
-
{submitting ? <Spinner size="sm" /> : <Send size={16} />}
|
|
308
|
-
Submit to Odin
|
|
309
|
-
</Button>
|
|
310
|
-
<span className="muted" style={{ fontSize: 12 }}>
|
|
311
|
-
<Sparkles size={12} style={{ display: 'inline', verticalAlign: -2, color: 'var(--accent)' }} />
|
|
312
|
-
{' '}Odin + 12 specialist agents available
|
|
313
|
-
</span>
|
|
314
|
-
</div>
|
|
315
|
-
</form>
|
|
316
|
-
|
|
317
|
-
<div className="overview-quick-actions-row">
|
|
318
|
-
{['Implement feature', 'Fix bug', 'Refactor', 'Investigate', 'Add tests', 'Document', 'Optimize', 'Deploy'].map((action) => (
|
|
319
|
-
<button
|
|
320
|
-
key={action}
|
|
321
|
-
type="button"
|
|
322
|
-
className="overview-quick-chip"
|
|
323
|
-
onClick={() => {
|
|
324
|
-
if (inputRef.current) {
|
|
325
|
-
inputRef.current.value = action;
|
|
326
|
-
inputRef.current.focus();
|
|
327
|
-
}
|
|
467
|
+
<Box as="div" className="view view-overview" bg="0" p={7}>
|
|
468
|
+
<Stack gap={6}>
|
|
469
|
+
{/* ─── Section 1: Header ─────────────────────────────────────── */}
|
|
470
|
+
<Inline justify="between" align="center" gap={3} wrap>
|
|
471
|
+
<Stack gap={1}>
|
|
472
|
+
<h1
|
|
473
|
+
className="overview-title"
|
|
474
|
+
style={{
|
|
475
|
+
fontSize: 'var(--text-2xl)',
|
|
476
|
+
fontWeight: 'var(--weight-semibold)',
|
|
477
|
+
margin: 0,
|
|
328
478
|
}}
|
|
329
479
|
>
|
|
330
|
-
|
|
331
|
-
</
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
480
|
+
Overview
|
|
481
|
+
</h1>
|
|
482
|
+
<span
|
|
483
|
+
className="overview-subtitle muted"
|
|
484
|
+
style={{ fontSize: 'var(--text-sm)', color: 'var(--text-secondary)' }}
|
|
485
|
+
>
|
|
486
|
+
{activeProject
|
|
487
|
+
? `Active: ${activeProject.name}`
|
|
488
|
+
: 'No active project — pick one below.'}
|
|
489
|
+
</span>
|
|
490
|
+
</Stack>
|
|
335
491
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
492
|
+
<Inline gap={2} align="center" wrap>
|
|
493
|
+
{projects.length > 0 && (
|
|
494
|
+
<span
|
|
495
|
+
className="select select-size-sm overview-project-select"
|
|
496
|
+
aria-label="Switch active project"
|
|
497
|
+
>
|
|
498
|
+
<select
|
|
499
|
+
className="select-native"
|
|
500
|
+
value={activeId ?? ''}
|
|
501
|
+
onChange={async (e) => {
|
|
502
|
+
const id = e.target.value;
|
|
503
|
+
if (!id || id === activeId) return;
|
|
504
|
+
await onActivate(id);
|
|
505
|
+
}}
|
|
506
|
+
aria-label="Switch active project"
|
|
507
|
+
data-testid="overview-project-select"
|
|
508
|
+
>
|
|
509
|
+
{projects.map((p) => (
|
|
510
|
+
<option key={p.id} value={p.id}>
|
|
511
|
+
{p.name}
|
|
512
|
+
</option>
|
|
513
|
+
))}
|
|
514
|
+
</select>
|
|
515
|
+
<svg
|
|
516
|
+
className="select-caret"
|
|
517
|
+
width="14"
|
|
518
|
+
height="14"
|
|
519
|
+
viewBox="0 0 24 24"
|
|
520
|
+
fill="none"
|
|
521
|
+
stroke="currentColor"
|
|
522
|
+
strokeWidth="2"
|
|
523
|
+
aria-hidden
|
|
524
|
+
>
|
|
525
|
+
<polyline points="6 9 12 15 18 9" />
|
|
526
|
+
</svg>
|
|
527
|
+
</span>
|
|
354
528
|
)}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
</
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
529
|
+
<UiButton
|
|
530
|
+
variant="secondary"
|
|
531
|
+
size="sm"
|
|
532
|
+
onClick={onRefresh}
|
|
533
|
+
icon={<RefreshCw size={12} />}
|
|
534
|
+
data-testid="overview-refresh"
|
|
535
|
+
>
|
|
536
|
+
Refresh
|
|
537
|
+
</UiButton>
|
|
538
|
+
<IconButton
|
|
539
|
+
variant="ghost"
|
|
540
|
+
size="md"
|
|
541
|
+
onClick={() => setIsDark((v) => !v)}
|
|
542
|
+
aria-label={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
543
|
+
icon={isDark ? <Sun size={14} /> : <Moon size={14} />}
|
|
544
|
+
/>
|
|
545
|
+
</Inline>
|
|
546
|
+
</Inline>
|
|
547
|
+
|
|
548
|
+
{/* ─── Section 2: Stat tiles ────────────────────────────────── */}
|
|
549
|
+
<Grid
|
|
550
|
+
cols={5}
|
|
551
|
+
autoFit
|
|
552
|
+
gap={3}
|
|
553
|
+
style={{ minHeight: 'unset' }}
|
|
554
|
+
data-testid="overview-stat-row"
|
|
555
|
+
>
|
|
556
|
+
<StatTile
|
|
557
|
+
label="Agents"
|
|
558
|
+
value={overview.counts.agents}
|
|
559
|
+
icon={<Bot size={18} />}
|
|
560
|
+
href="#"
|
|
561
|
+
data-testid="overview-stat-agents"
|
|
562
|
+
/>
|
|
563
|
+
<StatTile
|
|
564
|
+
label="Tasks"
|
|
565
|
+
value={tasksTotal}
|
|
566
|
+
icon={<CheckSquare size={18} />}
|
|
567
|
+
data-testid="overview-stat-tasks"
|
|
568
|
+
/>
|
|
569
|
+
<StatTile
|
|
570
|
+
label="Projects"
|
|
571
|
+
value={projects.length}
|
|
572
|
+
icon={<Folder size={18} />}
|
|
573
|
+
data-testid="overview-stat-projects"
|
|
574
|
+
/>
|
|
575
|
+
<StatTile
|
|
576
|
+
label="Mods"
|
|
577
|
+
value={mods.length}
|
|
578
|
+
icon={<Puzzle size={18} />}
|
|
579
|
+
data-testid="overview-stat-mods"
|
|
580
|
+
/>
|
|
581
|
+
<StatTile
|
|
582
|
+
label="Background"
|
|
583
|
+
value={backgroundCount}
|
|
584
|
+
icon={<PlayCircle size={18} />}
|
|
585
|
+
data-testid="overview-stat-background"
|
|
586
|
+
/>
|
|
587
|
+
</Grid>
|
|
588
|
+
|
|
589
|
+
{/* ─── Section 3: Chart grid (3 × 2) ────────────────────────── */}
|
|
590
|
+
<Grid cols={3} autoFit gap={3}>
|
|
591
|
+
<Panel
|
|
592
|
+
title="Tasks · last 30 days"
|
|
593
|
+
description="Bucketed from snapshot.tasks createdAt"
|
|
594
|
+
padding={3}
|
|
595
|
+
data-testid="overview-panel-tasks-spark"
|
|
596
|
+
>
|
|
597
|
+
<Box p={3}>
|
|
598
|
+
<Stack direction="row" justify="between" align="end" gap={3}>
|
|
599
|
+
<Sparkline
|
|
600
|
+
data={tasksSparkline}
|
|
601
|
+
width={220}
|
|
602
|
+
height={56}
|
|
603
|
+
ariaLabel="Tasks created per day, last 30"
|
|
604
|
+
/>
|
|
605
|
+
<Box
|
|
606
|
+
as="div"
|
|
607
|
+
className="overview-spark-meta"
|
|
608
|
+
style={{
|
|
609
|
+
fontFamily: 'var(--font-mono)',
|
|
610
|
+
fontVariantNumeric: 'tabular-nums',
|
|
611
|
+
fontSize: 'var(--text-2xl)',
|
|
612
|
+
fontWeight: 'var(--weight-semibold)',
|
|
613
|
+
color: 'var(--text-primary)',
|
|
614
|
+
}}
|
|
615
|
+
>
|
|
616
|
+
{tasksTotal}
|
|
617
|
+
</Box>
|
|
618
|
+
</Stack>
|
|
619
|
+
</Box>
|
|
620
|
+
</Panel>
|
|
621
|
+
|
|
622
|
+
<Panel
|
|
623
|
+
title="Agent activity · 30d"
|
|
624
|
+
description="Distinct assignees touching tasks per day"
|
|
625
|
+
padding={3}
|
|
626
|
+
data-testid="overview-panel-agents-spark"
|
|
627
|
+
>
|
|
628
|
+
<Box p={3}>
|
|
629
|
+
<Stack direction="row" justify="between" align="end" gap={3}>
|
|
630
|
+
<Sparkline
|
|
631
|
+
data={agentsSparkline}
|
|
632
|
+
width={220}
|
|
633
|
+
height={56}
|
|
634
|
+
stroke="var(--chart-2)"
|
|
635
|
+
fill="var(--info-subtle)"
|
|
636
|
+
ariaLabel="Agent touches per day, last 30"
|
|
637
|
+
/>
|
|
638
|
+
<Box
|
|
639
|
+
as="div"
|
|
640
|
+
style={{
|
|
641
|
+
fontFamily: 'var(--font-mono)',
|
|
642
|
+
fontVariantNumeric: 'tabular-nums',
|
|
643
|
+
fontSize: 'var(--text-2xl)',
|
|
644
|
+
fontWeight: 'var(--weight-semibold)',
|
|
645
|
+
color: 'var(--text-primary)',
|
|
646
|
+
}}
|
|
647
|
+
>
|
|
648
|
+
{overview.counts.agents}
|
|
649
|
+
</Box>
|
|
650
|
+
</Stack>
|
|
651
|
+
</Box>
|
|
652
|
+
</Panel>
|
|
653
|
+
|
|
654
|
+
<Panel
|
|
655
|
+
title="Tasks by status"
|
|
656
|
+
description="Live snapshot of the kanban buckets"
|
|
657
|
+
padding={3}
|
|
658
|
+
data-testid="overview-panel-tasks-by-status"
|
|
377
659
|
>
|
|
378
|
-
<
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
660
|
+
<Box as="div" p={3}>
|
|
661
|
+
<BarChart data={tasksByStatus} />
|
|
662
|
+
</Box>
|
|
663
|
+
</Panel>
|
|
664
|
+
|
|
665
|
+
<Panel
|
|
666
|
+
title="Memory"
|
|
667
|
+
description="Vault, LightRAG, git sync"
|
|
668
|
+
padding={3}
|
|
669
|
+
data-testid="overview-panel-memory"
|
|
670
|
+
>
|
|
671
|
+
<Box as="div" p={3}>
|
|
672
|
+
<MemoryStatusCard setActiveTab={setActiveTab} />
|
|
673
|
+
</Box>
|
|
674
|
+
</Panel>
|
|
675
|
+
|
|
676
|
+
<Panel
|
|
677
|
+
title="Active project"
|
|
678
|
+
description={
|
|
679
|
+
activeProject
|
|
680
|
+
? `${activeProject.path}`
|
|
681
|
+
: 'No active project selected'
|
|
682
|
+
}
|
|
683
|
+
padding={3}
|
|
684
|
+
data-testid="overview-panel-active-project"
|
|
685
|
+
>
|
|
686
|
+
{activeProject ? (
|
|
687
|
+
<Box p={3}>
|
|
688
|
+
<Stack gap={3}>
|
|
689
|
+
<KeyValueList
|
|
690
|
+
items={[
|
|
691
|
+
{
|
|
692
|
+
key: 'pname',
|
|
693
|
+
label: 'Name',
|
|
694
|
+
value: activeProject.name,
|
|
695
|
+
mono: true,
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
key: 'ppath',
|
|
699
|
+
label: 'Path',
|
|
700
|
+
value: truncateMiddle(activeProject.path, 48),
|
|
701
|
+
mono: true,
|
|
702
|
+
copyable: true,
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
key: 'pstatus',
|
|
706
|
+
label: 'Status',
|
|
707
|
+
value: (
|
|
708
|
+
<Badge
|
|
709
|
+
variant={
|
|
710
|
+
activeProject.status === 'active'
|
|
711
|
+
? 'success'
|
|
712
|
+
: activeProject.status === 'error'
|
|
713
|
+
? 'danger'
|
|
714
|
+
: 'neutral'
|
|
715
|
+
}
|
|
716
|
+
>
|
|
717
|
+
<StatusDot
|
|
718
|
+
variant={
|
|
719
|
+
activeProject.status === 'active'
|
|
720
|
+
? 'success'
|
|
721
|
+
: activeProject.status === 'error'
|
|
722
|
+
? 'danger'
|
|
723
|
+
: 'neutral'
|
|
724
|
+
}
|
|
725
|
+
size="sm"
|
|
726
|
+
pulse={activeProject.status === 'active'}
|
|
727
|
+
/>
|
|
728
|
+
{activeProject.status}
|
|
729
|
+
</Badge>
|
|
730
|
+
),
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
key: 'pseen',
|
|
734
|
+
label: 'Last opened',
|
|
735
|
+
value: activeProject.lastAccessed
|
|
736
|
+
? formatRelative(activeProject.lastAccessed) ||
|
|
737
|
+
'—'
|
|
738
|
+
: '—',
|
|
739
|
+
},
|
|
740
|
+
]}
|
|
391
741
|
/>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
742
|
+
<Inline gap={2} wrap>
|
|
743
|
+
<UiButton
|
|
744
|
+
variant="primary"
|
|
745
|
+
size="sm"
|
|
746
|
+
onClick={() => setActiveTab('tasks')}
|
|
747
|
+
icon={<CheckSquare size={12} />}
|
|
748
|
+
>
|
|
749
|
+
Open tasks
|
|
750
|
+
</UiButton>
|
|
751
|
+
<UiButton
|
|
752
|
+
variant="secondary"
|
|
753
|
+
size="sm"
|
|
754
|
+
onClick={onUseCurrentDir}
|
|
755
|
+
icon={<Folder size={12} />}
|
|
756
|
+
>
|
|
757
|
+
Auto-detect
|
|
758
|
+
</UiButton>
|
|
759
|
+
</Inline>
|
|
760
|
+
</Stack>
|
|
761
|
+
</Box>
|
|
762
|
+
) : (
|
|
763
|
+
<Box as="div" p={3}>
|
|
764
|
+
<EmptyState
|
|
765
|
+
title="No active project"
|
|
766
|
+
description="Add one or auto-detect from the current directory."
|
|
767
|
+
inline
|
|
768
|
+
icon={<Folder size={20} />}
|
|
769
|
+
/>
|
|
770
|
+
</Box>
|
|
771
|
+
)}
|
|
772
|
+
</Panel>
|
|
399
773
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
774
|
+
<Panel
|
|
775
|
+
title="Recent mods"
|
|
776
|
+
description="Last few installed extensions"
|
|
777
|
+
padding={3}
|
|
778
|
+
data-testid="overview-panel-recent-mods"
|
|
779
|
+
>
|
|
780
|
+
{mods.length === 0 ? (
|
|
781
|
+
<Box as="div" p={3}>
|
|
782
|
+
<EmptyState
|
|
783
|
+
title="No mods installed"
|
|
784
|
+
description="Install from the Mods tab."
|
|
785
|
+
inline
|
|
786
|
+
icon={<Puzzle size={20} />}
|
|
787
|
+
/>
|
|
788
|
+
</Box>
|
|
789
|
+
) : (
|
|
790
|
+
<Box p={2}>
|
|
791
|
+
<Stack gap={0}>
|
|
792
|
+
{mods.slice(0, 6).map((m) => (
|
|
793
|
+
<Box
|
|
794
|
+
key={m.id}
|
|
795
|
+
py={2}
|
|
796
|
+
px={3}
|
|
797
|
+
style={{
|
|
798
|
+
borderTop: '1px solid var(--border-subtle)',
|
|
799
|
+
}}
|
|
800
|
+
>
|
|
801
|
+
<Inline justify="between" align="center" gap={3}>
|
|
802
|
+
<Stack gap={0} style={{ minWidth: 0 }}>
|
|
803
|
+
<span
|
|
804
|
+
style={{
|
|
805
|
+
fontWeight: 'var(--weight-medium)',
|
|
806
|
+
fontSize: 'var(--text-base)',
|
|
807
|
+
}}
|
|
808
|
+
>
|
|
809
|
+
{m.name}
|
|
810
|
+
</span>
|
|
811
|
+
<span
|
|
812
|
+
className="muted"
|
|
813
|
+
style={{
|
|
814
|
+
fontFamily: 'var(--font-mono)',
|
|
815
|
+
fontSize: 'var(--text-xs)',
|
|
816
|
+
color: 'var(--text-tertiary)',
|
|
817
|
+
}}
|
|
818
|
+
>
|
|
819
|
+
v{m.version} · {m.type}
|
|
820
|
+
</span>
|
|
821
|
+
</Stack>
|
|
822
|
+
<Badge
|
|
823
|
+
variant={m.enabled ? 'success' : 'neutral'}
|
|
824
|
+
size="sm"
|
|
825
|
+
>
|
|
826
|
+
<StatusDot
|
|
827
|
+
variant={m.enabled ? 'success' : 'neutral'}
|
|
828
|
+
size="sm"
|
|
829
|
+
/>
|
|
830
|
+
{m.enabled ? 'on' : 'off'}
|
|
831
|
+
</Badge>
|
|
832
|
+
</Inline>
|
|
833
|
+
</Box>
|
|
834
|
+
))}
|
|
835
|
+
</Stack>
|
|
836
|
+
</Box>
|
|
837
|
+
)}
|
|
838
|
+
</Panel>
|
|
839
|
+
</Grid>
|
|
840
|
+
|
|
841
|
+
{/* ─── Section 4: Activity stream ────────────────────────────── */}
|
|
842
|
+
<Panel
|
|
843
|
+
title="Recent activity"
|
|
844
|
+
description={`${visibleActivity.length} item${visibleActivity.length === 1 ? '' : 's'} · live via SSE`}
|
|
845
|
+
actions={
|
|
846
|
+
<Inline gap={2} align="center">
|
|
847
|
+
{hiddenKeys.size > 0 && (
|
|
848
|
+
<UiButton
|
|
849
|
+
variant="ghost"
|
|
850
|
+
size="sm"
|
|
851
|
+
onClick={onRestoreAll}
|
|
852
|
+
icon={<Eye size={12} />}
|
|
853
|
+
aria-label={`Restore ${hiddenKeys.size} hidden items`}
|
|
854
|
+
>
|
|
855
|
+
Restore ({hiddenKeys.size})
|
|
856
|
+
</UiButton>
|
|
857
|
+
)}
|
|
858
|
+
{activityItems.length > 0 && (
|
|
859
|
+
<UiButton
|
|
860
|
+
variant="ghost"
|
|
861
|
+
size="sm"
|
|
862
|
+
onClick={onClearAll}
|
|
863
|
+
icon={<EyeOff size={12} />}
|
|
864
|
+
data-testid="overview-hide-all"
|
|
865
|
+
aria-label="Hide all activity from overview"
|
|
866
|
+
>
|
|
867
|
+
Hide all
|
|
868
|
+
</UiButton>
|
|
869
|
+
)}
|
|
870
|
+
</Inline>
|
|
871
|
+
}
|
|
872
|
+
padding={0}
|
|
873
|
+
data-testid="overview-panel-activity"
|
|
874
|
+
>
|
|
875
|
+
{hiddenKeys.size > 0 && (
|
|
876
|
+
<Box
|
|
877
|
+
as="div"
|
|
878
|
+
p={3}
|
|
879
|
+
style={{
|
|
880
|
+
background: 'var(--surface-2)',
|
|
881
|
+
borderTop: '1px solid var(--border-subtle)',
|
|
882
|
+
borderBottom: '1px solid var(--border-subtle)',
|
|
883
|
+
display: 'flex',
|
|
884
|
+
alignItems: 'center',
|
|
885
|
+
justifyContent: 'space-between',
|
|
886
|
+
gap: 'var(--space-3)',
|
|
430
887
|
}}
|
|
888
|
+
role="status"
|
|
431
889
|
>
|
|
432
|
-
<
|
|
433
|
-
|
|
890
|
+
<Inline gap={2} align="center">
|
|
891
|
+
<EyeOff size={12} />
|
|
892
|
+
<span style={{ fontSize: 'var(--text-sm)' }}>
|
|
893
|
+
{hiddenKeys.size} item{hiddenKeys.size === 1 ? '' : 's'}{' '}
|
|
894
|
+
hidden from the overview.
|
|
895
|
+
</span>
|
|
896
|
+
</Inline>
|
|
897
|
+
<UiButton variant="ghost" size="sm" onClick={onRestoreAll}>
|
|
898
|
+
<Eye size={12} /> Show them again
|
|
899
|
+
</UiButton>
|
|
900
|
+
</Box>
|
|
434
901
|
)}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
</CardMeta>
|
|
444
|
-
{projects.length === 0 ? (
|
|
445
|
-
<EmptyState
|
|
446
|
-
icon={<Folder size={32} />}
|
|
447
|
-
title="No projects yet"
|
|
448
|
-
message="Add a project to start tracking its tasks, plans, and schedules."
|
|
449
|
-
action={
|
|
450
|
-
<div className="empty-state-actions">
|
|
451
|
-
<Button variant="primary" onClick={onUseCurrentDir}>
|
|
452
|
-
<Plus size={14} /> Use current directory
|
|
453
|
-
</Button>
|
|
454
|
-
<Button variant="secondary" onClick={onAddProject}>
|
|
455
|
-
Add by path…
|
|
456
|
-
</Button>
|
|
457
|
-
</div>
|
|
458
|
-
}
|
|
459
|
-
/>
|
|
460
|
-
) : (
|
|
461
|
-
<div className="project-grid">
|
|
462
|
-
{projects.map((p) => (
|
|
463
|
-
<ProjectCard
|
|
464
|
-
key={p.id}
|
|
465
|
-
project={p}
|
|
466
|
-
active={activeId === p.id}
|
|
467
|
-
onOpen={() => onActivate(p.id)}
|
|
468
|
-
onRemove={() => onRemove(p.id)}
|
|
902
|
+
|
|
903
|
+
{visibleActivity.length === 0 ? (
|
|
904
|
+
<Box as="div" p={5}>
|
|
905
|
+
<EmptyState
|
|
906
|
+
title="No activity yet"
|
|
907
|
+
description="Use the prompt below or invoke a Bizar command to start a feed."
|
|
908
|
+
inline
|
|
909
|
+
icon={<Activity size={20} />}
|
|
469
910
|
/>
|
|
470
|
-
|
|
471
|
-
</div>
|
|
472
|
-
)}
|
|
473
|
-
</Card>
|
|
474
|
-
|
|
475
|
-
<MemoryStatusCard setActiveTab={setActiveTab} />
|
|
476
|
-
|
|
477
|
-
<div className="overview-cols">
|
|
478
|
-
<Card>
|
|
479
|
-
<CardTitle>Mods</CardTitle>
|
|
480
|
-
<CardMeta>Extensions installed under <code>~/.config/bizar/mods/</code></CardMeta>
|
|
481
|
-
{mods.length === 0 ? (
|
|
482
|
-
<div className="muted">No mods installed.</div>
|
|
911
|
+
</Box>
|
|
483
912
|
) : (
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
913
|
+
<Stack
|
|
914
|
+
as="ul"
|
|
915
|
+
gap={0}
|
|
916
|
+
role="list"
|
|
917
|
+
aria-live="polite"
|
|
918
|
+
aria-relevant="additions"
|
|
919
|
+
data-testid="overview-activity-list"
|
|
920
|
+
>
|
|
921
|
+
{visibleActivity.map(({ it, key }) => (
|
|
922
|
+
<ActivityRow
|
|
923
|
+
key={key}
|
|
924
|
+
item={it}
|
|
925
|
+
activityKey={key}
|
|
926
|
+
onNavigate={setActiveTab}
|
|
927
|
+
onHide={onHide}
|
|
928
|
+
/>
|
|
493
929
|
))}
|
|
494
|
-
</
|
|
930
|
+
</Stack>
|
|
495
931
|
)}
|
|
496
|
-
</
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
932
|
+
</Panel>
|
|
933
|
+
|
|
934
|
+
{/* ─── Section 5: Quick prompts ──────────────────────────────── */}
|
|
935
|
+
<Panel
|
|
936
|
+
title="What do you want to do?"
|
|
937
|
+
description="Describe what you want — Odin will split it into tasks, delegate to background agents, and track progress in real time."
|
|
938
|
+
padding={4}
|
|
939
|
+
actions={
|
|
940
|
+
<Badge variant="accent" size="sm">
|
|
941
|
+
<Sparkles size={12} />
|
|
942
|
+
<span style={{ marginLeft: 4 }}>Odin + 12 specialists</span>
|
|
943
|
+
</Badge>
|
|
944
|
+
}
|
|
945
|
+
data-testid="overview-panel-prompts"
|
|
946
|
+
>
|
|
947
|
+
<form
|
|
948
|
+
onSubmit={async (e) => {
|
|
949
|
+
e.preventDefault();
|
|
950
|
+
const text = (inputRef.current?.value || '').trim();
|
|
951
|
+
if (!text) return;
|
|
952
|
+
setSubmitting(true);
|
|
953
|
+
try {
|
|
954
|
+
const r = await api.post<{ subtasks?: unknown[] }>(
|
|
955
|
+
'/tasks/submit',
|
|
956
|
+
{ title: text },
|
|
957
|
+
);
|
|
958
|
+
toast.success(
|
|
959
|
+
`Odin split it into ${r.subtasks?.length || 1} task(s)`,
|
|
960
|
+
);
|
|
961
|
+
if (inputRef.current) inputRef.current.value = '';
|
|
962
|
+
await refreshSnapshot();
|
|
963
|
+
} catch (err) {
|
|
964
|
+
toast.error(`Failed: ${(err as Error).message}`);
|
|
965
|
+
} finally {
|
|
966
|
+
setSubmitting(false);
|
|
967
|
+
}
|
|
968
|
+
}}
|
|
969
|
+
>
|
|
970
|
+
<Stack gap={3}>
|
|
971
|
+
<textarea
|
|
972
|
+
ref={inputRef}
|
|
973
|
+
disabled={submitting}
|
|
974
|
+
aria-label="Describe what you want Odin to do"
|
|
975
|
+
rows={4}
|
|
976
|
+
style={{
|
|
977
|
+
width: '100%',
|
|
978
|
+
resize: 'vertical',
|
|
979
|
+
fontFamily: 'var(--font-sans)',
|
|
980
|
+
fontSize: 'var(--text-base)',
|
|
981
|
+
lineHeight: 'var(--leading-normal)',
|
|
982
|
+
padding: 'var(--space-3) var(--space-4)',
|
|
983
|
+
border: '1px solid var(--border-default)',
|
|
984
|
+
borderRadius: 'var(--radius-md)',
|
|
985
|
+
background: 'var(--surface-1)',
|
|
986
|
+
color: 'var(--text-primary)',
|
|
987
|
+
}}
|
|
988
|
+
placeholder="e.g. Implement user authentication with email + password, including registration, login, password reset, and integration tests. Use Bcrypt, JWT tokens, and the existing API style."
|
|
989
|
+
data-testid="overview-prompt-input"
|
|
990
|
+
/>
|
|
991
|
+
<Inline justify="between" align="center" gap={3} wrap>
|
|
992
|
+
<Inline gap={2} wrap>
|
|
993
|
+
{[
|
|
994
|
+
'Implement feature',
|
|
995
|
+
'Fix bug',
|
|
996
|
+
'Refactor',
|
|
997
|
+
'Investigate',
|
|
998
|
+
'Add tests',
|
|
999
|
+
'Document',
|
|
1000
|
+
'Optimize',
|
|
1001
|
+
'Deploy',
|
|
1002
|
+
].map((action) => (
|
|
1003
|
+
<button
|
|
1004
|
+
key={action}
|
|
1005
|
+
type="button"
|
|
1006
|
+
className="overview-quick-chip"
|
|
1007
|
+
onClick={() => {
|
|
1008
|
+
if (inputRef.current) {
|
|
1009
|
+
inputRef.current.value = action;
|
|
1010
|
+
inputRef.current.focus();
|
|
1011
|
+
}
|
|
1012
|
+
}}
|
|
1013
|
+
>
|
|
1014
|
+
{action}
|
|
1015
|
+
</button>
|
|
1016
|
+
))}
|
|
1017
|
+
</Inline>
|
|
1018
|
+
<UiButton
|
|
1019
|
+
type="submit"
|
|
1020
|
+
variant="primary"
|
|
1021
|
+
size="md"
|
|
1022
|
+
disabled={submitting}
|
|
1023
|
+
loading={submitting}
|
|
1024
|
+
icon={submitting ? undefined : <Send size={12} />}
|
|
1025
|
+
data-testid="overview-submit"
|
|
1026
|
+
>
|
|
1027
|
+
{submitting ? 'Submitting…' : 'Submit to Odin'}
|
|
1028
|
+
</UiButton>
|
|
1029
|
+
</Inline>
|
|
1030
|
+
</Stack>
|
|
1031
|
+
</form>
|
|
1032
|
+
</Panel>
|
|
1033
|
+
</Stack>
|
|
1034
|
+
</Box>
|
|
522
1035
|
);
|
|
523
1036
|
}
|
|
524
1037
|
|
|
525
|
-
// ─── AddProjectDialog
|
|
1038
|
+
// ─── AddProjectDialog (legacy <Modal>) ──────────────────────────────────────
|
|
526
1039
|
|
|
527
|
-
/**
|
|
528
|
-
* Pre-flight approach: before POST /projects we verify the selected path
|
|
529
|
-
* still exists (GET /fs?path=...). This avoids the ugly 404 toast when a
|
|
530
|
-
* directory is deleted between selection and submit.
|
|
531
|
-
*/
|
|
532
1040
|
function AddProjectDialog({
|
|
533
1041
|
settings,
|
|
534
1042
|
onAdd,
|
|
@@ -536,9 +1044,11 @@ function AddProjectDialog({
|
|
|
536
1044
|
settings: Settings;
|
|
537
1045
|
onAdd: (path: string, name: string | null) => void;
|
|
538
1046
|
}) {
|
|
539
|
-
const [path, setPath] = useState(
|
|
540
|
-
|
|
541
|
-
|
|
1047
|
+
const [path, setPath] = useState<string>(
|
|
1048
|
+
settings?.dashboard?.projectsDirectory ?? '',
|
|
1049
|
+
);
|
|
1050
|
+
const [name, setName] = useState<string>('');
|
|
1051
|
+
const [preflighting, setPreflighting] = useState<boolean>(false);
|
|
542
1052
|
const [preflightError, setPreflightError] = useState<string | null>(null);
|
|
543
1053
|
|
|
544
1054
|
const handleAdd = async () => {
|
|
@@ -546,9 +1056,7 @@ function AddProjectDialog({
|
|
|
546
1056
|
setPreflighting(true);
|
|
547
1057
|
setPreflightError(null);
|
|
548
1058
|
try {
|
|
549
|
-
// Pre-flight: verify the path still exists
|
|
550
1059
|
await api.get<DirectoryListing>('/fs?path=' + encodeURIComponent(path));
|
|
551
|
-
// Path is valid — proceed with the POST
|
|
552
1060
|
onAdd(path, name || null);
|
|
553
1061
|
} catch (err) {
|
|
554
1062
|
const apiErr = err as { status?: number; data?: { message?: string } };
|
|
@@ -556,7 +1064,9 @@ function AddProjectDialog({
|
|
|
556
1064
|
setPreflightError('That folder no longer exists. Pick another.');
|
|
557
1065
|
} else {
|
|
558
1066
|
setPreflightError(
|
|
559
|
-
apiErr.data?.message ??
|
|
1067
|
+
apiErr.data?.message ??
|
|
1068
|
+
(err as Error).message ??
|
|
1069
|
+
'Validation failed.',
|
|
560
1070
|
);
|
|
561
1071
|
}
|
|
562
1072
|
} finally {
|
|
@@ -577,12 +1087,17 @@ function AddProjectDialog({
|
|
|
577
1087
|
height={320}
|
|
578
1088
|
/>
|
|
579
1089
|
{preflightError && (
|
|
580
|
-
<p
|
|
1090
|
+
<p
|
|
1091
|
+
className="field-help"
|
|
1092
|
+
style={{ color: 'var(--error)', marginTop: 4 }}
|
|
1093
|
+
>
|
|
581
1094
|
{preflightError}
|
|
582
1095
|
</p>
|
|
583
1096
|
)}
|
|
584
1097
|
<div style={{ marginTop: 'var(--space-3)' }}>
|
|
585
|
-
<label className="field-label" htmlFor="add-project-name">
|
|
1098
|
+
<label className="field-label" htmlFor="add-project-name">
|
|
1099
|
+
Name (optional)
|
|
1100
|
+
</label>
|
|
586
1101
|
<input
|
|
587
1102
|
id="add-project-name"
|
|
588
1103
|
className="input"
|
|
@@ -595,7 +1110,13 @@ function AddProjectDialog({
|
|
|
595
1110
|
Display name for this project. Defaults to the folder name.
|
|
596
1111
|
</p>
|
|
597
1112
|
</div>
|
|
598
|
-
<div
|
|
1113
|
+
<div
|
|
1114
|
+
style={{
|
|
1115
|
+
marginTop: 'var(--space-3)',
|
|
1116
|
+
display: 'flex',
|
|
1117
|
+
justifyContent: 'flex-end',
|
|
1118
|
+
}}
|
|
1119
|
+
>
|
|
599
1120
|
<Button
|
|
600
1121
|
variant="primary"
|
|
601
1122
|
onClick={handleAdd}
|
|
@@ -609,59 +1130,126 @@ function AddProjectDialog({
|
|
|
609
1130
|
);
|
|
610
1131
|
}
|
|
611
1132
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1133
|
+
// ─── Activity row (the compact inline list item) ────────────────────────────
|
|
1134
|
+
|
|
1135
|
+
function ActivityRow({
|
|
1136
|
+
item,
|
|
1137
|
+
activityKey,
|
|
1138
|
+
onNavigate,
|
|
1139
|
+
onHide,
|
|
617
1140
|
}: {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1141
|
+
item: ActivityItem;
|
|
1142
|
+
activityKey: string;
|
|
1143
|
+
onNavigate: (tab: string) => void;
|
|
1144
|
+
onHide: (key: string) => void;
|
|
622
1145
|
}) {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1146
|
+
const severity = activitySeverity(item);
|
|
1147
|
+
const Icon = activityIcon(item.kind || '');
|
|
1148
|
+
const title = humanizeKind(item.kind || 'activity');
|
|
1149
|
+
const msg = formatActivitySummary(item);
|
|
1150
|
+
const navTarget = activityNavTarget(item.kind || '');
|
|
1151
|
+
|
|
628
1152
|
return (
|
|
629
|
-
<
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1153
|
+
<li
|
|
1154
|
+
className={`overview-activity-row overview-activity-row-${severity}`}
|
|
1155
|
+
style={{
|
|
1156
|
+
borderTop: '1px solid var(--border-subtle)',
|
|
1157
|
+
display: 'flex',
|
|
1158
|
+
alignItems: 'center',
|
|
1159
|
+
gap: 'var(--space-3)',
|
|
1160
|
+
paddingInline: 'var(--space-4)',
|
|
1161
|
+
paddingBlock: 'var(--space-3)',
|
|
1162
|
+
}}
|
|
1163
|
+
>
|
|
1164
|
+
<button
|
|
1165
|
+
type="button"
|
|
1166
|
+
onClick={() => {
|
|
1167
|
+
if (navTarget) onNavigate(navTarget);
|
|
1168
|
+
}}
|
|
1169
|
+
className="overview-activity-row__main"
|
|
1170
|
+
title={navTarget ? `Open ${navTarget}` : title}
|
|
1171
|
+
style={{
|
|
1172
|
+
flex: 1,
|
|
1173
|
+
display: 'flex',
|
|
1174
|
+
alignItems: 'center',
|
|
1175
|
+
gap: 'var(--space-3)',
|
|
1176
|
+
textAlign: 'left',
|
|
1177
|
+
background: 'transparent',
|
|
1178
|
+
color: 'inherit',
|
|
1179
|
+
padding: 0,
|
|
1180
|
+
}}
|
|
1181
|
+
>
|
|
1182
|
+
<span
|
|
1183
|
+
style={{
|
|
1184
|
+
color:
|
|
1185
|
+
severity === 'error'
|
|
1186
|
+
? 'var(--danger)'
|
|
1187
|
+
: severity === 'warning'
|
|
1188
|
+
? 'var(--warning)'
|
|
1189
|
+
: severity === 'success'
|
|
1190
|
+
? 'var(--success)'
|
|
1191
|
+
: 'var(--accent)',
|
|
1192
|
+
display: 'inline-flex',
|
|
1193
|
+
alignItems: 'center',
|
|
1194
|
+
justifyContent: 'center',
|
|
1195
|
+
width: 22,
|
|
1196
|
+
height: 22,
|
|
1197
|
+
flexShrink: 0,
|
|
643
1198
|
}}
|
|
644
1199
|
>
|
|
645
|
-
<
|
|
646
|
-
</
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1200
|
+
<Icon size={14} />
|
|
1201
|
+
</span>
|
|
1202
|
+
<Stack gap={0} style={{ minWidth: 0, flex: 1 }}>
|
|
1203
|
+
<Inline justify="between" align="center" gap={3} wrap>
|
|
1204
|
+
<span
|
|
1205
|
+
style={{
|
|
1206
|
+
fontSize: 'var(--text-sm)',
|
|
1207
|
+
fontWeight: 'var(--weight-medium)',
|
|
1208
|
+
}}
|
|
1209
|
+
>
|
|
1210
|
+
{title}
|
|
1211
|
+
</span>
|
|
1212
|
+
<span
|
|
1213
|
+
style={{
|
|
1214
|
+
fontFamily: 'var(--font-mono)',
|
|
1215
|
+
fontVariantNumeric: 'tabular-nums',
|
|
1216
|
+
fontSize: 'var(--text-xs)',
|
|
1217
|
+
color: 'var(--text-tertiary)',
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
{formatClock(item.ts)}
|
|
1221
|
+
</span>
|
|
1222
|
+
</Inline>
|
|
1223
|
+
<span
|
|
1224
|
+
className="muted"
|
|
1225
|
+
style={{
|
|
1226
|
+
fontSize: 'var(--text-sm)',
|
|
1227
|
+
color: 'var(--text-secondary)',
|
|
1228
|
+
overflow: 'hidden',
|
|
1229
|
+
textOverflow: 'ellipsis',
|
|
1230
|
+
whiteSpace: 'nowrap',
|
|
1231
|
+
display: 'block',
|
|
1232
|
+
}}
|
|
1233
|
+
title={msg}
|
|
1234
|
+
>
|
|
1235
|
+
{msg}
|
|
1236
|
+
</span>
|
|
1237
|
+
</Stack>
|
|
1238
|
+
</button>
|
|
1239
|
+
<IconButton
|
|
1240
|
+
variant="ghost"
|
|
1241
|
+
size="sm"
|
|
1242
|
+
onClick={() => onHide(activityKey)}
|
|
1243
|
+
aria-label="Hide from overview"
|
|
1244
|
+
title="Hide from overview (kept in Settings → Activity Log)"
|
|
1245
|
+
icon={<X size={12} />}
|
|
1246
|
+
/>
|
|
1247
|
+
</li>
|
|
662
1248
|
);
|
|
663
1249
|
}
|
|
664
1250
|
|
|
1251
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
1252
|
+
|
|
665
1253
|
function sentenceCase(s: string): string {
|
|
666
1254
|
if (!s) return s;
|
|
667
1255
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -687,19 +1275,32 @@ function humanizeKind(kind: string): string {
|
|
|
687
1275
|
providers: 'Provider',
|
|
688
1276
|
};
|
|
689
1277
|
const actionMap: Record<string, string> = {
|
|
690
|
-
create: 'created',
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1278
|
+
create: 'created',
|
|
1279
|
+
created: 'created',
|
|
1280
|
+
add: 'added',
|
|
1281
|
+
added: 'added',
|
|
1282
|
+
update: 'updated',
|
|
1283
|
+
updated: 'updated',
|
|
1284
|
+
delegate: 'delegated',
|
|
1285
|
+
delegated: 'delegated',
|
|
1286
|
+
invoke: 'invoked',
|
|
1287
|
+
invoked: 'invoked',
|
|
1288
|
+
restart: 'restarted',
|
|
1289
|
+
restarted: 'restarted',
|
|
1290
|
+
delete: 'deleted',
|
|
1291
|
+
deleted: 'deleted',
|
|
1292
|
+
remove: 'removed',
|
|
1293
|
+
removed: 'removed',
|
|
1294
|
+
archive: 'archived',
|
|
1295
|
+
archived: 'archived',
|
|
1296
|
+
restore: 'restored',
|
|
1297
|
+
restored: 'restored',
|
|
1298
|
+
complete: 'completed',
|
|
1299
|
+
completed: 'completed',
|
|
1300
|
+
fail: 'failed',
|
|
1301
|
+
failed: 'failed',
|
|
1302
|
+
error: 'errored',
|
|
1303
|
+
stuck: 'marked stuck',
|
|
703
1304
|
};
|
|
704
1305
|
const scope = scopeMap[scopeRaw] || sentenceCase(scopeRaw.replace(/-/g, ' '));
|
|
705
1306
|
const action = actionMap[actionRaw] || actionRaw.replace(/-/g, ' ');
|
|
@@ -727,12 +1328,6 @@ function formatActivitySummary(it: ActivityItem): string {
|
|
|
727
1328
|
return parts.length ? parts.join(' · ') : 'No additional details.';
|
|
728
1329
|
}
|
|
729
1330
|
|
|
730
|
-
// v3.7.0 — Format a timestamp as a relative string ("2m ago", "1h ago")
|
|
731
|
-
function formatRelativeTime(ts: string): string {
|
|
732
|
-
return formatRelative(ts);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// v3.7.0 — Get severity color token from kind or status field
|
|
736
1331
|
function activitySeverity(it: ActivityItem): 'error' | 'warning' | 'success' | 'info' {
|
|
737
1332
|
const k = ((it.kind as string) || '').toLowerCase();
|
|
738
1333
|
const s = ((it.status as string) || '').toLowerCase();
|
|
@@ -742,7 +1337,6 @@ function activitySeverity(it: ActivityItem): 'error' | 'warning' | 'success' | '
|
|
|
742
1337
|
return 'info';
|
|
743
1338
|
}
|
|
744
1339
|
|
|
745
|
-
// v3.7.0 — Map activity kind to icon
|
|
746
1340
|
function activityIcon(kind: string) {
|
|
747
1341
|
const k = kind.toLowerCase();
|
|
748
1342
|
if (k === 'task') return CheckSquare;
|
|
@@ -756,7 +1350,6 @@ function activityIcon(kind: string) {
|
|
|
756
1350
|
return Activity;
|
|
757
1351
|
}
|
|
758
1352
|
|
|
759
|
-
// v3.7.0 — Navigate to the relevant tab when a card is clicked
|
|
760
1353
|
function activityNavTarget(kind: string): string | null {
|
|
761
1354
|
const k = kind.toLowerCase();
|
|
762
1355
|
if (k === 'task') return 'tasks';
|
|
@@ -768,60 +1361,15 @@ function activityNavTarget(kind: string): string | null {
|
|
|
768
1361
|
return null;
|
|
769
1362
|
}
|
|
770
1363
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
activityKey,
|
|
775
|
-
onNavigate,
|
|
776
|
-
onHide,
|
|
777
|
-
}: {
|
|
778
|
-
item: ActivityItem;
|
|
779
|
-
activityKey: string;
|
|
780
|
-
onNavigate: (tab: string) => void;
|
|
781
|
-
onHide: (key: string) => void;
|
|
782
|
-
}) {
|
|
783
|
-
const severity = activitySeverity(item);
|
|
784
|
-
const Icon = activityIcon(item.kind || '');
|
|
785
|
-
const title = humanizeKind(item.kind || 'activity');
|
|
786
|
-
const msg = formatActivitySummary(item);
|
|
787
|
-
const navTarget = activityNavTarget(item.kind || '');
|
|
788
|
-
|
|
789
|
-
const accentColor =
|
|
790
|
-
severity === 'error' ? 'var(--error)' :
|
|
791
|
-
severity === 'warning' ? 'var(--warning)' :
|
|
792
|
-
severity === 'success' ? 'var(--success)' :
|
|
793
|
-
'var(--accent)';
|
|
1364
|
+
function formatClock(ts: string): string {
|
|
1365
|
+
return formatRelative(ts);
|
|
1366
|
+
}
|
|
794
1367
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
className="activity-feed-row-main"
|
|
800
|
-
onClick={() => { if (navTarget) onNavigate(navTarget); }}
|
|
801
|
-
title={navTarget ? `Open ${navTarget}` : title}
|
|
802
|
-
>
|
|
803
|
-
<div className="activity-feed-icon" style={{ color: accentColor }}>
|
|
804
|
-
<Icon size={14} />
|
|
805
|
-
</div>
|
|
806
|
-
<div className="activity-feed-body">
|
|
807
|
-
<div className="activity-feed-title-row">
|
|
808
|
-
<div className="activity-feed-title">{title}</div>
|
|
809
|
-
<div className="activity-feed-time text-xs muted tabular-nums">{formatRelativeTime(item.ts)}</div>
|
|
810
|
-
</div>
|
|
811
|
-
<div className="activity-feed-summary text-sm">{msg}</div>
|
|
812
|
-
</div>
|
|
813
|
-
</button>
|
|
814
|
-
<button
|
|
815
|
-
type="button"
|
|
816
|
-
className="activity-feed-hide-btn"
|
|
817
|
-
onClick={() => onHide(activityKey)}
|
|
818
|
-
title="Hide this from the overview (kept in Settings → Activity Log)"
|
|
819
|
-
aria-label="Hide from overview"
|
|
820
|
-
>
|
|
821
|
-
<X size={12} />
|
|
822
|
-
</button>
|
|
823
|
-
</div>
|
|
824
|
-
);
|
|
1368
|
+
function truncateMiddle(s: string, maxLen: number): string {
|
|
1369
|
+
if (!s || s.length <= maxLen) return s;
|
|
1370
|
+
const half = Math.floor((maxLen - 1) / 2);
|
|
1371
|
+
return `${s.slice(0, half)}…${s.slice(-half)}`;
|
|
825
1372
|
}
|
|
1373
|
+
|
|
826
1374
|
const OverviewMemo = React.memo(OverviewInner);
|
|
827
1375
|
export { OverviewMemo as Overview };
|