diff --git a/internal/webapp/frontend/e2e/home.spec.ts b/internal/webapp/frontend/e2e/home.spec.ts
index 4169f19..a33d617 100644
--- a/internal/webapp/frontend/e2e/home.spec.ts
+++ b/internal/webapp/frontend/e2e/home.spec.ts
@@ -175,6 +175,9 @@ test("project menu pages each own a URL: Dashboard, Installation, Settings", asy
await page.waitForURL(`/${pid}/install`);
await expect(page.locator("#crumb")).toHaveText("Installation");
await expect(page.locator("#nav-install")).toHaveClass(/active/);
+ await page.click("#nav-history");
+ await page.waitForURL(`/${pid}/history`);
+ await expect(page.locator("#nav-history")).toHaveClass(/active/);
await page.click("#nav-settings");
await page.waitForURL(`/${pid}/settings`);
await expect(page.locator("#crumb")).toHaveText("Project settings");
diff --git a/internal/webapp/frontend/src/apps/HubApp.tsx b/internal/webapp/frontend/src/apps/HubApp.tsx
index bf5df7c..ea2d958 100644
--- a/internal/webapp/frontend/src/apps/HubApp.tsx
+++ b/internal/webapp/frontend/src/apps/HubApp.tsx
@@ -198,9 +198,11 @@ export default function HubApp({ config }: { config: ServerConfig }) {
? "dashboard"
: route.view === "install"
? "install"
- : route.view === "settings"
- ? "settings"
- : null,
+ : route.view === "history"
+ ? "history"
+ : route.view === "settings"
+ ? "settings"
+ : null,
// Each page is a URL; explicitly close overlay panels because
// same-path navigation doesn't change pathname.
onDashboard: () => {
@@ -213,6 +215,11 @@ export default function HubApp({ config }: { config: ServerConfig }) {
navigate(urlForView("install", current.id));
closeSidebarOnMobile();
},
+ onHistory: () => {
+ setPanel(null);
+ navigate(urlForView("history", current.id));
+ closeSidebarOnMobile();
+ },
onSettings: () => {
setPanel(null);
navigate(urlForView("settings", current.id));
diff --git a/internal/webapp/frontend/src/components/ProjectNav.tsx b/internal/webapp/frontend/src/components/ProjectNav.tsx
index 5772d80..5b8fbe3 100644
--- a/internal/webapp/frontend/src/components/ProjectNav.tsx
+++ b/internal/webapp/frontend/src/components/ProjectNav.tsx
@@ -17,9 +17,10 @@ export function projColor(s: string): string {
}
export interface ProjectMenu {
- active: "dashboard" | "install" | "settings" | null;
+ active: "dashboard" | "install" | "history" | "settings" | null;
onDashboard: () => void;
onInstall: () => void;
+ onHistory: () => void;
onSettings: () => void;
}
@@ -91,6 +92,7 @@ export function ProjectNav({
[
["dashboard", "Dashboard", "dashboard", menu.onDashboard],
["install", "Installation", "terminal", menu.onInstall],
+ ["history", "History", "hist", menu.onHistory],
["settings", "Settings", "gear", menu.onSettings],
] as const
).map(([key, label, icon, onClick]) => (
diff --git a/internal/webapp/static/assets/index-B9q7i7UX.js b/internal/webapp/static/assets/index-T_RFOOBs.js
similarity index 65%
rename from internal/webapp/static/assets/index-B9q7i7UX.js
rename to internal/webapp/static/assets/index-T_RFOOBs.js
index 08f206c..6a37239 100644
--- a/internal/webapp/static/assets/index-B9q7i7UX.js
+++ b/internal/webapp/static/assets/index-T_RFOOBs.js
@@ -1,11 +1,11 @@
-(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))f(d);new MutationObserver(d=>{for(const m of d)if(m.type==="childList")for(const g of m.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&f(g)}).observe(document,{childList:!0,subtree:!0});function r(d){const m={};return d.integrity&&(m.integrity=d.integrity),d.referrerPolicy&&(m.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?m.credentials="include":d.crossOrigin==="anonymous"?m.credentials="omit":m.credentials="same-origin",m}function f(d){if(d.ep)return;d.ep=!0;const m=r(d);fetch(d.href,m)}})();var Gs={exports:{}},Xn={};var yh;function Bv(){if(yh)return Xn;yh=1;var i=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function r(f,d,m){var g=null;if(m!==void 0&&(g=""+m),d.key!==void 0&&(g=""+d.key),"key"in d){m={};for(var A in d)A!=="key"&&(m[A]=d[A])}else m=d;return d=m.ref,{$$typeof:i,type:f,key:g,ref:d!==void 0?d:null,props:m}}return Xn.Fragment=c,Xn.jsx=r,Xn.jsxs=r,Xn}var vh;function Lv(){return vh||(vh=1,Gs.exports=Bv()),Gs.exports}var o=Lv(),Xs={exports:{}},W={};var ph;function Yv(){if(ph)return W;ph=1;var i=Symbol.for("react.transitional.element"),c=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),d=Symbol.for("react.profiler"),m=Symbol.for("react.consumer"),g=Symbol.for("react.context"),A=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),M=Symbol.for("react.lazy"),E=Symbol.for("react.activity"),x=Symbol.iterator;function q(S){return S===null||typeof S!="object"?null:(S=x&&S[x]||S["@@iterator"],typeof S=="function"?S:null)}var z={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,Y={};function $(S,H,L){this.props=S,this.context=H,this.refs=Y,this.updater=L||z}$.prototype.isReactComponent={},$.prototype.setState=function(S,H){if(typeof S!="object"&&typeof S!="function"&&S!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,S,H,"setState")},$.prototype.forceUpdate=function(S){this.updater.enqueueForceUpdate(this,S,"forceUpdate")};function yt(){}yt.prototype=$.prototype;function ot(S,H,L){this.props=S,this.context=H,this.refs=Y,this.updater=L||z}var Ct=ot.prototype=new yt;Ct.constructor=ot,w(Ct,$.prototype),Ct.isPureReactComponent=!0;var Rt=Array.isArray;function et(){}var k={H:null,A:null,T:null,S:null},gt=Object.prototype.hasOwnProperty;function wt(S,H,L){var G=L.ref;return{$$typeof:i,type:S,key:H,ref:G!==void 0?G:null,props:L}}function ue(S,H){return wt(S.type,H,S.props)}function le(S){return typeof S=="object"&&S!==null&&S.$$typeof===i}function _t(S){var H={"=":"=0",":":"=2"};return"$"+S.replace(/[=:]/g,function(L){return H[L]})}var ce=/\/+/g;function Ut(S,H){return typeof S=="object"&&S!==null&&S.key!=null?_t(""+S.key):H.toString(36)}function kt(S){switch(S.status){case"fulfilled":return S.value;case"rejected":throw S.reason;default:switch(typeof S.status=="string"?S.then(et,et):(S.status="pending",S.then(function(H){S.status==="pending"&&(S.status="fulfilled",S.value=H)},function(H){S.status==="pending"&&(S.status="rejected",S.reason=H)})),S.status){case"fulfilled":return S.value;case"rejected":throw S.reason}}throw S}function D(S,H,L,G,J){var lt=typeof S;(lt==="undefined"||lt==="boolean")&&(S=null);var rt=!1;if(S===null)rt=!0;else switch(lt){case"bigint":case"string":case"number":rt=!0;break;case"object":switch(S.$$typeof){case i:case c:rt=!0;break;case M:return rt=S._init,D(rt(S._payload),H,L,G,J)}}if(rt)return J=J(S),rt=G===""?"."+Ut(S,0):G,Rt(J)?(L="",rt!=null&&(L=rt.replace(ce,"$&/")+"/"),D(J,H,L,"",function(Xl){return Xl})):J!=null&&(le(J)&&(J=ue(J,L+(J.key==null||S&&S.key===J.key?"":(""+J.key).replace(ce,"$&/")+"/")+rt)),H.push(J)),1;rt=0;var Vt=G===""?".":G+":";if(Rt(S))for(var Dt=0;Dt One synced folder for your whole team — humans and agents.
Share any file with people as a public URL; give every agent the same
- memory, synced across machines in seconds. Real files. Self-host the whole thing in one Go binary.d(L,V))Gd(L,V))GGoogle Drive for AI agents.Your agent knows what their agent knows.
Gated links your agents hand you. When Claude writes or updates a + file, it drops a link right beside the path — a hub link that only opens for + signed-in teammates on the project. Safe to paste in any internal channel; + the turn-start hook teaches every agent to do it automatically.
+Public share links. Any synced file becomes a rendered page at an - unguessable URL: HTML as a page, markdown Obsidian-style, PDFs inline. - Sandboxed, rate-limited, living until you revoke it.
+Public share links. Need it truly public? Any synced file becomes a + rendered page at an unguessable URL: HTML as a page, markdown Obsidian-style, + PDFs inline. Sandboxed, rate-limited, living until you revoke it.
Teams & organizations. Projects belong to your org; only
@@ -427,17 +427,19 @@ every tool works content-addressed blobs append-only, no locks, offl
CLAUDE CODE CLAUDE CODE & COWORK GET BEARDRIVE The entire product is open source under AGPL-3.0 — server, teams,
- history, share links, nothing held back. The cloud exists for teams who'd
- rather never think about buckets. brew install, run one hub, bdrive init. Files travel to people as URLs; context travels to every agent on the team. Cloud — zero-setup — is on the waitlist. Sign up for the managed cloud and go, or brew install and self-host in one binary. Files travel to people as URLs; context travels to every agent on the team.Your agents become fluent in it.
Explore the plugin →
Self-host everything. Or let us run it.
Open source
@@ -487,16 +489,16 @@ every tool works content-addressed blobs append-only, no locks, offl
BearDrive Cloud
-
-
-
+ bdrive login and gobdrive login, and goShare files. Share context.
-
Self-host in one binary.Share files. Share context.
+
Cloud or self-hosted.