From 279a091b3942a9f8ba9ee1217f0ff408ad9d483d Mon Sep 17 00:00:00 2001 From: Mario Aldayuz Date: Sat, 13 Jun 2026 05:51:28 -0400 Subject: [PATCH] feat(cursor): enhance dashboard functionality with goals management and improve session handling - Added goals management features to the dashboard, including the ability to add and filter goals. - Updated the dashboard UI to include a new goals section. - Improved session loading logic to better handle recent sessions and their associated data. - Refactored existing scripts for loading dashboard data, rules, and session summaries to streamline functionality and enhance performance. --- .../hivemind-cursor-extension-0.1.0.vsix | Bin 32873 -> 40583 bytes .../cursor/extension/scripts/lib/deeplake.mjs | 140 ++++++++++++++ .../extension/scripts/load-dashboard.mjs | 173 +++++++++++++++++- .../cursor/extension/scripts/load-goals.mjs | 62 +++++++ .../cursor/extension/scripts/load-rules.mjs | 77 +++++--- .../scripts/load-session-summary.mjs | 72 ++++---- .../extension/scripts/load-sessions.mjs | 75 +++++++- .../cursor/extension/src/bridge/skill-sync.ts | 21 +++ .../extension/src/webview/DashboardPanel.ts | 38 +++- .../extension/src/webview/data-bridge.ts | 79 +++++++- .../src/webview/html/dashboard-shell.ts | 74 +++++++- 11 files changed, 727 insertions(+), 84 deletions(-) create mode 100644 harnesses/cursor/extension/scripts/lib/deeplake.mjs create mode 100644 harnesses/cursor/extension/scripts/load-goals.mjs diff --git a/harnesses/cursor/extension/hivemind-cursor-extension-0.1.0.vsix b/harnesses/cursor/extension/hivemind-cursor-extension-0.1.0.vsix index afbcfb47ec3ae0425553c76a16bbea65b0c982ed..21230773aba11f24a38d45b77a69e4d793a851fd 100644 GIT binary patch delta 36047 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 delta 28401 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 ""); + throw new Error(`Query failed: ${resp.status}: ${text.slice(0, 200)}`); + } + const raw = await resp.json().catch(() => null); + if (!raw || !Array.isArray(raw.rows) || !Array.isArray(raw.columns)) return []; + return raw.rows.map((row) => Object.fromEntries(raw.columns.map((col, i) => [col, row[i]]))); +} + +/** Collapse the surface forms of a git remote URL. Mirrors src/utils/repo-identity.ts. */ +export function normalizeGitRemoteUrl(url) { + let s = String(url).trim(); + const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i); + const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null; + if (schemeMatch) s = s.slice(schemeMatch[0].length); + if (!scheme) { + const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/); + if (scp) s = `${scp[1]}/${scp[2]}`; + } + s = s.replace(/^[^@/]+@/, ""); + const defaultPorts = { http: "80", https: "443", ssh: "22", git: "9418" }; + if (scheme && defaultPorts[scheme]) { + s = s.replace(new RegExp(`^([^/]+):${defaultPorts[scheme]}(/|$)`), "$1$2"); + } + s = s.replace(/\.git\/?$/i, ""); + s = s.replace(/\/+$/, ""); + return s.toLowerCase(); +} + +/** + * Stable per-repo key: sha1 of the normalized git remote (fallback to the + * absolute cwd), first 16 hex chars. Mirrors core deriveProjectKey so the + * extension resolves the SAME `~/.hivemind/graphs/` dir the CLI writes. + */ +export function deriveProjectKey(cwd) { + let signature = null; + try { + const raw = execSync("git config --get remote.origin.url", { + cwd, + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + signature = raw ? normalizeGitRemoteUrl(raw) : null; + } catch { + signature = null; + } + const input = signature ?? cwd; + return createHash("sha1").update(input).digest("hex").slice(0, 16); +} + +export function graphsHome() { + return process.env.HIVEMIND_GRAPHS_HOME ?? join(homedir(), ".hivemind", "graphs"); +} diff --git a/harnesses/cursor/extension/scripts/load-dashboard.mjs b/harnesses/cursor/extension/scripts/load-dashboard.mjs index 1966ba15..71bdccc3 100644 --- a/harnesses/cursor/extension/scripts/load-dashboard.mjs +++ b/harnesses/cursor/extension/scripts/load-dashboard.mjs @@ -1,5 +1,172 @@ -import { loadDashboardData } from "../../../../src/dashboard/data.js"; +/** + * Build the dashboard data envelope (KPIs + codebase graph snapshot). + * + * Self-contained: resolves the per-repo graph key the same way the core CLI + * does (see lib/deeplake.mjs deriveProjectKey) so it finds the snapshot the + * `hivemind graph build` command actually wrote under + * ~/.hivemind/graphs//snapshots. KPIs come from the org-stats cache the + * CLI maintains, falling back to local usage records, then to an empty state. + * Prints a DashboardDataEnvelope JSON to stdout. + */ +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; +import { loadCreds, deriveProjectKey, graphsHome } from "./lib/deeplake.mjs"; const cwd = process.argv[2] || process.cwd(); -const data = await loadDashboardData({ cwd }); -console.log(JSON.stringify(data)); + +const BYTES_PER_TOKEN = 4; +const SAVINGS_MULTIPLIER = 1.7; + +function bytesToSavedTokens(bytes) { + if (!Number.isFinite(bytes) || bytes <= 0) return 0; + return (SAVINGS_MULTIPLIER - 1) * (bytes / BYTES_PER_TOKEN); +} + +function countUserGeneratedSkills(userName) { + if (!userName) return 0; + const dir = join(homedir(), ".claude", "skills"); + if (!existsSync(dir)) return 0; + const suffix = `--${userName}`; + try { + let count = 0; + for (const name of readdirSync(dir)) { + const idx = name.lastIndexOf(suffix); + if (idx > 0 && idx + suffix.length === name.length) count += 1; + } + return count; + } catch { + return 0; + } +} + +function readUsageRecords() { + const path = join(homedir(), ".deeplake", "usage-stats.jsonl"); + if (!existsSync(path)) return []; + const out = []; + try { + for (const line of readFileSync(path, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const rec = JSON.parse(trimmed); + if (typeof rec.endedAt === "string" && typeof rec.sessionId === "string") { + out.push({ + memorySearchBytes: typeof rec.memorySearchBytes === "number" ? rec.memorySearchBytes : 0, + memorySearchCount: typeof rec.memorySearchCount === "number" ? rec.memorySearchCount : 0, + }); + } + } catch { + /* skip bad line */ + } + } + } catch { + return []; + } + return out; +} + +function readOrgStatsCache(creds) { + if (!creds) return null; + const path = join(homedir(), ".deeplake", "hivemind-stats-cache.json"); + if (!existsSync(path)) return null; + try { + const cache = JSON.parse(readFileSync(path, "utf-8")); + const data = cache?.data; + if (!data?.org) return null; + const fetchedAt = typeof cache.fetchedAt === "number" ? new Date(cache.fetchedAt).toISOString() : null; + const stale = typeof cache.fetchedAt === "number" ? Date.now() - cache.fetchedAt > 3600_000 : false; + return { org: data.org, user: data.user ?? data.org, fetchedAt, stale }; + } catch { + return null; + } +} + +function resolveSnapshot(repoDir) { + const snapshotsDir = join(repoDir, "snapshots"); + if (!existsSync(snapshotsDir)) return null; + let snapshotPath = null; + const pointer = join(repoDir, "latest-commit.txt"); + if (existsSync(pointer)) { + try { + const sha = readFileSync(pointer, "utf-8").trim(); + const candidate = join(snapshotsDir, `${sha}.json`); + if (sha && existsSync(candidate)) snapshotPath = candidate; + } catch { + /* fall through to newest-file scan */ + } + } + if (!snapshotPath) { + try { + const candidates = readdirSync(snapshotsDir) + .filter((n) => n.endsWith(".json")) + .map((n) => ({ full: join(snapshotsDir, n), mtime: statSync(join(snapshotsDir, n)).mtimeMs })) + .sort((a, b) => b.mtime - a.mtime); + if (candidates[0]) snapshotPath = candidates[0].full; + } catch { + return null; + } + } + if (!snapshotPath) return null; + try { + const parsed = JSON.parse(readFileSync(snapshotPath, "utf-8")); + if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.links)) return null; + return { + commitSha: parsed.graph?.commit_sha ?? null, + snapshotPath, + nodeCount: parsed.nodes.length, + edgeCount: parsed.links.length, + snapshot: parsed, + }; + } catch { + return null; + } +} + +const creds = loadCreds(); +const repoKey = deriveProjectKey(cwd); +const repoProject = basename(cwd); +const graph = resolveSnapshot(join(graphsHome(), repoKey)); +const skillsCreated = countUserGeneratedSkills(creds?.userName); + +const cache = readOrgStatsCache(creds); +let kpis; +if (cache) { + kpis = { + tokensSaved: bytesToSavedTokens(cache.org.memorySearchBytes ?? 0), + tokensSource: "org", + skillsCreated, + memorySearches: cache.org.memoryRecallCount ?? 0, + sessionsCount: cache.org.sessionsCount ?? null, + userTokensSaved: bytesToSavedTokens((cache.user ?? cache.org).memorySearchBytes ?? 0), + orgStatsFetchedAt: cache.fetchedAt, + orgStatsStale: cache.stale, + orgStatsOffline: false, + }; +} else { + const records = readUsageRecords(); + const localBytes = records.reduce((s, r) => s + r.memorySearchBytes, 0); + const localCount = records.reduce((s, r) => s + r.memorySearchCount, 0); + const has = records.length > 0; + kpis = { + tokensSaved: has ? bytesToSavedTokens(localBytes) : null, + tokensSource: has ? "local" : "none", + skillsCreated, + memorySearches: localCount, + sessionsCount: has ? records.length : null, + userTokensSaved: has ? bytesToSavedTokens(localBytes) : null, + orgStatsFetchedAt: null, + orgStatsStale: false, + orgStatsOffline: false, + }; +} + +process.stdout.write( + JSON.stringify({ + repoKey, + repoProject, + generatedAt: new Date().toISOString(), + kpis, + graph, + }), +); diff --git a/harnesses/cursor/extension/scripts/load-goals.mjs b/harnesses/cursor/extension/scripts/load-goals.mjs new file mode 100644 index 00000000..8bfbf2de --- /dev/null +++ b/harnesses/cursor/extension/scripts/load-goals.mjs @@ -0,0 +1,62 @@ +/** + * List goals from the Deeplake `hivemind_goals` table. + * + * Self-contained: reads credentials and queries the Deeplake HTTP endpoint + * directly. Mirrors core src/commands/goal.ts goalList, with latest-version + * dedup per goal_id (the VFS write path appends a fresh row per overwrite). + * Prints a GoalsListResult JSON to stdout. + * + * argv[2]: filter — "mine" (default) or "all". + */ +import { loadCreds, query, sqlIdent, sqlStr, tableNames, isMissingTableError } from "./lib/deeplake.mjs"; + +const filter = process.argv[2] === "all" ? "all" : "mine"; + +function emit(obj) { + process.stdout.write(JSON.stringify(obj)); +} + +const creds = loadCreds(); +if (!creds) { + emit({ loggedOut: true, goals: [], message: "Log in with `hivemind login` to track team goals." }); + process.exit(0); +} + +const table = sqlIdent(tableNames().goals); +const where = filter === "mine" ? `WHERE owner = '${sqlStr(creds.userName)}'` : ""; + +let rows; +try { + rows = await query( + creds, + `SELECT goal_id, owner, status, content, version, created_at FROM "${table}" ${where} ORDER BY version DESC, created_at DESC LIMIT 200`, + ); +} catch (e) { + if (isMissingTableError(e?.message)) { + emit({ loggedOut: false, goals: [] }); + process.exit(0); + } + emit({ loggedOut: false, goals: [], message: "Could not load goals." }); + process.exit(0); +} + +const latest = new Map(); +for (const r of rows) { + const goalId = String(r.goal_id ?? ""); + if (!goalId || latest.has(goalId)) continue; + const text = String(r.content ?? "").split(/\r?\n/)[0].trim(); + latest.set(goalId, { + goalId, + owner: String(r.owner ?? ""), + status: String(r.status ?? ""), + text, + createdAt: String(r.created_at ?? ""), + }); +} + +const goals = [...latest.values()] + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .slice(0, 50) + .map(({ goalId, owner, status, text }) => ({ goalId, owner, status, text })); + +emit({ loggedOut: false, goals }); diff --git a/harnesses/cursor/extension/scripts/load-rules.mjs b/harnesses/cursor/extension/scripts/load-rules.mjs index c7208bca..4fef11da 100644 --- a/harnesses/cursor/extension/scripts/load-rules.mjs +++ b/harnesses/cursor/extension/scripts/load-rules.mjs @@ -1,36 +1,69 @@ -import { loadConfig } from "../../../../src/config.js"; -import { DeeplakeApi } from "../../../../src/api/deeplake-api.js"; -import { listRules } from "../../../../src/rules/read.js"; +/** + * List team rules from the Deeplake `hivemind_rules` table. + * + * Self-contained: reads credentials and queries the Deeplake HTTP endpoint + * directly (see lib/deeplake.mjs). Mirrors the latest-version-per-rule dedup + * in core src/rules/read.ts. Prints a RulesListResult JSON to stdout. + */ +import { loadCreds, query, sqlIdent, tableNames, isMissingTableError } from "./lib/deeplake.mjs"; const status = process.argv[2] || "active"; const limit = parseInt(process.argv[3] || "10", 10); -const config = loadConfig(); -if (!config) { - console.log(JSON.stringify({ - loggedOut: true, - rules: [], - message: "Log in with `hivemind login` to manage team rules.", - })); +function emit(obj) { + process.stdout.write(JSON.stringify(obj)); +} + +const creds = loadCreds(); +if (!creds) { + emit({ loggedOut: true, rules: [], message: "Log in with `hivemind login` to manage team rules." }); process.exit(0); } -const api = new DeeplakeApi( - config.token, - config.apiUrl, - config.orgId, - config.workspaceId, - config.skillsTableName, -); -const query = (sql) => api.query(sql); -const rows = await listRules(query, config.rulesTableName, { status, limit }); -console.log(JSON.stringify({ +const table = sqlIdent(tableNames().rules); + +let rows; +try { + rows = await query(creds, `SELECT id, rule_id, text, scope, status, assigned_by, version, created_at FROM "${table}" ORDER BY version DESC, created_at DESC, id DESC`); +} catch (e) { + // The rules table is created lazily by the CLI on first write. Until then + // a read 400s with "does not exist" — that just means no rules yet. + if (isMissingTableError(e?.message)) { + emit({ loggedOut: false, rules: [] }); + process.exit(0); + } + emit({ loggedOut: false, rules: [], message: "Could not load rules." }); + process.exit(0); +} + +const latest = new Map(); +for (const r of rows) { + const versionRaw = r.version; + const version = typeof versionRaw === "number" ? versionRaw : Number(versionRaw); + if (!Number.isFinite(version)) continue; + const ruleId = String(r.rule_id ?? ""); + if (!ruleId || latest.has(ruleId)) continue; + latest.set(ruleId, { + id: String(r.id ?? ""), + rule_id: ruleId, + text: String(r.text ?? ""), + status: String(r.status ?? ""), + assigned_by: String(r.assigned_by ?? ""), + version, + created_at: String(r.created_at ?? ""), + }); +} + +const filtered = [...latest.values()].filter((r) => (status === "all" ? true : r.status === status)); +filtered.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)); + +emit({ loggedOut: false, - rules: rows.map((r) => ({ + rules: filtered.slice(0, limit).map((r) => ({ id: r.rule_id, status: r.status, version: r.version, author: r.assigned_by, text: r.text, })), -})); +}); diff --git a/harnesses/cursor/extension/scripts/load-session-summary.mjs b/harnesses/cursor/extension/scripts/load-session-summary.mjs index 011c4866..f37f4df0 100644 --- a/harnesses/cursor/extension/scripts/load-session-summary.mjs +++ b/harnesses/cursor/extension/scripts/load-session-summary.mjs @@ -1,16 +1,25 @@ -import { readFileSync, existsSync } from "node:fs"; +/** + * Load a session summary: remote Deeplake memory table first, local disk + * fallback second. + * + * Self-contained: reads credentials and queries the Deeplake HTTP endpoint + * directly (see lib/deeplake.mjs). Mirrors the resolution order in the core + * memory summary path. Prints a SessionSummaryResult JSON to stdout. + */ +import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { loadCredentials } from "../../../../src/commands/auth-creds.js"; -import { loadConfig } from "../../../../src/config.js"; -import { DeeplakeApi } from "../../../../src/deeplake-api.js"; -import { sqlStr, sqlIdent } from "../../../../src/utils/sql.js"; +import { loadCreds, query, sqlIdent, sqlStr, tableNames } from "./lib/deeplake.mjs"; const sessionId = process.argv[2]; const userArg = process.argv[3] ?? ""; +function emit(obj) { + process.stdout.write(JSON.stringify(obj)); +} + if (!sessionId || !/^[a-zA-Z0-9_-]{1,128}$/.test(sessionId)) { - console.log(JSON.stringify({ text: null, source: "invalid", message: "Invalid session id." })); + emit({ text: null, source: "invalid", message: "Invalid session id." }); process.exit(0); } @@ -32,30 +41,21 @@ function readLocal(userName) { } async function readRemote(creds) { - if (!creds?.token || !creds.userName || !creds.orgId) return { text: null, unreachable: false }; - const cfg = loadConfig(); + if (!creds || !creds.userName) return { text: null, unreachable: false }; let table; try { - table = sqlIdent(cfg?.tableName ?? "memory"); + table = sqlIdent(tableNames().memory); } catch { return { text: null, unreachable: false }; } - const api = new DeeplakeApi( - creds.token, - creds.apiUrl ?? "https://api.deeplake.ai", - creds.orgId, - creds.workspaceId ?? "default", - table, - ); const vpath = `/summaries/${creds.userName}/${sessionId}.md`; try { - const rows = await Promise.race([ - api.query( - `SELECT summary FROM "${table}" WHERE path = '${sqlStr(vpath)}' AND author = '${sqlStr(creds.userName)}' ` + - `AND summary <> '' ORDER BY last_update_date DESC LIMIT 1`, - ), - new Promise((resolve) => setTimeout(() => resolve(null), 4000)), - ]); + const rows = await query( + creds, + `SELECT summary FROM "${table}" WHERE path = '${sqlStr(vpath)}' AND author = '${sqlStr(creds.userName)}' ` + + `AND summary <> '' ORDER BY last_update_date DESC LIMIT 1`, + 4000, + ); if (!rows || rows.length === 0) return { text: null, unreachable: false }; const summary = rows[0]?.summary; return { text: typeof summary === "string" && summary.trim() ? summary : null, unreachable: false }; @@ -64,36 +64,28 @@ async function readRemote(creds) { } } -const creds = loadCredentials(); +const creds = loadCreds(); const userName = userArg || creds?.userName || "unknown"; const remote = await readRemote(creds); if (remote.text) { - console.log(JSON.stringify({ text: remote.text, source: "remote", message: null })); + emit({ text: remote.text, source: "remote", message: null }); process.exit(0); } const local = readLocal(userName); if (local) { - console.log(JSON.stringify({ text: local, source: "local", message: null })); + emit({ text: local, source: "local", message: null }); process.exit(0); } if (remote.unreachable) { - console.log( - JSON.stringify({ - text: null, - source: "unreachable", - message: "Memory table unreachable. Showing no summary until connectivity returns.", - }), - ); + emit({ + text: null, + source: "unreachable", + message: "Memory table unreachable. Showing no summary until connectivity returns.", + }); process.exit(0); } -console.log( - JSON.stringify({ - text: null, - source: "missing", - message: `No summary found for session ${sessionId}.`, - }), -); +emit({ text: null, source: "missing", message: `No summary found for session ${sessionId}.` }); diff --git a/harnesses/cursor/extension/scripts/load-sessions.mjs b/harnesses/cursor/extension/scripts/load-sessions.mjs index 305c3a8e..0bc9da47 100644 --- a/harnesses/cursor/extension/scripts/load-sessions.mjs +++ b/harnesses/cursor/extension/scripts/load-sessions.mjs @@ -1,10 +1,65 @@ -import { readUsageRecords } from "../../../../src/notifications/usage-tracker.js"; - -const records = readUsageRecords().slice(-20).reverse(); -console.log(JSON.stringify(records.map((r) => ({ - sessionId: r.sessionId, - endedAt: r.endedAt, - memorySearchCount: r.memorySearchCount, - project: r.project ?? null, - hadRecall: (r.memorySearchCount ?? 0) > 0, -})))); +/** + * List recent captured sessions from the Deeplake `sessions` table. + * + * Self-contained: reads credentials and queries the Deeplake HTTP endpoint + * directly. Mirrors the grouped listing in core src/commands/session-prune.ts + * (one row per session path, newest first). Sessions are scoped to the + * current repo's project when possible, falling back to all of the user's + * recent sessions. Prints RecentSession[] JSON to stdout. + */ +import { basename } from "node:path"; +import { loadCreds, query, sqlIdent, sqlStr, tableNames } from "./lib/deeplake.mjs"; + +const cwd = process.argv[2] || process.cwd(); + +function emit(arr) { + process.stdout.write(JSON.stringify(arr)); +} + +/** /sessions//___.jsonl -> sessionId */ +function extractSessionId(path) { + const m = String(path).match(/\/sessions\/[^/]+\/[^/]+_([^.]+)\.jsonl$/); + if (m) return m[1]; + return String(path).split("/").pop()?.replace(/\.jsonl$/, "") ?? String(path); +} + +const creds = loadCreds(); +if (!creds || !creds.userName) { + emit([]); + process.exit(0); +} + +const table = sqlIdent(tableNames().sessions); + +let rows; +try { + rows = await query( + creds, + `SELECT path, COUNT(*) as cnt, MAX(creation_date) as last_event, MAX(project) as project ` + + `FROM "${table}" WHERE author = '${sqlStr(creds.userName)}' ` + + `GROUP BY path ORDER BY last_event DESC LIMIT 100`, + ); +} catch { + emit([]); + process.exit(0); +} + +const all = rows.map((r) => { + const eventCount = Number(r.cnt) || 0; + return { + sessionId: extractSessionId(r.path), + endedAt: String(r.last_event ?? ""), + eventCount, + memorySearchCount: eventCount, + project: r.project ? String(r.project) : null, + hadRecall: eventCount > 0, + }; +}); + +// Prefer sessions from this repo's project; fall back to all when the +// project name does not line up with the captured `project` column. +const repoProject = basename(cwd); +const scoped = all.filter((s) => s.project && s.project === repoProject); +const result = (scoped.length > 0 ? scoped : all).slice(0, 20); + +emit(result); diff --git a/harnesses/cursor/extension/src/bridge/skill-sync.ts b/harnesses/cursor/extension/src/bridge/skill-sync.ts index 7d584a13..3d4afe3e 100644 --- a/harnesses/cursor/extension/src/bridge/skill-sync.ts +++ b/harnesses/cursor/extension/src/bridge/skill-sync.ts @@ -309,6 +309,7 @@ export function listLocalSkillsForPromoter(): Array<{ const workspace = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const globalRoot = canonicalSkillsRoot(); + const seen = new Set(); for (const name of listMinedSkillNames(workspace)) { const projectPath = workspace ? join(workspace, ".claude", "skills", name) : ""; const globalPath = join(globalRoot, name); @@ -319,6 +320,7 @@ export function listLocalSkillsForPromoter(): Array<{ path: projectPath, shareScope: readSkillShareScope(projectPath), }); + seen.add(name); } else if (existsSync(join(globalPath, "SKILL.md"))) { out.push({ dirName: name, @@ -326,8 +328,27 @@ export function listLocalSkillsForPromoter(): Array<{ path: globalPath, shareScope: readSkillShareScope(globalPath), }); + seen.add(name); } } + + // Reconcile with what the settings "skills synced" count reports: pulled + // skills (`--` dirs under the canonical root) are real, + // installed skills too. Surfacing them here keeps the Skills tab from + // claiming "no skills" while settings shows a non-zero synced count. + for (const dirName of listPulledSkillDirs(globalRoot)) { + if (seen.has(dirName)) continue; + const pulledPath = join(globalRoot, dirName); + if (!existsSync(join(pulledPath, "SKILL.md"))) continue; + const declared = readSkillShareScope(pulledPath); + out.push({ + dirName, + scope: "global", + path: pulledPath, + shareScope: declared === "unknown" ? "team" : declared, + }); + seen.add(dirName); + } return out; } diff --git a/harnesses/cursor/extension/src/webview/DashboardPanel.ts b/harnesses/cursor/extension/src/webview/DashboardPanel.ts index 7b7c34de..8246f9ec 100644 --- a/harnesses/cursor/extension/src/webview/DashboardPanel.ts +++ b/harnesses/cursor/extension/src/webview/DashboardPanel.ts @@ -8,9 +8,9 @@ import { loadGraphSnapshotFromEnvelope, parseGraphSnapshot } from "../graph/snap import type { GraphSnapshot } from "../graph/types"; import { logError } from "../utils/output"; import { getDashboardHtml } from "./html/dashboard-shell"; -import { loadDashboardData, loadRecentSessions, loadRulesList, loadSessionSummary, runHivemindCli, runHivemindCliAsync, invalidateOrgStatsCache } from "./data-bridge"; +import { loadDashboardData, loadGoalsList, loadRecentSessions, loadRulesList, loadSessionSummary, runHivemindCli, runHivemindCliAsync, invalidateOrgStatsCache } from "./data-bridge"; -type DashboardPane = "kpi" | "settings" | "sessions" | "graph" | "rules" | "skills"; +type DashboardPane = "kpi" | "settings" | "sessions" | "graph" | "rules" | "skills" | "goals"; const NEXT_STEPS_SECTION_RE = /^##\s+Next Steps\s*$/im; @@ -25,6 +25,7 @@ interface WebviewInboundMessage { orgName?: string; workspaceName?: string; rulesStatus?: string; + goalsFilter?: string; } interface ParsedRule { @@ -67,6 +68,7 @@ class DashboardController { private editorSync: EditorGraphSyncHandle | undefined; private snapshot: GraphSnapshot | null = null; private rulesStatusFilter = "active"; + private goalsFilter: "mine" | "all" = "mine"; private promotedSteps: Set; private refreshInFlight = false; private lastDashboardEnvelope: Awaited> | null = null; @@ -106,6 +108,7 @@ class DashboardController { await this.pushSessions(); await this.pushRules(); await this.pushSkills(); + await this.pushGoals(); } private post(message: Record): void { @@ -203,6 +206,22 @@ class DashboardController { if (msg.rulesStatus) this.rulesStatusFilter = msg.rulesStatus; await this.pushRules(); break; + case "goalsList": + if (msg.goalsFilter === "all" || msg.goalsFilter === "mine") this.goalsFilter = msg.goalsFilter; + await this.pushGoals(); + break; + case "goalAdd": + if (msg.text) { + const result = await runHivemindCli(["goal", "add", msg.text], workspaceRoot()); + this.post({ + type: "actionResult", + target: "goals", + ok: result.ok, + message: result.ok ? "Goal added." : result.stderr || "Failed to add goal.", + }); + if (result.ok) await this.pushGoals(); + } + break; case "rulesAdd": if (msg.text) { const result = await runHivemindCli(["rules", "add", msg.text, "--scope", "team"], workspaceRoot()); @@ -277,6 +296,7 @@ class DashboardController { ok: result.ok, message: result.ok ? `Goal created: "${msg.text}"` : result.stderr || "Failed to create goal.", }); + if (result.ok) await this.pushGoals(); } break; case "switchWorkspace": @@ -427,6 +447,20 @@ class DashboardController { this.post({ type: "rules", rules: result.rules, loggedOut: false }); } + private async pushGoals(): Promise { + const result = await loadGoalsList(this.goalsFilter); + if (result.loggedOut) { + this.post({ + type: "goals", + goals: [], + loggedOut: true, + message: result.message ?? "Log in with `hivemind login` to track goals.", + }); + return; + } + this.post({ type: "goals", goals: result.goals, loggedOut: false }); + } + private async pushSkills(): Promise { const auth = await detectAuthState(); if (auth.state !== "logged_in") { diff --git a/harnesses/cursor/extension/src/webview/data-bridge.ts b/harnesses/cursor/extension/src/webview/data-bridge.ts index 971bcc6a..9f86ec47 100644 --- a/harnesses/cursor/extension/src/webview/data-bridge.ts +++ b/harnesses/cursor/extension/src/webview/data-bridge.ts @@ -1,4 +1,5 @@ import { spawn, execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from "node:fs"; import { homedir } from "node:os"; import { basename, join } from "node:path"; @@ -44,10 +45,17 @@ export interface RecentSession { sessionId: string; endedAt: string; memorySearchCount: number; + eventCount?: number; project?: string | null; hadRecall?: boolean; } +export interface GoalsListResult { + loggedOut: boolean; + goals: Array<{ goalId: string; owner: string; status: string; text: string }>; + message?: string; +} + function repoRootFromExtension(): string { return join(__dirname, "..", "..", "..", ".."); } @@ -86,15 +94,48 @@ function readUsageRecords(): Array<{ endedAt: string; sessionId: string; memoryS } } +/** Collapse the surface forms of a git remote URL. Mirrors core src/utils/repo-identity.ts. */ +function normalizeGitRemoteUrl(url: string): string { + let s = url.trim(); + const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i); + const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null; + if (schemeMatch) s = s.slice(schemeMatch[0].length); + if (!scheme) { + const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/); + if (scp) s = `${scp[1]}/${scp[2]}`; + } + s = s.replace(/^[^@/]+@/, ""); + const defaultPorts: Record = { http: "80", https: "443", ssh: "22", git: "9418" }; + if (scheme && defaultPorts[scheme]) { + s = s.replace(new RegExp(`^([^/]+):${defaultPorts[scheme]}(/|$)`), "$1$2"); + } + s = s.replace(/\.git\/?$/i, ""); + s = s.replace(/\/+$/, ""); + return s.toLowerCase(); +} + +/** + * Stable per-repo key: sha1 of the normalized git remote (fallback to cwd), + * first 16 hex chars. Mirrors core deriveProjectKey so the fallback resolves + * the SAME ~/.hivemind/graphs/ dir that `hivemind graph build` writes. + */ function deriveProjectKey(cwd: string): { key: string; project: string } { + let project = basename(cwd); + let signature: string | null = null; try { - const out = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" }).trim(); - const key = Buffer.from(out).toString("hex").slice(0, 16); - return { key, project: basename(out) }; + const top = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" }).trim(); + if (top) project = basename(top); } catch { - const key = Buffer.from(cwd).toString("hex").slice(0, 16); - return { key, project: basename(cwd) }; + /* not a git repo */ } + try { + const raw = execFileSync("git", ["config", "--get", "remote.origin.url"], { cwd, encoding: "utf-8" }).trim(); + signature = raw ? normalizeGitRemoteUrl(raw) : null; + } catch { + signature = null; + } + const key = createHash("sha1").update(signature ?? cwd).digest("hex").slice(0, 16); + return { key, project }; } function resolveSnapshot(repoDir: string): DashboardGraphSummary | null { @@ -212,7 +253,7 @@ export async function loadRecentSessions(_cwd: string): Promise const scriptPath = join(__dirname, "..", "..", "scripts", "load-sessions.mjs"); if (existsSync(scriptPath)) { return new Promise((resolve) => { - const child = spawn(process.execPath, [scriptPath], { + const child = spawn(process.execPath, [scriptPath, _cwd], { cwd: repoRootFromExtension(), env: { ...process.env, NODE_OPTIONS: "" }, stdio: ["ignore", "pipe", "pipe"], @@ -279,6 +320,32 @@ export async function loadRulesList(status: string, limit = 10): Promise { + const scriptPath = join(__dirname, "..", "..", "scripts", "load-goals.mjs"); + if (!existsSync(scriptPath)) { + return { loggedOut: true, goals: [], message: "Goals loader unavailable." }; + } + return new Promise((resolve) => { + const child = spawn(process.execPath, [scriptPath, filter], { + cwd: repoRootFromExtension(), + env: { ...process.env, NODE_OPTIONS: "" }, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.on("close", () => { + try { + resolve(JSON.parse(stdout) as GoalsListResult); + } catch { + resolve({ loggedOut: false, goals: [], message: "Failed to parse goals." }); + } + }); + child.on("error", () => resolve({ loggedOut: false, goals: [], message: "Failed to load goals." })); + }); +} + /** Load session summary from remote memory table with local disk fallback. */ export async function loadSessionSummary(sessionId: string, cwd: string): Promise { const creds = readJson<{ userName?: string }>(credentialsPath()); diff --git a/harnesses/cursor/extension/src/webview/html/dashboard-shell.ts b/harnesses/cursor/extension/src/webview/html/dashboard-shell.ts index fdd7ee97..278c9985 100644 --- a/harnesses/cursor/extension/src/webview/html/dashboard-shell.ts +++ b/harnesses/cursor/extension/src/webview/html/dashboard-shell.ts @@ -106,6 +106,7 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U +
@@ -191,6 +192,19 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U
    +
    +
    + + + +
    + +
      +

      +

      Local skills available under Claude/Cursor skill directories. Use "Promote to team" to share a skill so teammates pull it on their next session.

      @@ -256,6 +270,27 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U document.getElementById("rules-status-filter").addEventListener("change", (e) => { post("rulesList", { rulesStatus: e.target.value }); }); + document.getElementById("goals-filter").addEventListener("change", (e) => { + post("goalsList", { goalsFilter: e.target.value }); + }); + document.getElementById("btn-goal-add").addEventListener("click", () => { + const text = document.getElementById("goal-text").value.trim(); + const errEl = document.getElementById("goal-add-error"); + if (!text) return; + if (text.includes("\\n") || text.includes("\\r")) { + errEl.hidden = false; + errEl.textContent = "Goal text must be a single line (no newlines)."; + return; + } + if (text.length > 2000) { + errEl.hidden = false; + errEl.textContent = "Goal text exceeds 2000 characters."; + return; + } + errEl.hidden = true; + post("goalAdd", { text }); + document.getElementById("goal-text").value = ""; + }); document.getElementById("btn-org-switch").addEventListener("click", () => { const orgName = document.getElementById("org-switch-input").value.trim(); if (orgName) post("switchOrg", { orgName }); @@ -337,8 +372,9 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U ul.innerHTML = sessions.map(s => { const recall = s.hadRecall ? "recalled" : "no recalls"; const proj = s.project ? " · " + s.project : ""; + const events = s.eventCount != null ? s.eventCount : s.memorySearchCount; return '
    • ' + esc(s.sessionId.slice(0, 8)) + '… · ' + - esc(s.endedAt) + proj + ' · ' + esc(recall) + ' · searches: ' + esc(s.memorySearchCount) + '
    • '; + esc(s.endedAt) + proj + ' · ' + esc(recall) + ' · events: ' + esc(events) + ''; }).join(""); ul.querySelectorAll("li[data-session]").forEach(li => { li.addEventListener("click", () => post("openSession", { sessionId: li.dataset.session })); @@ -390,6 +426,21 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U }); } + function renderGoals(goals) { + const ul = document.getElementById("goals-list"); + if (!goals || goals.length === 0) { + ul.innerHTML = '
    • No goals yet.
    • '; + return; + } + ul.innerHTML = goals.map(g => + '
    • ' + + '' + esc(g.status || "open") + ' ' + + '' + esc(g.text || g.goalId) + '' + + (g.owner ? ' · ' + esc(g.owner) + '' : '') + + '
    • ' + ).join(""); + } + function renderSkills(skills) { const root = document.getElementById("skills-list"); if (!skills || skills.length === 0) { @@ -705,6 +756,13 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U renderSkills(msg.skills); } break; + case "goals": + if (msg.loggedOut) { + document.getElementById("goals-list").innerHTML = '
    • ' + esc(msg.message || "Log in required.") + '
    • '; + } else { + renderGoals(msg.goals); + } + break; case "impact": state.impact = msg.impact; const caveat = document.getElementById("impact-caveat"); @@ -733,6 +791,20 @@ export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.U const btn = document.getElementById("btn-graph-build"); if (btn) btn.disabled = !!msg.inProgress; document.getElementById("graph-build-result").textContent = msg.message || ""; + const inlineBtn = document.getElementById("btn-graph-build-inline"); + if (inlineBtn) { + inlineBtn.disabled = !!msg.inProgress; + inlineBtn.textContent = msg.inProgress ? "Building graph…" : "Build graph now"; + } + const emptyBanner = document.getElementById("graph-empty-banner"); + if (emptyBanner && (msg.inProgress || !msg.ok)) { + emptyBanner.hidden = false; + emptyBanner.textContent = msg.message || ""; + } + } + if (msg.target === "goals") { + const el = document.getElementById("goal-add-result"); + if (el) el.textContent = msg.message || ""; } if (msg.target === "skillSync") { document.getElementById("skill-sync-result").textContent = msg.message || "";