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; }