feat(web): project home page — connect-an-agent guide + embedded Insights

Clicking a project now lands on an index page (/<project-id>) 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.
- /<project-id>/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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
This commit is contained in:
Snow Lee
2026-07-12 16:53:47 -07:00
co-authored by Claude Fable 5
parent bc0f0c5a66
commit 46f48e4c82
2 changed files with 161 additions and 4 deletions
+141 -4
View File
@@ -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 = `<div class="empty">Select a file to read it.<br><span class="empty-hint">On a phone, tap ☰ to browse.</span></div>`;
$("content").innerHTML = `<div class="empty">Loading…</div>`;
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 (/<project-id>): 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
+20
View File
@@ -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; }