From d05d2904f3a98d932710906a415a82cca043aef7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 29 Jun 2026 22:41:40 +0000 Subject: [PATCH 1/7] feat: add daemon dashboard project selector --- Cargo.lock | 1 + Cargo.toml | 1 + .../holographic/src/HolographicMemoryPage.tsx | 6 +- dashboard/shell/src/main.jsx | 70 ++++- dashboard/shell/src/sdk.jsx | 58 +++- dashboard/shell/src/styles.css | 39 ++- dashboard/test/shell-sdk.test.mjs | 56 ++++ src/dashboard/memory_curate.rs | 1 + src/dashboard/mod.rs | 135 +++++++++- src/dashboard/projects.rs | 248 ++++++++++++++++++ tests/dashboard_api_test.rs | 131 +++++++++ 11 files changed, 720 insertions(+), 26 deletions(-) create mode 100644 src/dashboard/projects.rs diff --git a/Cargo.lock b/Cargo.lock index 6d1a7d25..6bcb6e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4536,6 +4536,7 @@ dependencies = [ "tokensave-medium-treesitters", "tokio", "toml", + "tower 0.5.3", "tracing", "tree-sitter", "tree-sitter-hlsl", diff --git a/Cargo.toml b/Cargo.toml index 143ba8b5..3cf44539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,7 @@ path = "src/main.rs" [dependencies] axum = "0.8" +tower = "0.5" libsql = "0.9.30" tree-sitter = "0.26" tree-sitter-language = "0.1" diff --git a/dashboard/holographic/src/HolographicMemoryPage.tsx b/dashboard/holographic/src/HolographicMemoryPage.tsx index 0676d93f..faddeb0c 100644 --- a/dashboard/holographic/src/HolographicMemoryPage.tsx +++ b/dashboard/holographic/src/HolographicMemoryPage.tsx @@ -333,9 +333,9 @@ function SearchBox({ return (
{refreshing ? ( - + ) : ( - + )} setQuery("")} aria-label="Clear" > diff --git a/dashboard/shell/src/main.jsx b/dashboard/shell/src/main.jsx index ac835660..7c98f132 100644 --- a/dashboard/shell/src/main.jsx +++ b/dashboard/shell/src/main.jsx @@ -13,7 +13,7 @@ import React, { useEffect, useState, useCallback, useRef, useSyncExternalStore } from "react"; import { createRoot } from "react-dom/client"; -import { buildSDK, fetchJSON, cn } from "./sdk.jsx"; +import { buildSDK, fetchJSON, cn, setSelectedProjectId as setSDKSelectedProjectId } from "./sdk.jsx"; // --------------------------------------------------------------------------- // Registry @@ -174,6 +174,14 @@ function injectPluginAssets(manifest) { const POLL_INTERVAL_MS = 15_000; +function basename(path) { + return path?.split("/").filter(Boolean).pop() || path; +} + +function projectLabel(project) { + return project?.label || basename(project?.project_root) || project?.project_id || "Project"; +} + // --------------------------------------------------------------------------- // App // --------------------------------------------------------------------------- @@ -181,6 +189,9 @@ const POLL_INTERVAL_MS = 15_000; function App() { const [plugins, setPlugins] = useState([]); const [capabilities, setCapabilities] = useState(null); + const [projects, setProjects] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState(""); + const [projectRevision, setProjectRevision] = useState(0); const [active, setActive] = useState(""); // Tabs that have been activated at least once; their panels stay mounted // (hidden) afterwards so in-progress exploration survives tab switches. @@ -259,16 +270,25 @@ function App() { } }, []); - // Initial load: plugin list + capabilities in parallel. + // Initial load: plugin manifests are daemon-wide; project inventory selects + // which project-scoped APIs plugin panels should read from. useEffect(() => { let cancelled = false; (async () => { try { - const [list] = await Promise.all([ + const [list, projectPayload] = await Promise.all([ fetchJSON("/api/dashboard/plugins"), - fetchCapabilities(), + fetchJSON("/api/projects"), ]); if (cancelled) return; + const rows = Array.isArray(projectPayload?.projects) ? projectPayload.projects : []; + const initialProjectId = + projectPayload?.active_project_id || rows.find((p) => p.is_active)?.project_id || rows[0]?.project_id || ""; + setProjects(rows); + setSelectedProjectId(initialProjectId); + setSDKSelectedProjectId(initialProjectId); + await fetchCapabilities(); + if (cancelled) return; setPlugins(list); if (list.length > 0) { const fromUrl = tabFromUrl(); @@ -290,6 +310,20 @@ function App() { }; }, []); // eslint-disable-line react-hooks/exhaustive-deps + const onProjectChange = useCallback( + async (event) => { + const next = event.target.value; + if (next === selectedProjectId) return; + setSelectedProjectId(next); + setSDKSelectedProjectId(next); + setCapabilities(null); + setProjectRevision((rev) => rev + 1); + setVisited(active ? new Set([active]) : new Set()); + await fetchCapabilities(); + }, + [active, fetchCapabilities, selectedProjectId], + ); + // Poll capabilities for connection health — but only while the browser tab // is visible; a hidden tab refreshes once on return instead. useEffect(() => { @@ -366,8 +400,12 @@ function App() { const activeManifest = plugins.find((p) => p.name === active); const ActiveComponent = registered.get(active); - const projectName = - capabilities?.project_root?.split("/").filter(Boolean).pop() ?? capabilities?.project_root; + const selectedProject = + projects.find((p) => p.project_id === selectedProjectId) || + projects.find((p) => p.is_active) || + null; + const projectName = selectedProject ? projectLabel(selectedProject) : basename(capabilities?.project_root); + const projectRoot = selectedProject?.project_root || capabilities?.project_root; const connLabel = connState === "ok" @@ -383,7 +421,7 @@ function App() {

tracedecay

{projectName && ( - + {projectName} )} @@ -418,6 +456,22 @@ function App() {
+ {projects.length > 0 && ( + + )} +