From 46f48e4c822d98b5169dc3a76f5b3f720e645e02 Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Sun, 12 Jul 2026 16:53:47 -0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20project=20home=20page=20=E2=80=94?= =?UTF-8?q?=20connect-an-agent=20guide=20+=20embedded=20Insights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a project now lands on an index page (/) instead of redirecting to Insights: a tabbed guide for mounting the project locally and wiring it to a coding agent, with the Insights dashboard embedded below for admins/org-owners. - Claude Code & Cowork share one tab guiding through the plugin (/plugin marketplace add + /beardrive:install with this hub's URL and the project id pre-filled); Hermes and Codex get the four-step CLI flow (install, login, init, hooks install) with real copyable commands. - //insights stays a dedicated deep-linkable route; the sidebar project name is now a home link. - Copy buttons, localStorage-persisted tab choice, scroll/back-forward behavior consistent with the other views. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt --- internal/webapp/static/app.js | 145 ++++++++++++++++++++++++++++++- internal/webapp/static/style.css | 20 +++++ 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js index e89d256..34d32ab 100644 --- a/internal/webapp/static/app.js +++ b/internal/webapp/static/app.js @@ -159,20 +159,23 @@ function selectProject(p, route) { lastHeatJSON = ""; apiBase = "/api/p/" + p.id + "/"; $("vault-name").textContent = p.name; + // The project name doubles as a "home" link back to the index page. + $("vault-name").classList.add("vault-link"); + $("vault-name").onclick = () => { showProjectHome(); closeSidebarOnMobile(); }; document.title = p.name + " — BearDrive"; currentPath = null; $("crumb").textContent = ""; $("meta").textContent = ""; $("download").hidden = true; $("content").className = "view"; - $("content").innerHTML = `
Select a file to read it.
On a phone, tap ☰ to browse.
`; + $("content").innerHTML = `
Loading…
`; loadProjects(); // refresh active highlight updateOrgBar(); initUpload(); initHistory(); updateShareButton(); - // Landing view (applyRoute with an empty route): admins/owners get the - // Insights dashboard instead of an empty "select a file" pane. + // Landing view (applyRoute with an empty route): the project home page — + // the connect-an-agent guide, with Insights below for admins/owners. refreshTree().then(() => applyRoute(route || {})); if (!route || (!route.path && !route.view)) pushURL("/" + p.id); } @@ -709,7 +712,7 @@ function applyRoute(r) { return showHistory(hq, false); } if (r.path) return openPath(r.path); - if (canSeeInsights()) showInsights("replace"); + if (serverConfig.mode === "hub" && currentProject) showProjectHome("replace"); } /* Per-route scroll positions of the content pane, so back/forward returns @@ -1220,6 +1223,140 @@ function updateShareButton() { }; } +/* ---- project home ---- + The index page of a project (/): how to mount the project as a + local folder and connect a coding agent, with the Insights dashboard + embedded below for admins/org-owners. The guide shows real, copyable + commands with this hub's URL and this project's id filled in. */ + +const GUIDE_AGENTS = [ + { key: "claude", label: "Claude Code & Cowork" }, + { key: "hermes", label: "Hermes", hook: "hermes", + note: "Registers BearDrive's hooks in Hermes's config: pull before every turn, push after edits " + + "with a session note, and report file reads to Insights." }, + { key: "codex", label: "Codex", hook: "codex", + note: "Registers hooks in .codex/hooks.json.", + extra: "Run /hooks inside Codex once to trust the project's .codex layer — after that every turn " + + "pulls, edits push automatically, and reads are reported to Insights." }, +]; + +let guideAgent = "claude"; +try { guideAgent = localStorage.getItem("bdrive-guide-agent") || "claude"; } catch { /* private mode */ } + +function guideSteps(agent) { + const origin = location.origin, pid = currentProject.id; + + // Claude Code and Claude Cowork share one plugin: install it once, then + // /beardrive:install does the whole setup conversationally. + if (agent.key === "claude") { + return [ + { title: "Add the BearDrive plugin", + desc: "One time, in any Claude Code session. The plugin ships the beardrive skill, the /beardrive " + + "commands, and turn-boundary sync hooks — and Claude Cowork shares the same plugins, so " + + "installing it once covers both.", + code: "/plugin marketplace add runbear-io/beardrive\n/plugin install beardrive@beardrive" }, + { title: "Set up this project conversationally", + desc: "In a Claude Code or Cowork session in the folder where you want the files, run:", + code: "/beardrive:install connect to " + origin + ", project " + pid, + extra: "Claude installs the CLI, signs this machine in, mounts the project, and registers the " + + "sync hooks — pull the latest before every turn, push after edits (stamped with the session " + + "that made them), and report file reads to Insights. It asks before anything it changes." }, + ]; + } + + const slug = (currentProject.name || "project").toLowerCase().replace(/[^a-z0-9._-]+/g, "-") || "project"; + return [ + { title: "Install the BearDrive CLI", + desc: "One static binary. Homebrew on macOS and Linux; releases and `go install` also work.", + code: "brew install runbear-io/tap/beardrive" }, + { title: "Sign in to this hub", + desc: "Opens the browser once and stores a device token on this machine — the synced folder itself never holds credentials.", + code: "bdrive login " + origin }, + { title: "Mount the project into a local folder", + desc: "Run it where you want the files. An existing folder works too — contents merge, and re-running init later (or after moving the folder) just resumes.", + code: "mkdir -p ~/" + slug + " && cd ~/" + slug + "\nbdrive init --project " + pid }, + { title: "Connect " + agent.label, + desc: agent.note, + code: "bdrive hooks install --agent " + agent.hook, + extra: agent.extra }, + ]; +} + +function renderConnectGuide(content) { + const wrap = el(content, "div", "guide"); + el(wrap, "h1", "in-title", currentProject.name); + el(wrap, "p", "dl-sub", + "Mount this project as a folder on any machine and connect your coding agent: files sync both " + + "ways in the background, every change is journaled with who made it, and agent reads feed Insights."); + const tabs = el(wrap, "div", "gd-tabs"); + const body = el(wrap, "div", "gd-body"); + const render = () => { + const agent = GUIDE_AGENTS.find((a) => a.key === guideAgent) || GUIDE_AGENTS[0]; + for (const b of tabs.children) b.classList.toggle("active", b.dataset.key === agent.key); + body.innerHTML = ""; + guideSteps(agent).forEach((s, i) => { + const step = el(body, "div", "gd-step"); + const head = el(step, "div", "gd-step-head"); + el(head, "span", "gd-num", String(i + 1)); + el(head, "span", "gd-step-title", s.title); + if (s.desc) el(step, "p", "gd-desc", s.desc); + if (s.code) { + const pre = el(step, "pre", "gd-code"); + el(pre, "code", null, s.code); + const btn = el(pre, "button", "gd-copy", "Copy"); + btn.onclick = async () => { + btn.textContent = (await copyText(s.code)) ? "Copied" : "Copy failed"; + setTimeout(() => { btn.textContent = "Copy"; }, 1400); + }; + } + if (s.extra) el(step, "p", "gd-desc gd-extra", s.extra); + }); + el(body, "p", "gd-done", + "That's it — the folder now syncs on its own. Every agent turn starts from the latest state, " + + "edits appear here (and on every teammate's mount) within seconds, and what your agents read " + + "shows up in Insights."); + }; + for (const a of GUIDE_AGENTS) { + const b = el(tabs, "button", "gd-tab", a.label); + b.dataset.key = a.key; + b.onclick = () => { + guideAgent = a.key; + try { localStorage.setItem("bdrive-guide-agent", a.key); } catch { /* private mode */ } + render(); + }; + } + render(); +} + +async function showProjectHome(push = true) { + if (!(serverConfig.mode === "hub" && currentProject)) return; + if (push === "replace") replaceURL("/" + currentProject.id); + else if (push) pushURL("/" + currentProject.id); + const restoreTop = pendingScroll != null ? pendingScroll : 0; + pendingScroll = null; + currentPath = null; + markActive(); + $("crumb").textContent = currentProject.name; + $("meta").textContent = ""; + $("download").hidden = true; + $("more-btn").hidden = true; + const content = $("content"); + content.className = "view"; + content.innerHTML = ""; + renderConnectGuide(content); + if (canSeeInsights()) { + const host = el(content, "div", "home-insights"); + await refreshHeat(true); + insightsDevices = null; + try { + insightsDevices = (await getJSON(apiBase + "heat?by=device&days=30")).devices || []; + } catch { /* older server: the coverage section simply doesn't render */ } + if (!host.isConnected) return; // user navigated away during the fetches + renderInsights(host, "all"); + } + content.scrollTo({ top: restoreTop, behavior: "instant" }); +} + /* ---- insights: the read×write matrix ---- Every file plotted by how much it is read (30 days, from the heat API) against how long since it last changed (from the tree). The hot-but-stale diff --git a/internal/webapp/static/style.css b/internal/webapp/static/style.css index 1fa24c5..75c03e5 100644 --- a/internal/webapp/static/style.css +++ b/internal/webapp/static/style.css @@ -275,6 +275,26 @@ button, input, a.btn { font-family: inherit; } .hentry.clickable:hover { background: var(--hover); } .dl-more { margin-top: 10px; } +/* ---- project home: the connect-an-agent guide ---- */ +#vault-name.vault-link { cursor: pointer; } +#vault-name.vault-link:hover { color: var(--accent-bright); } +.guide { max-width: 760px; margin: 0 auto; } +.gd-tabs { display: flex; gap: 2px; margin: 20px 0 16px; border-bottom: 1px solid var(--border); overflow-x: auto; } +.gd-tab { font: inherit; font-size: 13px; font-weight: 600; padding: 7px 12px 9px; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; color: var(--text-faint); cursor: pointer; white-space: nowrap; } +.gd-tab:hover { color: var(--text); } +.gd-tab.active { color: var(--accent-bright); border-bottom-color: var(--accent); } +.gd-step { margin: 0 0 18px; } +.gd-step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 3px; } +.gd-num { flex: none; width: 22px; height: 22px; border-radius: 50%; background: var(--glow); color: var(--accent-bright); font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; } +.gd-step-title { font-weight: 600; font-size: 14px; color: var(--text); } +.gd-desc { margin: 2px 0 8px 32px; color: var(--text-faint); font-size: 13px; line-height: 1.5; } +.gd-extra { font-size: 12.5px; margin-top: 6px; } +.gd-code { position: relative; margin: 6px 0 6px 32px; padding: 10px 72px 10px 12px; background: var(--bg-raise); border: 1px solid var(--border); border-radius: var(--r-card); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.6; color: var(--text); overflow-x: auto; white-space: pre; } +.gd-copy { position: absolute; top: 7px; right: 7px; font: inherit; font-family: inherit; font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--border-2); background: var(--surface); color: var(--text-faint); cursor: pointer; } +.gd-copy:hover { color: var(--accent-bright); border-color: var(--accent-dim); } +.gd-done { margin: 22px 0 8px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--r-card); background: var(--bg-side); color: var(--text-faint); font-size: 13px; line-height: 1.5; } +.home-insights { margin-top: 30px; padding-top: 22px; border-top: 1px solid var(--border); } + /* ---- insights (read×write matrix) ---- */ .insights { max-width: 760px; margin: 0 auto; } .in-title { font-size: 21px; font-weight: 640; letter-spacing: -.02em; margin: 0 0 4px; color: #f4f6f9; }