UNPKG

@blundergoat/goat-flow

Version:

AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.

310 lines (309 loc) 11.9 kB
"use strict"; /** * Project-list, project-browser, and dashboard-state helpers. * Loaded as a classic script and called by thin Alpine methods in app.ts. */ function dashboardRememberProjectIdentity(ctx, project) { if (!project.identity) return; const aliases = project.paths && project.paths.length > 0 ? project.paths : [project.path]; ctx.projectIdentities[project.path] = project.identity; for (const alias of aliases) { ctx.projectIdentities[alias] = project.identity; } } function dashboardRememberProjectIdentities(ctx, projects) { for (const project of projects) { dashboardRememberProjectIdentity(ctx, project); } } /** Decode one persisted project record while dropping entries without identity or path. */ function dashboardReadProjectRecord(value) { if (!isRecord(value)) return null; const path = readString(value.currentPath); const identity = readString(value.identity); if (!path || !identity) return null; const entry = { path, paths: readStringArray(value.paths), identity, state: "...", action: "...", details: "Not audited", }; if (value.identitySource === "git-remote" || value.identitySource === "goat-marker" || value.identitySource === "path") { entry.identitySource = value.identitySource; } const remoteUrlHash = readString(value.remoteUrlHash); if (remoteUrlHash) entry.remoteUrlHash = remoteUrlHash; const markerId = readString(value.markerId); if (markerId) entry.markerId = markerId; return entry; } /** Decode the persisted project-record map into dashboard project rows. */ function dashboardReadProjectRecords(value) { if (!isRecord(value)) return []; return Object.values(value) .map((project) => dashboardReadProjectRecord(project)) .filter((project) => project !== null); } function dashboardContainsProjectPath(projects, path) { return projects.some((project) => project.path === path || project.paths?.includes(path)); } /** Open the project browser at the current workspace path. */ async function dashboardOpenBrowser(ctx) { ctx.showBrowser = !ctx.showBrowser; if (ctx.showBrowser) await ctx.browseTo(ctx.projectPath); } /** Load child directories for the requested browser path. */ async function dashboardBrowseTo(ctx, path) { try { const res = await dashboardFetch(`/api/browse?path=${encodeURIComponent(path)}`); const payload = readRecord(await res.json(), "Browse response"); const error = readErrorMessage(payload); if (error) { ctx.showToast(error, true); return; } ctx.browserCurrent = readString(payload.current); ctx.browserParent = readString(payload.parent); ctx.browserDirs = Array.isArray(payload.dirs) ? payload.dirs .map((dir) => readBrowseDir(dir)) .filter((dir) => dir !== null) : []; } catch { ctx.showToast("Browse failed", true); } } /** Set a browsed directory as the active project. */ function dashboardSelectDir(ctx, dir) { if (dir.isProject) { ctx.projectPath = dir.path; ctx.showBrowser = false; void ctx.runAudit(); } else { void ctx.browseTo(dir.path); } } /** Add one project to the saved workspace list and fetch its status. */ async function dashboardAddProject(ctx) { if (!ctx.newProjectPath) return; if (ctx.projectsList.some((p) => p.path === ctx.newProjectPath)) { ctx.showAddProject = false; ctx.newProjectPath = ""; return; } ctx.projectsList.push({ path: ctx.newProjectPath, state: "...", action: "...", details: "Auditing...", }); ctx.showAddProject = false; try { const res = await dashboardFetch(`/api/projects/status?paths=${encodeURIComponent(ctx.newProjectPath)}`); const payload = readRecord(await res.json(), "Project status response"); const result = Array.isArray(payload.projects) ? readProjectEntry(payload.projects[0]) : null; if (result) { const idx = ctx.projectsList.findIndex((p) => p.path === ctx.newProjectPath || p.path === result.path); if (idx >= 0) ctx.projectsList[idx] = result; dashboardRememberProjectIdentity(ctx, result); } } catch (err) { // Surface, don't swallow: the project was added optimistically with an // "Auditing..." placeholder, so a silent status-fetch failure would strand // it in that state. Non-fatal — the row stays and a later refresh retries. console.warn("[goat-flow] Failed to load status for added project:", err); } ctx.newProjectPath = ""; ctx._saveProjectsList(); } /** Remove a project from the saved workspace list. */ function dashboardRemoveProject(ctx, path) { ctx.projectsList = ctx.projectsList.filter((p) => p.path !== path); ctx._saveProjectsList(); } /** Sort saved projects by the active key and direction. */ function dashboardSortProjects(ctx, key) { if (ctx.projectsSortKey === key) { ctx.projectsSortAsc = !ctx.projectsSortAsc; } else { ctx.projectsSortKey = key; ctx.projectsSortAsc = true; } } /** Sort projects by visible columns while keeping the derived "name" column first-class. */ function dashboardSortedProjectsList(ctx) { const key = ctx.projectsSortKey; const dir = ctx.projectsSortAsc ? 1 : -1; return [...ctx.projectsList].sort((a, b) => { const firstValue = key === "name" ? ctx.displayNameFor(a.path) : a[key]; const secondValue = key === "name" ? ctx.displayNameFor(b.path) : b[key]; return firstValue.localeCompare(secondValue) * dir; }); } /** Refresh audit status for every saved project. */ async function dashboardAuditAllProjects(ctx) { ctx.projectsAuditing = true; try { const paths = ctx.projectsList.map((p) => p.path).join(","); const res = await dashboardFetch(`/api/projects/status?paths=${encodeURIComponent(paths)}`); const payload = readRecord(await res.json(), "Project status response"); if (Array.isArray(payload.projects)) { ctx.projectsList = payload.projects .map((project) => readProjectEntry(project)) .filter((project) => project !== null); dashboardRememberProjectIdentities(ctx, ctx.projectsList); } } catch (err) { // Surface, don't swallow: a failed status refresh previously vanished, so // the projects list silently kept stale rows. Non-fatal — existing rows are // left intact and the next refresh retries. console.warn("[goat-flow] Failed to refresh project statuses:", err); } ctx.projectsAuditing = false; } /** Load saved dashboard state from disk, with localStorage as a migration fallback. */ async function dashboardLoadSavedDashboardState(ctx) { let savedPaths = []; let savedFavorites = []; let savedProjectTitles = {}; let savedProjectRecords = []; let loadedFromServer = false; try { const res = await dashboardFetch("/api/projects/list"); const payload = readRecord(await res.json(), "Dashboard state response"); const paths = readStringArray(payload.paths); const favorites = readStringArray(payload.favorites); const projectRecords = dashboardReadProjectRecords(payload.projects); if (paths.length > 0) { savedPaths = paths; } if (favorites.length > 0) { savedFavorites = favorites; } savedProjectTitles = readStringMap(payload.projectTitles); if (projectRecords.length > 0) { savedProjectRecords = projectRecords; savedPaths = projectRecords.map((project) => project.path); } loadedFromServer = true; } catch { /* server unavailable */ } ctx.projectTitles = savedProjectTitles; ctx.projectIdentities = {}; dashboardRememberProjectIdentities(ctx, savedProjectRecords); const localPaths = readStoredStringArray("goat-flow-projects"); const localFavorites = readStoredStringArray("goat-flow-preset-favorites"); if (savedPaths.length === 0 && localPaths.length > 0) { savedPaths = localPaths; } if (savedFavorites.length === 0 && localFavorites.length > 0) { savedFavorites = localFavorites; } if (!loadedFromServer && localPaths.length > savedPaths.length) { savedPaths = localPaths; } if (!loadedFromServer && localFavorites.length > savedFavorites.length) { savedFavorites = localFavorites; } const launchPath = window.__GOAT_FLOW_DEFAULT_PATH__; if (launchPath && !savedPaths.includes(launchPath) && !dashboardContainsProjectPath(savedProjectRecords, launchPath)) { savedPaths.unshift(launchPath); savedProjectRecords.unshift({ path: launchPath, state: "...", action: "...", details: "Not audited", }); } ctx.presetFavorites = [...new Set(savedFavorites)]; if (savedProjectRecords.length > 0) { ctx.projectsList = savedProjectRecords; } else if (savedPaths.length > 0) { ctx.projectsList = savedPaths.map((path) => ({ path, state: "...", action: "...", details: "Not audited", })); } dashboardRememberProjectIdentities(ctx, ctx.projectsList); if (savedPaths.length > 0 || ctx.presetFavorites.length > 0) { ctx._saveDashboardState(); } } /** Persist the current dashboard state to localStorage and the server store; swallows storage failures. */ function dashboardSaveDashboardState(ctx) { const paths = [ ...new Set(ctx.projectsList.flatMap((p) => p.paths && p.paths.length > 0 ? p.paths : [p.path])), ]; const favorites = [...new Set(ctx.presetFavorites)]; const projectTitles = { ...ctx.projectTitles }; localStorage.setItem("goat-flow-projects", JSON.stringify(paths)); localStorage.setItem("goat-flow-preset-favorites", JSON.stringify(favorites)); dashboardFetch("/api/projects/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paths, favorites, projectTitles }), }).catch((err) => { console.warn("[goat-flow] Failed to persist dashboard state:", err); }); } /** Begin editing the current project's title. */ function dashboardStartEditProjectTitle(ctx) { ctx.projectTitleDraft = ctx.displayNameFor(ctx.projectPath); ctx.editingProjectTitle = true; } /** Commit the inline-edited title for the current project path. */ function dashboardSaveProjectTitle(ctx) { if (!ctx.editingProjectTitle) return; ctx.editingProjectTitle = false; const trimmed = ctx.projectTitleDraft.trim().slice(0, 120); const next = { ...ctx.projectTitles }; const titleKey = ctx.projectKeyFor(ctx.projectPath); if (trimmed.length === 0 || trimmed === getProjectDisplayName(ctx.projectPath)) { Reflect.deleteProperty(next, titleKey); Reflect.deleteProperty(next, ctx.projectPath); } else { next[titleKey] = trimmed; if (titleKey !== ctx.projectPath) { Reflect.deleteProperty(next, ctx.projectPath); } } ctx.projectTitles = next; ctx.projectTitleDraft = ""; ctx._saveDashboardState(); document.title = `${ctx.displayNameFor(ctx.projectPath)} | GOAT Flow`; } /** Discard the inline-edited title. */ function dashboardCancelEditProjectTitle(ctx) { ctx.editingProjectTitle = false; ctx.projectTitleDraft = ""; }