Commit Graph
100 Commits
Author SHA1 Message Date
a05a1f8a52 refactor(auth): one CLI sign-in flow for every provider (webapp.CLIAuth) (#104)
* refactor(auth): one CLI sign-in flow for every provider (webapp.CLIAuth)

The `bdrive login` surface — /auth/cli, /auth/device/<token>, the approval
page both show, /api/auth/exchange and /api/auth/device/{start,poll} — moves
out of BuiltinAuth into its own type. A provider supplies the two things
that actually differ: who the browser session is, and how a device token is
minted.

Nothing changes for a self-hosted hub; this is the same code behind the same
paths. It moves because the managed hub's provider carries its own copy, and
the copy drifted: months after the OSS flow moved to a single approval link
naming the device, that hub was still printing a four-byte code to retype
into a text box. Sharing the implementation is the only fix that stays fixed.

BuiltinAuth's own grant map now holds just what it should: verification and
password-reset links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* fix(architecture): make the webapp-server diagram parse again

Two mermaid syntax errors, so GitHub rendered the first block as an error
box instead of a diagram:

- the CLIAuth class listed its routes as bare lines, and the `{` in
  /auth/device/{token} opens a struct inside a class body — the routes are a
  note now, where prose belongs;
- `note for` strings escaped quotes as \" (mermaid has no backslash escapes,
  so the string ended early). Pre-existing, in the DirectUploader and
  Project notes; both use &quot; now, like the &lt;/&gt; already in there.

Checked by parsing every block in architecture/*.md with mermaid 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:42:07 +09:00
b408e004b3 fix(history): stop reporting the device IP on every change (BEA-43) (#81)
* fix(history): stop reporting the device IP on every change (BEA-43)

The history API embedded the whole DeviceInfo, so every project member
read a teammate's server-observed IP (plus user/last_seen) next to every
change on a page whose job is "who changed this file". Project a
three-field historyDevice instead — id/name/os — mirroring heatByDevice.

The device registry is unchanged: Observe/requestIP and both MetaStore
backends keep recording the IP. This is a response projection, not a
change in what gets collected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: history shows device name and OS, not the IP (BEA-43)

README, SKILL.md and the docs site all promised the History view would
show the connecting IP. It no longer does — the registry still records it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:10:18 +09:00
af9ef4d181 fix(dashboard): make the knowledge treemap honest on a young project (#93)
* fix(dashboard): make the treemap honest on a young project

Every cell was the same green (staleColor spreads 0-300d, so <=3d-old
content lands in one stop), half the cells were anonymous with no hover
fallback, and read counts existed only inside file <title>s. The map said
nothing the file tree didn't.

- group rects get a <title> (folder, reads/30d, file count), so a group
  whose files are all too small to label is still identifiable
- read counts appended to file and group labels; the count is part of the
  string the fit is measured against, so it can never overflow, and when it
  doesn't fit the label degrades to the bare truncated name as before
- a freshness legend under the map: the gradient plus the age span actually
  observed in this scope+lens, and when that span is under a week it says
  the colour channel carries no signal instead of implying one
- group read totals come from a true sum, not the padded layout value

No relative colour scale: normalising to the observed range would paint a
3-day-old file the red that means hot-and-stale everywhere else on the page.
Sizing (reads + 1), scatter, hot path and coverage are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(architecture): heat.ts joins the frontend lib

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:40:36 +09:00
3d0eee0254 fix(history): size an agent run by files touched, not by ops (BEA-39) (#87)
The run card header counted ops, so a path rewritten five times inflated
the one number a reader uses to size a run: 14 ops across 10 paths read
"14 files". It now counts distinct paths and keeps the word "files";
every op is still a row inside the card.

The same count decided whether a run got a card at all, so a run that hit
one path five times drew a card claiming "5 files". Counting by file
demotes it to bare rows, each still showing its session note.

groupRuns and the Run/Item types move to src/lib/runs.ts (pure, no React)
so node's test runner can import them — a .tsx with JSX can't be. The
grouping key, ordering, time span, who and device are unchanged, and
run.idx still addresses the flat feed so diffs and restore shas are
unaffected. The key's NUL separator moves across as an explicit "\0" —
it was a raw NUL byte in the source, which is also why git saw the old
HistoryView.tsx as binary.

Deviation from the reviewed plan: flipping the threshold in place would
have dropped rows. Only a run's first entry was ever pushed to the output
list, so a demoted 5-op run would have rendered one row, not five.
groupRuns now builds runs first and emits items in a second pass over the
feed, which keeps demoted rows at their own newest-first positions.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:32:20 +09:00
01f33c9fe7 fix(dashboard): the hot-path bar paints share reads as share, not as people (BEA-38) (#86)
* fix(dashboard): the hot-path bar paints share reads as share, not as people

The bar computed one fraction (agent/total) and painted the whole remainder
in the human colour, so a file read only through a share link rendered as if
a person had browsed the hub — and the legend named only two readers, while
the file header has been breaking out all three all along.

Each reader now gets its own segment from its own count. hotPathSplit() does
that arithmetic, and it lives with the rest of the heat helpers in the new
lib/heat.ts (pure, no React) so one unit test over one fixture can pin the
invariant the report doubted: the file header and the Dashboard read the same
total from the same helper. useBrowse.ts re-exports them, so no import site
moved.

No server change — /heat already returns share and stays identity-free.

* docs(architecture): lib gains heat.ts, the one read-count arithmetic

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:23:20 +09:00
11dd7ac528 fix(webapp): the viewer credits the account you signed in with, like History does (BEA-37) (#84)
* fix(webapp): viewer credits the signed-in account, like History does

RemoteSource.Files built each FileInfo from op.Author alone and dropped
op.User/op.UserName, so the file viewer header showed a git/OS identity
the user never signed in with while History rows for the same op showed
the account. Carry both fields through FileInfo -> Node -> /render and
render them with the whoChanged() helper History already uses, so there
is one attribution rule in the frontend rather than two.

The guard on the meta line stays on the raw fields: whoChanged() answers
"unknown" rather than "", and plain-folder (DirSource) mode has no
identity at all and must keep printing nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(webapp): plain-folder render carries no empty identity fields

Locks the other half of BEA-37: DirSource has no account behind a file,
and sending empty user fields would make whoChanged() print "unknown"
where plain-folder mode has always printed nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:18:32 +09:00
4dfe6f44f0 feat(history): undo a file an agent run created (BEA-35) (#82)
History could restore an edit or a deletion, but a file a run CREATED was
the one thing it couldn't reverse — the ADDED row said so in copy and
offered no button. The missing capability was a hub-written delete op:
restore.go only ever journaled puts.

POST /api/p/<id>/remove journals exactly one journal.KindDelete op under
the hub's own device identity, behind restore's gates (gateUpload,
PermWrite, cleanUploadPath, quota CheckWrite/RecordUsage) plus a volume-
snapshot existence check so the API 404s on what the tree doesn't show.
Commit's journal-append tail moves into RemoteSource.appendOp, which both
writes now share — one writer per journal, unchanged.

The ADDED-in-a-run row gets an "undo — remove file" control that confirms
first (it reaches every synced device), and the DELETED row it leaves
behind restores the file with its original bytes.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:11:20 +09:00
31472c56e7 fix(hub): one Revoke per share link — the modal hands off, the banner undoes (BEA-32) (#77)
Opening Share on a file that already had a live public link showed the URL
twice, each copy with its own Revoke: the transient dialog on top of the
persistent ShareBanner. Both hit the same link, so nothing was at risk, but a
destructive control shown twice is a control nobody wants to click.

The dialog loses Revoke and keeps its job — confirm what happened, hand over
the URL (Copy link / Open / Done) and set an expiry. The banner keeps Revoke:
it is what BEA-16 added so the undo outlives the dialog, and it is still there
on every later visit to the file.

Deviation from the reviewed plan: it also had this delete the `token`
derivation and the `api` import, which existed only for the Revoke handler.
BEA-29 (#74) landed first and its expiry PATCH now uses both, so they stay —
the plan assumed this PR would go in ahead of it.

The e2e that revoked through the modal now revokes through the banner, and
asserts `.modal .ai-del` has count 0 right after Share — the assertion that
stops the duplication coming back.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:04:30 +09:00
Snow Lee (Sungwon)andGitHub 946a552c64 fix(dashboard): name the fourth quadrant on reads × freshness (BEA-42) (#79) 2026-07-31 06:00:36 +09:00
872ba702cb fix(log): bdrive log reads as a timeline — newest first by edit time (#BEA-40) (#90)
`bdrive log` sorted by the lamport clock, so the wall-clock stamps it prints
came out non-monotonic — two 06:09:24 rows above a 06:10:00 row. And every op
of one scan is stamped with that scan's commit time, so a 22-file agent run
collapsed onto a single stamp. Neither is readable as a timeline, which is the
whole job of the command.

Two display-only changes:

- `journal.Op` gains `Mtime` (`omitzero`, so old journals and old binaries are
  unaffected), populated on put ops from the `os.FileInfo` the scan already
  holds. Deletes and conflict copies keep their commit time.
- `syncer.DisplayTime` / `SortForDisplay` order by the timestamp that is
  actually printed, ties broken by reversed `journal.Less`. `bdrive log` sorts
  and *then* truncates, so `-n 25` is the 25 newest by that stamp.

`journal.Less`, `Sort`, and `Replay` are untouched — replay order is the
convergence contract, so the sort lives in `syncer`, not in `journal`.
`LogEntries` also keeps returning causal order because `bdrive restore` walks
it to find a file's previous version.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:49:23 +09:00
b1c0bba415 feat(auth): the browser sign-in says whose account the terminal gets (#96)
* feat(auth): ask before signing a terminal in as whoever the browser is

`bdrive login` opened /auth/cli and the browser bounced straight back with a
code. Whoever the browser happened to be signed in as is who the terminal
became — silently. That is frequently not the account the user meant: a
personal login left open in the default browser, a teammate's session on a
shared machine. The mistake surfaces much later, as a synced folder full of
commits authored by the wrong person, which is far more work to undo than one
click would have been.

The device flow already got this right in #83 — it names the account, offers
to switch, and says what approving grants. The browser flow said nothing at
all, for the same outcome: a token that acts as you.

So /auth/cli now confirms first. GET renders the page (who you would be
signing in as, a Switch account link that comes back to this same pending
sign-in, what is asking, and where it is waiting); POST is what mints the
code and redirects to the loopback listener. A GET therefore grants nothing,
so a link someone else got you to open can no longer mint a code on your
behalf.

whoBlock loses its pendingGrant parameter and renders only the identity half.
What is asking differs per flow — a device has a name and an OS, a CLI on this
computer has a loopback port — so each page now renders its own rows through a
small helper instead of whoBlock pretending to a shape neither quite fits.

The CLI's own wording follows: "waiting for you to approve the sign-in in your
browser", since being signed in already is no longer the whole story.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* docs: the browser sign-in confirms first, and says whose account it grants

README, the CLI reference, and the self-hosting auth page all described the
old behaviour — sign in and the page bounces a code straight to the terminal.
They also read as though only `--device` had an approval step. Both flows now
confirm; say so, and say why it matters (the browser session is often not the
account the user meant).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* feat(auth): one web step for a first sign-in, not two

The confirmation page fixed the wrong-account problem and created a smaller
one: a user with no browser session now saw two pages on their first
`bdrive init` — sign in, then approve — where the sign-in had already settled
the only question the second page asks.

So authenticating *for* a pending CLI sign-in now counts as approving it. The
login and signup pages carry a line saying a terminal is waiting and that the
account used here is the one it will act as, which is where that consent is
made informed; reaching the callback then needs no second click.

The marker is server-side, bound to the exact pending sign-in, single use, and
two minutes long, so it can only ever skip the page it was granted for and only
once. It cannot be forged: setting it requires authenticating as that account,
and anyone who could do that could click Approve anyway.

An existing session still gets the page — that is the case where the browser
may be signed in as someone the user did not intend, which is the whole reason
it exists. Net effect: exactly one web interaction either way.

The device flow keeps its explicit approval. Its page names a machine that
isn't this one, along with the OS and address it came from — information no
login form can convey, about a grant to somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* fix(auth): keep `bdrive login` on one line in the approval hint

It wrapped mid-phrase into two separate code boxes, which reads as two
commands rather than one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* refactor(auth): one approval page for both sign-in flows

The two flows ask the same question — shall this thing act as you? — and had
two copies of the page asking it, differing in three strings. They had already
started drifting: a wrapping fix went into the CLI copy only, leaving the
device page able to break `bdrive login --device` across two code boxes. A
page whose whole purpose is consistent disclosure is a bad place to keep two
of everything.

So pageAuth owns the shape (session check, redirect to login, whoBlock, rows,
the Approve form, the note) and each flow supplies an authRequest describing
what differs: how the request is identified, what is asking, and what
approving does.

Two asymmetries are now explicit rather than accidental. freshAuthSkips is
true only for the local flow — signing in and approving are the same act when
the terminal is on this machine, and are not when the token goes to another
one. live() reports whether the request still exists, because the device
flow's link expires while the CLI flow carries its whole request in the URL
and has nothing to expire.

detail is a function, not a slice: the device rows come off the pending grant,
which only exists after live() has found it.

No test changed. The pages render byte-identically — same sha256 for all three
CLI screenshots before and after — and the device flow was driven end to end
against a real hub, approving a real `bdrive login --device`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* feat(auth): both sign-in flows always ask you to approve

Consistency between the two flows is worth more than the click it saves.
Letting a sign-in count as its own approval made the local flow one step and
the device flow two, so the same product asked for consent in two different
shapes depending on which machine you were on — and the shape that skipped it
was the one where the page had something to tell you.

So the fresh-auth marker is gone: sign in, then approve, on both flows. That
drops a map, two methods, a descriptor field, and a branch in pageAuth — the
unified handler now has exactly one path through it.

A first `bdrive init` on a fresh machine is two web pages again. That is the
deliberate trade: the approval page is where a user sees which account a
machine is about to act as, and nothing shortcuts it.

The sign-in page keeps the line saying a terminal is waiting. It no longer
carries the consent — the next page does — so it is there to explain why a
password prompt appeared at all.

TestBothFlowsAlwaysAskToApprove replaces the one-step test and runs the same
assertions over both flows as subtests: no session sends you to sign in
carrying the request, signing in returns to the request without granting, the
approval page is there every time, and only the POST grants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:14:13 +09:00
Snow Lee (Sungwon)andGitHub 118abc67e2 feat(cli): "bdrive serve" replaces "bdrive web" (web stays as an alias), and the README leads with the agent install (#94) 2026-07-30 17:46:58 +09:00
Snow Lee (Sungwon)andGitHub 951c6b9de2 feat(viewer): preview text by its bytes, not its extension — and render PDFs (#95)
The viewer decided what to preview from a filename regex, so every
extensionless file an agent writes — Dockerfile, LICENSE, .bdriveignore —
and every unlisted extension (main.tf, schema.graphql) hit a dead "No
preview for this file type." card. That bites hardest in the core use
case: an agent writes an artifact, a teammate opens the hub to read it,
and the hub refuses.

The unmatched path now fetches once and decides on the bytes. The logic
already existed for the history diff — 1 MB cap, Content-Length cheap-out,
8 KB NUL scan, fatal UTF-8 decode — so the pure half moves to
lib/sniff.ts (importable by node --test, no React Query) and both
DiffView and FileView call it. Exactly one sniffer, per the spec.

.pdf gets the browser's own viewer in an iframe, in the wide page column
(768px is unreadable for a PDF page). No sandbox attribute, deliberately:
the PDF viewer is not this page's JS realm, so it cannot reach the hub
API or its cookies, and sandboxing without allow-same-origin breaks
Firefox's pdf.js.

Files that already previewed (md/html/img/.txt) are untouched and issue
no extra fetch.

BEA-44
2026-07-30 16:25:21 +09:00
7b863a4684 test(sandbox): a disposable Linux machine to run a scenario in (#92)
Some things cannot be tested from a Go test on your Mac. A real `claude`
session needs the real permission classifier and a $HOME it may write agent
hooks into. The systemd user unit only exists on Linux. A reboot needs
processes to die while the filesystem survives. Until now those were tested by
hand, against the real ~/.bdrive and ~/.claude — so testing onboarding from
scratch meant polluting the machine you were testing from, and `bdrive init`
registering hooks user-level made that worse.

This is an ENVIRONMENT, not a suite. It provides a hub on file:// storage, a
seeded account, browserless sign-in (bdrive-signin drives both halves of the
device flow), Claude Code, the binary under test, and a $HOME thrown away with
the container. Scenarios still live where they belong: deterministic ones in
internal/webapp/cli_e2e_test.go, the conversational one in the onboarding-e2e
skill. The rule, written into the Dockerfile so it survives me: if it doesn't
need a conversation or an OS, it's a Go test.

The two scripts it ships are the scenarios with nowhere else to go.
onboarding.sh runs a real `claude -p` following the LOCAL
INSTALL_FOR_AGENTS.md and checks the scope hard gate, hooks-via-init, and that
nothing reaches for a plugin or skill. daemon-linux.sh covers the systemd unit
and the daemon.pid/stop race.

Notes for whoever reads this next:

  - The binary is bind-mounted, not built in, so a code change rebuilds the
    binary and not the image. BDRIVE_SRC=<checkout> tests a branch without
    touching your working tree; BDRIVE_BIN=<binary> skips the build.
  - No `# syntax=` directive in the Dockerfile on purpose: it makes every
    build resolve the frontend from the registry, which turns a slow network
    into a build that hangs with no output. That also rules out RUN heredocs,
    hence boot.sh being a file.
  - Claude auth comes from CLAUDE_CODE_OAUTH_TOKEN (`claude setup-token`).
    The Keychain is deliberately not read: the container would refresh that
    token and rotate it out from under your Mac, logging you out there.
  - The hub lives only as long as the container's command, so the project
    link init prints is dead once a scripted run exits. Use the interactive
    shell to browse it.


Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:34:33 +09:00
b927e56fab fix(daemon): let the daemon own its pidfile, so stop can stop it (#91)
* fix(daemon): let the daemon own its pidfile, so stop can stop it

`bdrive stop` could fail with "no such process" and leave sync running.

Liveness became the flock in #88, and Run already announces the child's own
pid only after it holds that lock. But Start still wrote daemon.pid from the
parent, right after fork, with the pid of a child that had not earned
anything yet. Two starts inside that window — `bdrive init` followed by the
login agent's `bdrive resume`, or two resumes close together — race: Running
still reads false, a second child spawns, it loses hold(), and it exits
without ever being the daemon. Its pid is already in the file.

Everything downstream trusts that file. Stop signals the loser and gets
ESRCH, so it reports failure while the winner keeps syncing — the one command
whose job is "stop sending my files" silently does not. status prints the
phantom pid, or "pid 0" when the loser's cleanup removed the file the winner
wrote.

So the parent no longer writes it: the pidfile belongs to whoever holds the
lock. Start now waits for the lock to be taken instead of assuming the spawn
worked, which also means a caller that gets a pid back can trust a daemon
owns it — `bdrive resume` used to print "started (pid N)" for a child that
had already died.

The regression test needs the real binary (Start execs os.Executable), so it
lives with the CLI e2e rather than in internal/daemon, whose tests synthesize
locks. It is deterministic on Linux and roughly one run in five on macOS,
where the window is tighter; `sandbox/run.sh daemon-linux` is the reliable
reproducer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* docs(architecture): the daemon owns its pidfile, Start only waits for the lock

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:31:44 +09:00
fd4f5c7964 feat(onboarding): folder name follows the project name, and project ids are UUIDs (#89)
The paste prompt now carries the project's name so an agent recommends a
folder of that name; with no project at all the recommendation is `shared/`
(and `bdrive init shared` names the new project after the folder), replacing
the old `wiki/` default.

New project ids are UUIDs instead of `p-` + 8 hex chars. The route validator
still accepts the legacy shape — ids are permanent — and the client-side URL
parsers (remote/http.go, bdrive share) now only check the shape of a URL
segment, leaving the hub as the single authority on which ids are valid.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 13:42:07 +09:00
fb6ce347c4 feat(daemon): survive a reboot — login autostart on macOS/Linux/Windows, and a lock instead of a pidfile (#88)
* feat(daemon): bring sync back after a reboot, and stop trusting the pidfile

A reboot killed every daemon and nothing restarted them. Agent hooks still
synced per turn, which is what made it easy to miss: a folder looked fine
while an agent worked in it and went stale the moment one didn't. `bdrive
init` now registers a login item (macOS: a user LaunchAgent) that runs the
new `bdrive resume` — one registration per machine, which starts a daemon for
every enrolled, unpaused mount, so adding a project later needs no
re-registration and `bdrive stop` still means stay stopped. `--no-autostart`
opts out, `bdrive autostart install|uninstall` manages it.

Writing the plist is the whole job: no `launchctl` shell-out. launchd loads
agents at login anyway, the caller has just started the daemon for this
session, and shelling out would let a test or a packaging script register a
real login item as a side effect.

The recovery path was also broken, which is why this is one change. Liveness
came from `kill(pid, 0)` on daemon.pid — but that file lives in
$BDRIVE_HOME and survives the reboot that killed its process, so any
same-user process recycling the pid read as a live daemon. `bdrive status`
said "running", and worse `daemon.Start` returned early, so the one
documented recovery (`bdrive init`) reported success and started nothing.
Liveness is now an flock held for the daemon's lifetime: the kernel drops it
at death or reboot, and it makes two daemons on one mount impossible. The pid
stays for display and for signalling.

internal/autostart is darwin-only today; autostart_other.go returns
ErrUnsupported and every caller already treats that as "nothing to do", so
Linux (systemd user unit) and Windows are one file each.

Tests: internal/daemon gets its first ones — a recycled pid must not read as
running (the exact regression), the lock decides liveness, a second holder is
refused. internal/autostart covers write/idempotency/stale-path-rewrite/
uninstall with HOME redirected, and lints the plist with plutil so launchd
can actually parse it. The CLI e2e asserts init registers the agent, that it
runs `resume`, that resume finds the live daemon instead of starting a
second, and that --no-autostart is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Linux support — a systemd user unit alongside the launchd agent

Same three functions, same discipline. Linux writes
$XDG_CONFIG_HOME/systemd/user/beardrive.service (Type=oneshot, no Restart= —
`bdrive resume` exits by design) plus the default.target.wants symlink that
`systemctl --user enable` would create, because systemd ignores a unit
nothing wants. No `systemctl` shell-out, for the same reasons as launchctl:
the file is the registration, it only matters at the next login, and a
container or ssh session has no session bus to talk to.

Install declines with ErrUnsupported unless systemd is actually the init
system (/run/systemd/system, i.e. sd_booted) — on Alpine, WSL1 or a slim
container a unit file is inert decoration, and reporting "registered" would
be a lie. Installed() likewise requires the enable symlink, not just the
unit: a unit nothing wants never starts.

os.UserConfigDir honors XDG_CONFIG_HOME, so relocated config dirs work.
Windows is now the only gap; autostart_other.go is !darwin && !linux, and the
shared writeIfDifferent/selfPath moved into the tag-free autostart.go (darwin
now uses them too).

Tests run on Linux, not just compiled for it: cross-compiled test binaries
executed in a container, both with /run/systemd/system present (unit written,
enabled, idempotent, stale ExecStart rewritten, broken symlink repaired,
XDG honored, uninstall removes both) and without it (Install declines and
writes nothing). The daemon flock tests were run there too, since flock
semantics are per-OS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Windows support — a per-user Run entry

Third platform, same three functions. Windows has no user service manager in
the launchd/systemd sense, so the registration is a HKCU\...\Run value via
golang.org/x/sys/windows/registry (already in the module graph; go mod tidy
just promotes it to direct).

Chosen over the alternatives for the same reason the other two write files:
no admin rights, no COM (a Startup-folder .lnk needs it), no schtasks
shell-out. It is also honestly discoverable — the entry appears in Task
Manager's Startup tab, where someone can disable it without knowing bdrive
exists. The executable is quoted because Explorer parses the value as a
command line and Program Files has a space in it.

Two things a reader should not have to discover for themselves:

- The tests here have NEVER RUN. They are written and compile-checked
  (GOOS=windows go test -c) from macOS; there is no Windows host or usable
  container on an arm64 mac. They execute the first time the suite runs on
  Windows. They also cannot use a temp HOME the way the macOS and Linux tests
  do — HKCU is real — so each one snapshots and restores the previous value.
- `GOOS=windows go build ./...` still does not pass, and this package is not
  why: internal/store's Lock uses syscall.Flock and internal/daemon uses
  syscall.Kill and Setsid, all unix-only (true before this branch too). A
  Windows port means LockFileEx plus a stop story for a platform with no
  SIGTERM — a separate change, against the sync invariants, and untestable
  from here. So this code is correct and currently unreachable.

autostart_other.go is now !darwin && !linux && !windows (the BSDs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:32:13 +09:00
31f705e287 chore: drop the Claude plugin and the bundled skill — hooks are the integration (#85)
Two front doors for the same setup, and one of them was a second copy of
everything. The plugin shipped the skill the CLI already installs (to four
platforms, not one), hooks that ran the identical commands `bdrive hooks
install` writes machine-level, and an install flow duplicating
INSTALL_FOR_AGENTS.md. Nothing deduped, so a machine with both pulled twice
per turn — two blocking syncs — and showed two identical `beardrive` skills
in the picker.

What remains is `internal/agenthooks` plus the runbook: init registers a
blocking pull (which also injects the gated-link convention as
additionalContext), an async push on Write/Edit, and read-log for the
heatmap, in each platform's user config, once per machine. That is the whole
integration, and it is the part that was never optional.

Removed: plugin/, .claude-plugin/marketplace.json, internal/agentskills,
`bdrive skill`, `bdrive hook-approve` (its PreToolUse auto-approve only ever
helped when a plugin pre-installed it; the substitute is a `Bash(bdrive:*)`
permission entry, which is user-owned config and needs no code).

The e2e now asserts the absence: no SKILL.md in any platform's skills dir
after init, and no `skill` subcommand. login_test keeps the "no revoke
surface" wording check on logoutNote alone.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:40:47 +09:00
dfb9da3260 feat(auth): one approval link for device sign-in, and a page that says what it grants (#83)
The headless flow printed a short code to retype into a bare "Approve"
form. Now `bdrive login --device` prints a single link — the token lives in
the path (/auth/device/<token>), so there is nothing to read off one screen
and type into another.

The page it opens is a consent page rather than a text field: it names the
account the device would act as, offers Switch account (logout now honors
?next, so you land back here), and shows the device name, OS, and the
address the server observed. That matters because this flow's weakness is a
stranger sending you their pending link; an anonymous "Approve" gives you
nothing to notice with. Approval is still a POST from the page, so a link
alone cannot grant, and SameSite=Lax keeps a cross-site form out.

Also aligns the /auth/* pages with the app's tokens, which had drifted:
card #0c0e10 vs --color-card #15171b, 8px controls vs --radius-ctl 7px,
hand-picked #ff9b91/#6fd699 vs --color-del/--color-add. The style block now
declares the tw.css tokens by name and every rule uses them.

Older CLIs still print /auth/device?code=…, so that shape 303s to the path
form; a pre-0.13 hub returning no verify_url still gets the old
type-the-code instruction from the CLI.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:41:27 +09:00
2f68bbe92e feat(hub): optional analytics seam so a managed deployment can measure the app (#80)
Adds Server.Analytics (webapp.AnalyticsConfig) and emits it as /api/config
`analytics` when a key is set. The frontend loads posthog-js from the CDN at
runtime rather than as a dependency, so an unconfigured hub ships no tracker
and makes no third-party request — the OSS bundle grows 1.1KB (the loader),
not 230KB.

Product events come from one table in api/http.ts keyed on method+path.
Every mutating call in the app already goes through api()/postJSON(), so a
new write is measured or it isn't, instead of depending on someone
remembering a capture() call. Share creation is the one raw fetch and fires
its own.

Session replay masks every text node: in this product nearly all of it is
customer file names and document bodies. Replays are layout, clicks and
navigation only.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:30:02 +09:00
Snow LeeandClaude Opus 5 511b838da0 chore: gitignore .orca/
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:20:43 -07:00
4ff92c56ab feat(history): group agent runs and restore any version (BEA-6) (#69)
* feat(history): group agent runs and restore any version (BEA-6)

BearDrive recorded everything and could restore nothing. Now every version
of a file has a Restore button — in the hub's History view and as
`bdrive restore` — and the changes one agent run made read as one card
instead of N loose rows.

Restore is a NEW put op pointing at the old blob: journals are never
rewritten, so one-writer-per-journal holds and peers converge on the
restore like any other edit. The hub reuses RemoteSource.Commit (the
upload commit minus the upload); the CLI writes the bytes into the working
folder and lets the ordinary cycle journal them, so the sync engine gains
no new write path.

Grouping is a pure frontend group-by on (note, device) over the existing
/history response — no journal or API change.

Known gap, stated in the UI and the docs: nothing in the hub writes a
delete op yet, so a file a run *created* cannot be un-created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(history): don't repeat a run's note on every row in its card

UI pass on the real hub: inside a run card the note is the card's header, so
printing it again on each row said the same thing N times. The header now
carries the note (linkified, so an agent's session link still opens) and the
collapse control is its own button rather than the whole header — the link
could not live inside a button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:15:46 +09:00
dcd0517e92 feat(cli): bdrive scope --explain — prove what leaves this machine (BEA-24) (#70)
The local-first claim was asserted, never demonstrated: nothing anywhere
told you what your laptop chose *not* to send. `bdrive scope --explain`
walks the folder and prints two sorted lists — synced and not synced —
with counts and a pointer at what it does not answer.

The decisions come from the same walk the sync cycle uses. scan()'s
WalkDir decision tree moves into walkFolder (internal/syncer/walk.go),
the only copy of the rules; scan and Explain both go through it, so the
output provably cannot drift from real sync behavior.

Pure read: its own Filter, no Session, no volume flock, no network.
Fully-excluded directories collapse to one counted line; nested mounts
are annotated as syncing through their own project rather than called
"not synced", which would be a lie in a trust surface.

Known gap, deliberate: this answers "what leaves from now on", not
"what is already on the hub" — the footer points at `bdrive forget`.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:10:51 +09:00
ecc8328512 fix(shares): say when "the latest version" was (BEA-31) (#76)
A /s/<token> page promises the reader it always shows the latest
version, then never says when latest was — so a stranger can't tell
whether a living wiki page is from today or last March.

Print FileInfo.Time, already in hand at the point handleShared renders,
as a muted line above the content: human date, precise RFC3339 in the
title attribute. Zero time prints nothing rather than a 1970 date.

Markdown shells only. The .html/.htm branch stays a raw io.Copy and
binaries stay untouched — the new test asserts byte equality for both.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:09:20 +09:00
6da3c7957e fix(hub): deterministic order for public share links (BEA-30) (#75)
ShareDB.List ranged over a map, so the project Settings → Public links
table and the org-wide share audit came back in a different order on
every load — with a Revoke button on each row. Sort in List (Created
desc, then Path, then Token) so both surfaces inherit one total order.

Created.Equal rather than !=: a time.Time carries a monotonic reading
and a location, so two logically-equal instants can compare unequal,
which would make the comparator non-transitive.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:07:23 +09:00
acb99e85e3 feat(hub): set an expiry on a share link from the UI (BEA-29) (#74)
The CLI has had --expires all along and the Public links table has always
said "no expiry" — naming an alternative the hub UI never offered. Every
link minted from the browser was permanent.

Expiry is offered AFTER minting, not before: the one-click share stays one
click, and PATCHing the token we just handed out keeps the URL already on
the clipboard valid. Minting with a TTL would instead create a second link,
since ShareDB.Create only reuses permanent ones.

- ShareDB.SetExpiry re-dates a live share in place; repo-write failure
  restores the previous row rather than deleting it (unlike Create's
  rollback, this row already existed — dropping it would revoke a live
  link over a disk hiccup).
- PATCH /api/shares/{token} mirrors handleShareRevoke: resolve the token,
  requirePerm(PermWrite), act. Duration parsing is copied from
  handleShareCreate so the two routes can't drift.
- Share dialog gets an Expires select (Never / 24h / 7d / 30d). A failed
  PATCH toasts and reverts the control. Copy link keeps the dialog's
  initial focus — the new control would otherwise have taken it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:05:06 +09:00
6ae941683e fix(hub): a folder URL with a trailing slash is the same page as without (BEA-28) (#73)
* fix(hub): a folder URL with a trailing slash is the same page as without (BEA-28)

* docs(architecture): Route.trailingSlash in the frontend diagram (BEA-28)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:03:21 +09:00
feacbe3b3a fix(hub): put visible Open/Download controls on every history version (BEA-26) (#72)
A history row was already an address for the version it describes — BEA-7
made clicking one open that version, and the pinned file page has had
"Download this version" since. But the row is a bare `div role="button"`
with no visible affordance, so a reviewer who dumps every <button> and <a>
on the page finds neither, and concludes recovery from a bad edit is
impossible. Undiscoverable is close enough to absent.

Every content-bearing row now carries "Open this version" and "Download",
sitting on the same line as "show changes". No API work — /blob?sha= has
always served the bytes.

`apiBase` becomes HistoryRow's own prop rather than riding inside the
optional `diff`, which is per-file-only: without that the two feeds that
pass no diff (project/subtree, and the folder's Recent changes) could not
build a download URL at all.

Neither control claims aria-expanded — only the note and the diff really
expand anything — and both stop click and keydown from reaching the row so
acting on a version never doubles as navigating it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:01:16 +09:00
4e34d03e14 feat(hooks): user-scope agent sync hooks, one-command setup, --only scoping (#71)
* feat(hooks): register agent sync hooks per machine, not per project

Agent platforms read hook config only from the directory a session starts
in — never a parent, never a subfolder. Project-level hooks therefore fired
only for sessions that happened to start at the mount, and, living inside a
synced folder, they replicated one machine's agent config to the whole team
(a second writer of a file bdrive already owns). Claude Code additionally
ignores project hooks until the folder is trusted, so in practice they were
often inert without any visible sign.

Hooks now go to each platform's user config, once per machine, covering
every session in every folder; the existing shell guard keeps them a no-op
outside BearDrive projects. Install migrates away blocks older versions
wrote into projects, and `bdrive hooks uninstall` removes ours while leaving
foreign hooks untouched.

Setup is also one command now. init absorbs the skill install, prints the
hub link, and takes --server, so connecting to a named hub no longer needs a
separate login; the runbook forbids preflight and command chaining, since
each distinct command costs the user a permission prompt. For plugin users a
PreToolUse hook auto-approves bdrive's own setup subcommands — narrowly: any
shell operator in the command disqualifies it.

Also drops --shared in favor of `init . --only wiki,docs`, which writes a
managed block of .bdriveignore rules instead of a second scope mechanism.
Because those rules sync, `sync --prune` now refuses on a scoped project
rather than stripping everything outside the scope from the hub for everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* docs: fix stale claims an audit found against the new CLI

An audit of every doc surface against the code turned up claims that the
user-scope hook move and the one-command init made false: project-level
hooks "riding the repo", the Claude trust prompt, Codex's //hooks project
layer, `--no-hooks` skipping the skill (it does not), prune reconciling
against a per-device scope (it now refuses on a scoped project), and
`--scan-interval`/`--remote-interval` documented as init flags when they
only exist on `bdrive daemon run`.

Also documents the surface added today — `--server`, `bdrive hooks
uninstall`, and the plugin's PreToolUse auto-approval — refreshes the two
sample `init` transcripts to the real output, and corrects hook matchers
that had drifted from agenthooks.go.

`bdrive scope` told users to narrow an existing mount with `bdrive init .
--only <dirs>`, which resume then ignored — a dead end. Init now applies
--only on resume, writing the scope block, so the advice works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:08:45 +09:00
Snow LeeandClaude Fable 5 22461a3b4f chore(release): v0.11.0 — changelog, plugin 0.4.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ
2026-07-27 16:09:16 -07:00
9088127176 feat(sync): bdrive forget + sync --prune to take ignored paths off the hub (BEA-20) (#68)
Adding a path to .bdriveignore only stopped future uploads: anything that
synced before the rule existed stayed on the hub forever, with no command
that removed it without deleting it from local disk on every device.

Two engine changes make an explicit removal safe:

- materialize's delete loop now consults the filter. A cached path absent
  from the replayed target that the rules exclude is dropped from tracking
  instead of unlinked — without this, any delete op for a now-filtered path
  wipes every peer's local copy, which is the data loss this issue is about.
- the filter is reloaded mid-cycle from the pulled .bdriveignore, before
  materialize. A peer receiving the new rules and the deletes they justify
  in one batch would otherwise materialize with stale rules and the guard
  would never fire. materialize's write side is split into materializeFile
  so the ignore file can land on its own.

On top of that, Session.Prune journals a delete for every path the replayed
state still holds that the SHARED rules exclude — reconciling against the
replay, not the local cache, because a path filtered out in an earlier cycle
was dropped from the cache back then and is invisible locally today. The
rules are deliberately ignore-only: the include scope lives in each device's
own .bdrive/config.json and does not sync, so pruning against it would let a
narrow-scope device delete a whole-folder teammate's files.

Plain `bdrive sync` and the daemon are unchanged — pruning is never a side
effect of editing .bdriveignore.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:22:11 +09:00
411809a785 fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17) (#67)
* fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17)

The +/x in a history row is the kind glyph (added/edited/deleted), but
sitting leftmost inside a role="button" row it read as a tree disclosure
control — clicked, it navigated away instead of expanding. Merge the glyph
and its word into one text badge and vacate the toggle slot: the kind is
now text (no icon shape, no colour-only meaning), and the row's only real
expander stays the note, which keeps its own control and aria-expanded
and now turns its chevron when open.

* fix(hub): fit DELETED in the kind badge and align the row's meta under the path

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:21:38 +09:00
f35d889cfb fix(hub): manage a file's public links from the file, not the org panel (BEA-16) (#66)
Minting a public link was one click on the file page; revoking it meant
knowing to go avatar menu → Organization → scroll to "PUBLIC SHARE LINKS".
The action was instant and local, the undo remote and unhinted.

GET /api/p/{project}/shares already existed at PermRead with no frontend
consumer, so this is UI-only:

- A "Publicly shared" banner on the file page whenever the open file has
  live links: the count, each URL, and copy / open / revoke — the same
  words the Share dialog uses. Revoking updates it in place.
- A "Public links" card in project Settings listing that project's live
  links (path, who, when, expiry) with Revoke.
- One SharesTable behind both, plus the org-wide audit, which stays as the
  cross-project view and now links each row back to its file.

The banner shows for anyone with read (a member should know the folder
they rely on is exposed); Revoke only where the Share button already is.

The spec's double-mint bug does not exist: ShareDB.Create already reuses a
live share for (project, path) when neither side has a TTL, and the web UI
never sends expires_in. The real defect was the dialog claiming "Public
link created" on a second click — it now just says "Public link".

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:11:32 +09:00
aeee881fa6 fix(hub): say what a read count is made of, and pin the debounce (BEA-15) (#65)
The reported symptom was a read counter inflating while you watched: reopen
one file a few times as one account and it climbs. It does not — the ledger
debounce is correct, and a handler-level repro proves it. What moves is the
displayed *total*, which sums three kinds of reader (human, agent, share)
with independent debounces: your own revisits fold into one visit, but the
syncing agent's reads and share-link hits keep landing in the same number
with nothing to say so.

So the count was right and its framing was wrong. heatText now breaks the
total out by reader kind whenever more than people are reading — the seeded
"14 reads (9 agent)" becomes "15 reads (6 human, 9 agent)" — at the single
chokepoint every heat surface already routes through (file meta line, folder
row meta, folder subtitle, heat-dot tooltip).

No ledger change: the investigation found nothing to fix there. The spec's
prime suspect (a sandboxed-iframe fetch losing the session and recording as
"anonymous") cannot fire in hub mode — authGate 401s an unauthenticated
/api/ request before recordRead runs, and every authenticated identity
carries an account email. TestReadCountsOnePageOpenOnce pins the invariant
so this stays legible: one signed-in person, one file, one 10-minute window
is exactly one read, whichever of /render, /file, /download served it; a
second account adds exactly one; the sandboxed iframe's cookie-less fetch
adds none; history spelunking adds none; and no actor identity reaches the
/heat response. TestReadLedgerDebounce now also covers the other half — once
the window passes, the same actor counts again.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:08:21 +09:00
f773b0c6e7 fix(hub): keep folder-row metadata on phones and name the heat dot (BEA-14) (#64)
Below 430px `.dl-meta` was `display:none`, so a folder listing on a phone
showed a filename and an unexplained coloured dot — no read count, no size,
no date. The comment justifying it assumed the dot carried the signal, but
the dot's meaning lived entirely in a `title=` attribute: never shown on
touch, never read by a screen reader on any viewport.

- `.dl-row` wraps and `.dl-meta` takes a full-width second line at ≤430px,
  indented 27px to align under the filename. The name still wins line one
  and is never truncated; the full string fits at 360px, so no shortened
  variant is needed.
- The heat dot gets `role="img"` + `aria-label` on every viewport, which
  also fixes desktop screen-reader users.
- Playwright assertion in layout.spec.ts at 360/390/430: meta matches the
  desktop string, filename untruncated, rows ≥44px, no horizontal scroll,
  every dot has an accessible name.

Desktop (≥431px) is unchanged — measured identical row heights and meta
positions before and after.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:07:03 +09:00
eba6fe1a64 fix(cli): stop pointing users at a device list that doesn't exist (BEA-13) (#63)
`bdrive logout` and the beardrive skill both told users the device token
"stays valid until it expires — revoke it from the hub's device list".
All three parts are false: there is no device list page or route, device
tokens carry no expiry field, and logout makes no server call at all — it
only rewrites the local settings file.

Both strings now say what is true. The CLI note moves to a package-level
`logoutNote` const so `login_test.go` can assert it, plus SKILL.md's logout
row, mentions neither a device list nor expiry.

Correcting the strings only; the real device list + revoke route touches
the `AuthProvider` seam and is filed separately.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:05:19 +09:00
589be3a6de fix(hub): open the project Dashboard to every member, rename /insights → /dashboard (BEA-12) (#62)
* fix(hub): open the project Dashboard to every member, rename /insights → /dashboard (BEA-12)

"Dashboard" was the first sidebar item a new member clicked and it always
refused: it landed on /<project>/insights showing "Insights is for hub
admins and org owners." The gate was client-side only — GET /heat is
gated on project membership and returns counts without actor identities,
so every member's browser could already fetch every number the page draws.

Drops the canInsights gate (nav item, dedicated route, project-home
embed, ⋯ menu entry) and renames the view route insights → dashboard so
the nav label, the URL and the page title finally agree. The shipped
/insights URL still resolves and normalizes to /dashboard (LEGACY_VIEWS
in router.ts) so bookmarks don't 404 and only one URL stays live.

No server change: /heat gating and response shape are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(architecture): router VIEW_ROUTES now names dashboard, with LEGACY_VIEWS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:05:00 +09:00
224b3d6f53 feat(hub): show what changed between file versions (BEA-10) (#61)
Per-file history rows carried identity and a byte size and nothing else —
to answer "what did the agent change?" you had to download two blobs and
diff them by hand. Every non-first version now expands to a line diff
against the previous version of that path, with a +N −M count.

No new endpoint: /blob?sha= already serves both sides and the history
response already names both shas. No new dependency: the LCS is ~40 lines
in src/lib/diff.ts, unit-tested on node's built-in runner (npm test) —
node ≥ 23 strips the types, so the frontend gains no dev dependency.

Blobs are fetched only on expand and cached by sha with an infinite
staleTime (content-addressed, so staleness never applies). Binary is
decided on the bytes, never the extension; either side over 1 MB gets the
too-large fallback, checked against Content-Length before the body is read.
Diffs are per-file only — the subtree feed mixes paths.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:02:46 +09:00
63612743df fix(webapp): a history row opens the version it describes (BEA-7) (#58)
* fix(webapp): a history row opens the version it describes (BEA-7)

Clicking a row in any history feed called onOpen(e.path) and dropped the
row's blob, so every row opened the CURRENT file — a 7/25 "added" row
rendered content written on 7/26 with nothing on screen saying so. The
backend already served the exact bytes (/blob?sha=); only the UI could
not reach them.

A version is now an address: /<project-id>/<path>?v=<sha>. Routes carry
it (useLocationPath had to snapshot search too, or the URL would change
and nothing would re-render), the file view fetches the pinned blob, and
a banner names the version's time and author, says it is not the current
file, and offers View current + Download this version. /render gains an
optional ?sha= so historical markdown renders as markdown instead of raw
source; history views are still never counted as reads. Delete rows have
no content, so they stay unclickable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(webapp): keep an old version from borrowing the current file's framing

An unknown ?v= sat on a blank pane through react-query's retry before
saying anything, and the topbar still showed the path's read counts next
to content the banner had just called historical. A pinned version now
fails fast and drops the heat line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 06:59:41 +09:00
Snow W. Lee (Sungwon)andGitHub 849794a8e5 feat(hub): agent-first onboarding on the no-projects empty state (#60) 2026-07-27 21:11:25 +09:00
Snow W. Lee (Sungwon)andGitHub e629ae4ab0 fix(hub): sort project history by time, not Lamport clock (BEA-9) (#59) 2026-07-27 20:49:22 +09:00
Snow W. Lee (Sungwon)andGitHub 65d4df5a84 One paste-able URL onboards any agent: INSTALL_FOR_AGENTS.md (#57) 2026-07-27 20:23:42 +09:00
Snow Lee 7768f1cf51 Add .env to .gitignore 2026-07-27 02:48:22 -07:00
056c883204 fix(sync): anchor --shared include entries to the mount root (BEA-5) (#56)
`bdrive init --shared wiki` wrote `include: ["wiki/"]`, which compile()
treats as an unanchored gitignore pattern — so any nested directory named
`wiki` synced too. Shared-subfolder mode is what people use to keep private
material out of a project, and it was silently widening the scope: 15 files
under .agents/, .claude/ and .gemini/ leaked into a real project from
.../detector/shared/ dirs.

cleanShared now emits "/wiki/", which fixes both callers (init --shared and
bdrive scope add). config.LoadProject anchors legacy single-segment entries
on read, so the existing mounts are fixed without a re-init — and that also
keeps `bdrive scope rm wiki` working against pre-fix configs, with a
belt-and-braces unanchored candidate key in scopeRemove for any config that
bypasses LoadProject.

Not touched: compile() itself, and no delete op for the already-leaked
remote files (BEA-20 — a delete would unlink teammates' local copies).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:44:05 +09:00
46db6507c4 feat(hub): Billing in the account menu + in-app /billing view (managed hubs) (#55)
* feat(hub): Billing entry with current plan in the account menu (managed hubs)

webapp.Server gains an optional Billing hook — the display mirror of the
Quota seam: managed deployments return (plan, url) per signed-in user and
/api/config exposes it as the 'billing' block; OSS hubs leave it nil and
nothing changes. The frontend renders a Billing item with a plan chip under
the Organization section of the account menu when the block is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* architecture: Server gains the Billing display seam

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(hub): Billing is an in-app view at /billing (managed hubs)

The account-menu Billing entry now routes to a real SPA view instead of a
standalone server page: /billing is a top-level route like /orgs, rendered
in the app shell from BillingView, which fetches config.billing.url with
Accept: application/json (plan, usage, seats, plan cards, checkout/portal
form URLs). OSS hubs without a billing block get an honest 'no billing on
this hub' page at that path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:55:42 +09:00
2c85747464 feat(sync): .bdriveignore always syncs, regardless of scope or rules (#54)
The ignore file is exempt from filtering in Filter.Skip: it syncs even on
--shared mounts (where it sat outside the include list and was local-only)
and even when one of its own patterns matches it. One guard covers both
scan and materialize since they share the filter. Docs updated on all
three surfaces (SKILL.md, install.md, web/docs).


Claude-Session: https://claude.ai/code/session_01G3AdFdps7seYbW6FhyK58t

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:31:53 +09:00
0236e1b272 feat(cli): multi-folder --shared at init + bdrive scope to edit the sync scope (#53)
* feat(cli): init --shared accepts multiple subfolders (repeatable or comma-separated)

--shared is now a slice flag: `--shared wiki --shared docs` or
`--shared wiki,docs` sync several subfolders into one project
(include list ["wiki/", "docs/"]). The interactive scope prompt takes a
space- or comma-separated list. Entries resolving to ".", "", or ".."
error out — silently dropping them would widen scope to the whole folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs(plugin): skills/commands propose multiple --shared folders at init

The init/install flows now scan for all knowledge folder candidates and
offer them as one --shared list (one project, one permission set), noting
that folders needing different access belong in separate projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* feat(cli): bdrive scope — add/remove shared subfolders without editing JSON

`bdrive scope` shows the include list; `scope add`/`scope rm` edit it from
the mount root. The daemon re-reads config each tick, so changes apply in
seconds. rm deletes nothing (newly filtered paths drop from the cache with
no delete op); removing the last entry is refused since an empty include
list means whole-folder sync. add onto a whole-folder project is refused
for the same narrowing hazard. Skill/README/docs updated; scope added to
the cli-sync diagram's command list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs+cli: scoping guide covers multi-folder --shared and bdrive scope; init hints on ignored --shared at resume

The scoping guide (the dedicated page for this feature) now shows
--shared wiki,docs and a "Change the scope later" section for bdrive
scope; setup-by-hand and project-files point at it. init resume with an
explicit --shared now says the flag is ignored instead of staying silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:49:58 +09:00
69e7231a70 feat(hub): per-project permissions — none/read/write/admin, invite-only projects, honest degraded sync (#46)
Access was binary and org-wide: any org member got full read+write on every
project. Now each project carries four ordered levels, resolved by one
resolver and enforced at one choke point.

- `projectPerm` (perms.go) replaces `projectAllowed`; `proj(level, h)` in
  server.go gates every per-project route by the level it declares at
  registration, so no handler grows its own check.
- `Project` gains Creator/Default/Perms. `Default == ""` means write, so an
  upgraded hub behaves identically until someone edits permissions.
- Creator becomes the first project admin; org owners are implicitly admin
  everywhere in their org and a grant naming one is refused, not ignored; a
  project always keeps at least one explicit admin.
- Default `none` makes a project invite-only. A `none` member is treated
  exactly like a non-member, including on create-or-join by name.
- Rename/delete move from org-owner-only to project `admin`.
- Both metadata backends persist it: the file store rides along, the SQL
  store gains `project_perms` plus an idempotent ALTER for the two new
  columns (migrate() had only ever created tables).

Client side, a refusal stops looking like an outage: `remote.ErrForbidden`
plus `Result.ReadOnly` (push refused → pull-only) and `Result.NoAccess`
(pull refused → paused, working folder untouched). Neither sets Offline,
neither loses a local op, and re-granting self-heals on the next cycle.
`bdrive status`/`sync` and the daemon (once, on transition) say which.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:41:57 +09:00
1463fd8979 fix(hub): show "BearDrive" in the sidebar logo, never the storage name (#52)
The hub's brand fell back to config.volume — the bucket/dir basename —
so a hub on s3://beardrive/... rendered a lowercase "beardrive" logo and
tab title. Drop the fallback at the source (/api/config reports only what
a Brander provider returns) and let each app default on its own: the hub
to the literal "BearDrive", volume mode to the folder name (unchanged).

The e2e harness now seeds Volume: "beardrive" so hub.spec.ts's existing
#vault-name assertion actually catches this.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:18:06 +09:00
Snow W. Lee (Sungwon)andGitHub 65491f9576 feat(hub): rename, describe and pick an icon for a project, from its own Settings page (#51)
Project gains two optional fields — Description (<=280 chars) and Icon (a
lucide icon name) — and PATCH /api/projects/{id} becomes a real partial
update: every field is a *string, so only the keys present in the body
change, and {"description":""} clears where an omitted key leaves alone.
Validation returns 400 for an empty or >120-char name, a sibling-name
collision, a >280-char description, and an icon failing ^[a-z0-9-]{1,32}$.
The permission gate is deliberately untouched.

Storage: the file backend marshals Project whole, so it rides along; the SQL
backend needs the two columns added to an already-created table, which
CREATE TABLE IF NOT EXISTS can't do — hence addColumns(), an idempotent
ALTER helper (same shape BEA-2 introduces for creator/default_level, so the
two merge into one map).

Frontend: Settings is now shadcn sectioned cards (General / About / Danger
zone — adds card, separator, textarea to components/ui), with an RHF+zod
form that PATCHes only its dirty keys and refreshes the hub queries, so the
nav mark and dashboard header update without a reload. Icons come from a
curated ~30-icon lucide shortlist (named imports, so Vite still tree-shakes
the rest); an unknown or empty name renders the folder placeholder. The
glyph shows in the project mark on the switcher trigger and every menu row,
and beside the name on the dashboard header with the description under it.
The org admin panel loses its per-project Rename button, which collapses its
two project lists into one read-only list for everybody.

One fix found while driving the real UI: Tailwind preflight is off in this
app, so copied shadcn form controls rendered monospace/black and cards drew
a near-white hairline. Both are now supplied by slot in style.css.
2026-07-27 09:56:59 +09:00
93476700dc docs: PH gallery screenshots — history, insights heatmap, hub UI (#50)
Three 1440×900 screenshots of the seeded e2e hub for the Product Hunt
gallery (BEA-10): per-file history with agent attribution, Knowledge
Insights heatmap + reads×staleness quadrant, and the main hub file view.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:56:10 +09:00
8e8b3d7aa8 docs: quickstart→sync demo GIF + MP4 (agent-first, per content-calendar rev 3) (#49)
Recorded for the Wed 2026-07-29 beta (BEA-33): /beardrive:install → agent
writes plan.md → teammate's agent reads it fresh → share link. Outputs
replay the real fresh-machine run verified against v0.10.0; vhs tape +
storyboard committed for reproducible re-renders.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:21:51 +09:00
9bac355e5d chore: changelog for v0.10.0 (folds in un-changelogged v0.9.0) (#48)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:13:24 +09:00
11d667a447 fix(cli): report the real version for go-install builds (#47)
go install github.com/runbear-io/beardrive/cmd/bdrive@latest skips the
release ldflags, so every module-built binary claimed to be 0.1.0-dev —
useless in beta bug reports. Fall back to the module version Go stamps
into the binary (debug.ReadBuildInfo) when ldflags didn't set one.

Found during the BEA-33 fresh-machine quickstart check.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:11:58 +09:00
Snow W. Lee (Sungwon)andGitHub 379b19a9ec feat(web): delete a project from its Settings page, behind type-the-name (#45) 2026-07-26 15:57:32 +09:00
6628718f1d fix(cli): agent hooks never sync folders this device didn't opt into (#44)
* fix(cli): agent hooks never sync folders this device didn't opt into

The turn hooks decided "this folder is managed" from the mere presence of
.bdrive/config.json — a file designed to travel with the folder. Two holes:

- A config.json arriving via git clone / copied dir made one hook firing
  silently mint a device identity, register the mount, create a volume
  store, journal the whole folder, and inject the hub-link formula — on a
  device that never ran init or login.
- `bdrive stop` only killed the daemon: the next agent turn's
  `bdrive sync --hook` resumed a full sync cycle and kept injecting links,
  and `stop --forget` was undone within one turn by registry self-heal.

Fix: one gate (`syncBlocked`) in the paths all hooks route through —
sync/sync --hook/read-log now require the mount to already be enrolled in
this device's mounts.json (read without ResolveMount's enrolling
self-heal) and not paused. Hook mode exits silently; plain `bdrive sync`
errors with a `bdrive init` pointer. New per-device paused marker in the
volume dir: set by `bdrive stop`, cleared by `bdrive init` (startSync).
Only init enrolls or resumes; folder moves still self-heal since
enrollment is keyed by mount id, not path.

Docs updated (README, SKILL.md, docs cli reference, CHANGELOG). Tests:
hook/read-log no-op + no-enrollment on unenrolled and paused mounts,
plain-sync refusals, stop→pause→forget regression, paused marker contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: architecture-diagram PRs must show before/after excerpts of changed classes

The "Architecture changes" PR section now names exactly what changed and
shows Before and After mermaid excerpts scoped to the affected classes and
their immediate relationships — never the full diagram (Before = merge
base). Convention updated in CLAUDE.md, architecture/README.md, and the
pre-PR hook's reminder text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add cli-sync architecture diagram; widen diagram convention to the CLI

architecture/cli-sync.md draws the CLI and sync engine (cmd/bdrive +
internal/{syncer,store,journal,config,daemon,agenthooks}): the Session
cycle over Store/journal/remote, and the command layer with the new
syncBlocked opt-in gate, paused marker, and enrollment ownership. The
pre-PR hook and CLAUDE.md now watch these packages too, so CLI-side
structural changes trigger the before/after-excerpt convention the same
way server changes do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: full-coverage architecture diagrams — overview, frontend, agentskills

Every application package is now drawn somewhere: overview.md (system
diagram — package map, device↔hub↔storage flow, agent surfaces, and the
private cloud/ repo as an external seam consumer), webapp-frontend.md (the
hub SPA's modules: App/HubApp/VolumeApp/Browser, the in-repo nav/router,
api layer, hooks, components), and agentskills added to cli-sync.md. The
pre-PR hook now watches all of cmd/, internal Go code, and frontend/src
(generated static/ excluded); CLAUDE.md and architecture/README.md state
the coverage rule: every code change lands in exactly one detail diagram's
scope, web/docs and cloud/ deliberately excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: PR bodies start with a TL;DR — max 5 informal one-liners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:53:23 +09:00
4f5f517a6f feat(cli): bdrive export/import — move projects between hubs with full history (#43)
The anti-lock-in story for cloud-hesitant users: export a project's
complete store (every device's journal + every blob, i.e. full history
and authorship) into a portable tar.gz, and import it as a fresh project
on any other hub — cloud → self-hosted or back.

The archive is simply the remote store layout plus a manifest, streamed
through the existing remote.Backend, so no server-side support is needed
and it works against every existing hub. Import verifies each blob's
content hash, rejects foreign tar entries, requires an empty target
project, and refuses journal-less archives. Reconnecting devices resume
exactly where they were, because their journals are byte-identical.

Docs: README + SKILL.md command tables, docs-site CLI reference section,
and a new step-by-step reference page (Migrate between hubs).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:04:05 -07:00
071e21cd1a ci: auto-bump beardrive-cloud OSS pin on merge; pre-PR diagram hook (#42)
* tooling: pre-PR hook keeps architecture/ diagrams honest

PreToolUse(Bash) hook blocks gh pr create when internal/webapp or
internal/remote changed but architecture/ didn't; CLAUDE.md documents the
rule and the '# skip-diagram-check' escape hatch for non-structural changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: bump beardrive-cloud's OSS pin on every merge to main

Each OSS main push commits the new sha to OSS_COMMIT in
runbear-io/beardrive-cloud (CLOUD_BUMP_TOKEN: fine-grained PAT, that repo
only, contents r/w — already set). The bump push runs cloud CI against the
new pin and, only if green, the prod deploy — closing the OSS half of the
CI/CD loop. Rebase-retry loop absorbs racing merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:24:18 -07:00
220e27a9c2 review fixes: gate init next-steps on background mode, shared stdinIsTTY, daemon reconnects on token change, whoami surfaces settings errors, doc staleness (#41)
- init -f no longer prints 'daemon now keeps this folder in sync' after
  the foreground daemon has exited
- one stdinIsTTY() helper (TTY or Cygwin pty) shared by init's prompt
  gate and login's headless fallback — the two sites disagreed on Cygwin
- the daemon drops its remote backend when the device token changes, so
  an account switch mid-run reconnects with the new credential instead
  of pushing with the old one (httpBackend captures the token at open)
- whoami reports a settings read error instead of claiming 'not signed in'
- self-hosting/authentication and manual/setup-by-hand now describe the
  automatic device-code fallback instead of presenting --device as required

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:56 -07:00
2b9beaf41a docs: architecture class diagrams + keep-updated-in-PRs workflow (#40)
architecture/ holds mermaid class diagrams of the bdrive web server: the
Server core with its seams (Source, AuthProvider, Directory, QuotaProvider,
remote.Backend) and the MetaStore persistence layer.

Convention (CLAUDE.md): a PR that changes the drawn structure updates the
affected diagram in the same branch and embeds only the changed diagrams'
mermaid blocks in the PR description. A PreToolUse hook on gh pr create
(.claude/hooks/check-arch-diagrams.sh) blocks PR creation when
internal/webapp or internal/remote changed but architecture/ didn't;
override with '# skip-diagram-check' when nothing structural changed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:18 -07:00
34afef17cc fix(cli): onboarding friction — headless login fallback, --version, init next steps, labeled authorship (#39)
BEA-7 launch-critical set from the onboarding audit:

- login: shells without a TTY auto-fall back to the device-code flow
  (agents/CI/SSH no longer hang on the browser callback); a failed
  browser open also falls back, and the waiting state hints --device
- bdrive --version now works (cobra root Version), same output as
  bdrive version
- init prints a next-steps block: daemon auto-sync note, the
  'bdrive init --project p-xxx' connect command for teammates,
  bdrive log / bdrive share
- authorship: the daemon re-reads settings.json every tick so a
  login/logout/account switch is reflected in op authorship instead
  of stamping a stale identity forever; whoami now shows the
  signed-in account and labels the git/OS author as the signed-out
  fallback

Docs updated in README, plugin SKILL.md, and web/docs reference/cli.md.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:35:48 -07:00
Snow LeeandClaude Fable 5 a00f7cd1a9 cli: hub-era help text; don't print unused projects.json path under SQL metadata
Root --help still described the retired direct-to-bucket model (S3/GCS as
the sync transport); clients sync only through a hub now. And with
database: sqlite/postgres the hub startup line printed a projects.json
path that is never read — misleading for self-hosters checking where
their metadata lives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 07:52:14 -07:00
Snow LeeandClaude Fable 5 1de5fb1633 build: fix Docker/Cloud Run source deploy ignores
.dockerignore excluded plugin/, which is now a compiled package
(plugin/embed.go embeds SKILL.md). Add .gcloudignore so source uploads
skip node_modules; anchor /bdrive so cmd/bdrive isn't excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdqEkKvj4Dc2d718mcV6EY
2026-07-21 23:51:20 -07:00
Snow LeeandClaude Fable 5 623da892d2 fix(auth): first-account bootstrap — admin emails activate on signup; add CI
A fresh hub following docs/self-hosting.md was a locked room: invite-only
(the default) showed "Sign up disabled" with nobody to mint an invite, and
the approval-gated posture stranded the first admin as pending forever.

Emails on the config's admin list are operator-vetted, so they now
activate immediately on signup (any posture), and while the hub has zero
accounts they may sign up even on an invite-only hub. Strangers still
can't take the bootstrap slot, and the door closes after the first
account. Validated end to end from scratch: hub boot → admin signup →
device-code login × 2 devices → init → bidirectional sync → hooks install.

Also adds the missing GitHub Actions CI workflow (build/vet/test on
ubuntu + macos) — the repo previously had no CI at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 23:24:25 -07:00
Snow LeeandClaude Opus 4.8 16210f3236 docs: use case — give coding agents the business context
Add a job-shaped use-case page for engineering teams whose customer
context lives where coding agents can't see it: sync a context/ folder
into the repo via --shared, gitignore it (one-writer invariant), point
AGENTS.md at it, and run the in→used→back loop. Register it in the
sidebar between team-wiki and company-brain.

Also sharpen team-artifacts' "send a person a link" story — markdown/HTML
render as pages at a URL, member-only vs public links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sSVdMviADW8pu6sAJ4SfX
2026-07-21 23:14:36 -07:00
Snow Lee dfef5720df webapp: organizations behind a Directory seam
The hub already abstracted authentication — AuthProvider, with BuiltinAuth
as the built-in implementation — and then reached around that seam three
times: Accounts() was declared on neither interface, admin.go type-asserted
*BuiltinAuth (five handlers silently degraded to 404/empty under any other
provider), and organizations were not on the seam at all.

That last gap had teeth. A deployment whose identities come from elsewhere
had no way to own its orgs, so the code that did own them wrote into the
hub's OrgDB from the side — and nothing stopped the hub from inventing an
org that the identity system had never heard of. One did: a hub-created org
held every project while the mirrored one sat empty, and no sync path could
see the difference.

Directory (directory.go) is where organizations live now. LocalDirectory
wraps today's OrgDB unchanged — same last-owner protection, same normEmail,
same "o-"+randHex(4) ids, same file/SQL persistence — so a self-hosted hub
behaves exactly as before. A deployment whose orgs are owned elsewhere
implements the same interface, returns ErrManagedElsewhere from the write
half, and the handlers answer 409 with ManageURL. The hub never learns why
a write was refused, only where to send the user.

Two rules shape the interface. Reads are on the request path: Role runs on
every project request, including the /store/* endpoints a device hits every
few seconds with a token that carries no identity claims, so an
implementation backed by a remote system answers from its own cache — and
that cache is its business, not the hub's. Writes are optional, because
"this hub owns its orgs" is a deployment fact, not a code path.

- Server.Orgs *OrgDB becomes Server.Dir Directory: 28 call sites, 8
  nil-checks, one writeDirErr helper for the 409 translation.
- /api/orgs gains manage_url per org — the destination of the account
  menu's Settings entry. The client follows a link and never branches on
  which kind of hub it is talking to.
- Org administration becomes a real route, /orgs/<id>, retiring one of the
  two URL-less panels CLAUDE.md grandfathers. When a directory's ManageURL
  is not hub-local, the SPA fallback redirects there instead — so a hub that
  cannot administer its orgs cannot paint a console whose every control 409s.
- Accounts() moves onto AuthProvider. admin.go's type assertion becomes an
  optional AccountApprover, and a provider without one now answers 503
  rather than an empty approval queue: "no queue here" and "queue is empty"
  are different answers and only one of them was true.

Two reviews drove the rest. The architecture review caught a browser page
load that could delete org members (a display read ran the full membership
reconcile, and a 200 with an empty user list evicted everyone), one write
site that escaped the 409 translation, and a webhook that could wedge an
event stream behind an unappliable event. The design review, over eight
rounds, caught the org page rendering live controls on a hub that cannot
use them, a share link made unrevokable by a long filename, nine keyboard
tab stops parked off-screen behind a closed drawer, and — five separate
times — a fix of mine that looked right in the source and did nothing in
the browser.

Conformance tests run both a writable and a read-only implementation against
one contract; the seat, prune, and out-of-order regressions each have a test
written to fail against the old code.
2026-07-20 03:06:19 -07:00
Snow LeeandClaude Opus 4.8 93f2c6bc84 docs: move Use cases below Manual setup
Sidebar order is now Start here -> Working with agents -> Manual setup
(optional) -> Use cases -> Self-hosting -> Reference -> Concepts.

README and CLAUDE.md carry the group order and the rule for what belongs
in each, so both move with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:06:40 -07:00
Snow LeeandClaude Opus 4.8 16ababe37d docs: use cases — five job-shaped pages that route into the guides
"Is this for me?" was answerable only by reading the guides and doing the
translation yourself. These five pages answer it directly, sit between
Start here and Working with agents, and route out rather than re-teaching
features:

- Share work across your team's agents — a team that doesn't live in a
  terminal: Cowork and Claude Code share plugins, so the agent does the
  setup and nobody opens a shell.
- Keep a wiki your agents maintain — knowledge written as a side effect
  of work; Insights' hot-and-stale quadrant is the maintenance queue.
- Turn a personal brain into a company brain — OKF bundles, gbrain repos,
  Obsidian vaults. They are already markdown directories, so there is
  nothing to convert.
- Run a personal wiki, publish part of it — history as the point, plus
  per-file public links.
- Carry one context across agents and devices — one project, many mounts,
  and what actually happens when two machines edit one file.

Titles are job-shaped; the persona is named in the first line and in the
description, which is also the search snippet and the llms.txt line.

The company-brain page is the long one (790 words vs ~400) because it
carries two frictions worth being honest about. gbrain's own team setup
shares a brain through a remote Postgres, an HTTP MCP server, and
per-teammate OAuth with isolation enforced in SQL; file sync plus each
person's local brain skips all of that at small scale, and the page says
what the server still buys you rather than dunking. And privacy does not
map cleanly: gbrain scopes per person, BearDrive's unit of membership is
the project, so a walled boundary is a separate project — stated plainly,
with a table.

Unverified: whether "let one machine run consolidation" matches how
gbrain teams actually work. Written from the docs, not from practice.

Verified: 23 pages build, zero broken internal links, both external
references (Google Cloud's OKF announcement, the gbrain repo) resolve 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 21:40:00 -07:00
Snow LeeandClaude Opus 4.8 8f239dfae4 docs: reorganize around the agent path, CLI becomes optional
The sidebar and the homepage disagreed. index.md's "Where to start"
already led with connecting an agent, but the left rail read Install
(brew) -> Quickstart (bdrive login, bdrive init) -> ... -> Connect an
agent, three groups down. Anyone following the rail met the CLI first and
the skill last — the opposite of how the product is meant to be adopted.
The agent page was also filed under guides/, which this repo defines as
agent-workflow docs rather than setup.

Sidebar order is now the recommended path, and that path is agent-first:

  Start here          what it is -> set up with your agent -> your first hour
  Working with agents shared memory, artifacts, read heat, scoping
  Manual setup (opt)  install the CLI, set up by hand, skills and hooks
  Self-hosting / Reference / Concepts   unchanged

- start/setup (was guides/connect-an-agent): rewritten as the front door.
  Claude Code's plugin, then the one-paste for Codex/Gemini/Hermes, then
  what the agent just installed and how to check it.
- start/first-hour (new): the page that was missing — ask for a doc, get
  a link back, share it, a teammate's agent picks it up. What success
  looks like without a command you have to type.
- manual/skills-and-hooks (new): the mechanics lifted out of the old
  onboarding page — per-platform paths, hook events, idempotency,
  project-level vs per-user — so the Start page can stay conversational.
- manual/install and manual/setup-by-hand (were start/*): both now open
  by saying you probably don't need them, and link back to the agent path.
- index.md leads with "You don't install it — you ask your agent to";
  the CLI and hub sentence moves below it.

No `brew install` appears anywhere in Start here. Reference -> CLI stays
exactly where it was: the people most likely to self-host are CLI-first,
and burying it would read as condescending.

Three public URLs moved, so astro.config.mjs declares redirects. Static
builds emit meta-refresh only, so README carries copy-paste 301 rules for
the host — Firebase Hosting and a Cloud Storage + load balancer URL map.

Verified: 18 pages build, zero broken internal links across the built
output, all three redirects resolve. CLAUDE.md and the docs README record
the rule so this doesn't quietly revert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 20:52:20 -07:00
Snow LeeandClaude Opus 4.8 25f890c03f feat(brand): the Stack mark and Jersey 10 across app, auth, and docs
Replaces the 🐻 emoji standing in for a logo everywhere. The mark is the
letter B built from three rectangles — a rail and two blocks, the same
shape as the product (a spine with volumes hanging off it). One fill, so
`currentColor` themes it in the sidebar, the favicon, and flat ink.

- Web app: <Mark> in shell.tsx replaces the emoji-in-a-gradient-tile
  badge; the mark takes the honey and the wordmark takes text colour, so
  the accent lands once. #vault-name sets in Jersey 10 at 18px — the face
  is condensed, so that measures like 13px of the UI face.
- Auth pages (authlocal.go): server-rendered, so they had their own emoji
  logo. Same mark, inline.
- Docs: bear.svg becomes the mark (fixed honey fill — Starlight renders
  the logo as <img>, which can't inherit currentColor), and .site-title
  sets in Jersey 10. Starlight tints that title with the accent by
  default, which put honey on white in light mode and failed contrast;
  it now takes --sl-color-white, matching the app.
- Favicon: the mark, as a data URI.

Jersey 10 is SIL OFL and self-hosted in both trees — Vite fingerprints
the app's copy into static/assets/, the docs serve theirs from public/ —
so no surface makes a third-party font request. Licence ships beside each
file. It is deliberately not a design token: tw.css's @theme block is
mirrored by the cloud landing's tokens.css and a drift check fails the
build if they diverge, so the logo face lives in plain CSS.

The cloud landing page carries the same mark and face (separate repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 19:38:42 -07:00
b10c6f961e docs: a Starlight docs site for docs.beardrive.ai (#38)
Adds web/docs — the public product documentation, built with Astro +
Starlight and deployed on its own rather than embedded in the binary.

Why a standalone site in the OSS repo, rather than a section of the cloud
landing page:

- Docs change far more often than the binary does. Embedding them would
  mean a Go rebuild and redeploy to fix a typo, and would ship a Pagefind
  search index inside every self-hoster's install.
- Self-hosting instructions and the CLI reference document the OSS
  project, so "edit this page" should resolve to something an outside
  contributor can open a PR against.
- Design tokens get easier, not harder: scripts/tokens.mjs generates
  src/styles/tokens.gen.css from the @theme block in the hub frontend's
  tw.css, so there is one source of truth and nothing to police. (The
  cloud landing sits across a module edge and has to keep a *copy*,
  guarded by its own check-tokens.mjs.) custom.css maps Starlight's
  --sl-color-* onto those tokens and invents no colors of its own.

Content is seeded from README.md, docs/self-hosting.md, and the plugin
skill. Guides deliberately cover agent workflows — connecting an agent,
the two-file AGENTS.md orientation pattern, artifacts and links, read
heat, scoping the folder — rather than re-teaching the CLI, which lives
in Reference.

starlight-llms-txt emits /llms.txt at build. Convention wants that at the
root domain, so beardrive.ai/llms.txt should point here; that redirect
belongs to the cloud landing and is the one cross-repo coordination point
this split introduces.


Claude-Session: https://claude.ai/code/session_018GcqsM6prjdv9rUrhVVEiC

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:29:33 -07:00
19da7dc890 site: move the landing page to the cloud module (#37)
The landing page sells BearDrive Cloud, so it now lives in the private
cloud module (web/landing, served by the hub at / for signed-out visitors)
instead of this repo. A self-hosted hub should never serve "Start free"
CTAs pointing at our managed service, and marketing for the paid product
doesn't belong in the AGPL tree.

Rebuilt there as an Astro site on the webapp's own design tokens, so the
landing and the app stop drifting apart visually.

Doc pointers updated to the new location.


Claude-Session: https://claude.ai/code/session_01RA5pQH92cxk5SfiYJeTUjK

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:58:45 -07:00
Snow LeeandClaude Fable 5 008e765ad5 chore: gitignore /docs/ (private notes) and /.review-shots/
Existing tracked docs (self-hosting, assets) stay tracked — README and
the website link them; the ignore only keeps new files local.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:37:40 -07:00
Snow LeeandClaude Fable 5 3f9e465463 refactor(web): one md-width column for app views; read is for rendered files only
- --page-read and --page-app both resolve to 768px (Tailwind md)
- History, folder listings, and the onboarding empty state move to the
  default app column; Insights already sat there. Only rendered markdown
  keeps read (HTML files keep their wide frame).
- layout.spec.ts repinned first (test-first); 59/59 e2e green
- shell.tsx docstring and stale style.css width comments updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:37:40 -07:00
Snow LeeandClaude Opus 4.8 1b5a422af2 webapp: eight agents in the demo seed, and far less staleness
Two changes to the demo data, both driven by how the screenshots read.

Warning triangles were on roughly half the files, which stops meaning
"look here" and starts meaning nothing. Staleness is now ~15% overall, and
correlated with reads rather than uniform: a heavily-read file is far
likelier to be stale, because it's the one everybody trusts and nobody
owns. That puts the red on big cells in the treemap and in the top-right
of the scatter — where the story is — instead of scattering it across a
hundred files nobody opens. Fewer warnings, and the ones left are the ones
worth reading.

The agent fleet goes from four to eight: one per teammate plus shared CI,
which is what a team's coverage matrix actually looks like once everyone
runs their own. Each has a bias (agentBias) toward particular areas, so
the matrix shows agents specialising instead of eight identical rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA5pQH92cxk5SfiYJeTUjK
2026-07-19 16:29:41 -07:00
Snow LeeandClaude Opus 4.8 52f595efc0 webapp: seed the demo hub with a realistic wiki
The demo harness generated files named run-015.md, des-031.md and so on,
with one-line bodies. Screenshots taken from it end up on the website, and
"des-031.md" in a treemap tells a visitor nothing about what BearDrive is
for.

Replaced the generator with a wiki a company would actually have: runbooks,
ADRs with real slugs, dated meeting notes, product and research docs, and
three hand-written documents (q3-findings, incident-response, first-week)
whose markdown renders with headings, tables, code and lists so the file
view is worth screenshotting. Read-heat shaping is unchanged — runbooks are
what the on-call agents live in, research notes are written for humans and
barely read by anything — so the insights views still light up.

Project renamed proj -> acme-wiki to match.

Also dropped the "must never be committed" note: the file has been in the
tree for a while and is genuinely useful for exploring the UI and taking
product screenshots. It still only runs under BDRIVE_MANUAL_SERVE=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA5pQH92cxk5SfiYJeTUjK
2026-07-19 15:28:45 -07:00
Snow LeeandClaude Opus 4.8 afac6bef65 fix(web): design-review fixes to the column system
A design pass over the new tiers found four problems, two of them
introduced by the refactor itself:

- Insights was assigned `wide`, but its charts are viewBox="0 0 720 …"
  SVGs at width:100% — a wider column didn't show more, it magnified:
  measured 1.67x at 1600px, painting a 10.5px treemap label at 21px,
  larger than the page h1. Insights moves back to `app` and .in-chart
  caps at its 760px design width. Widening a column must never mean
  scaling content up; that line is now written into shell.tsx.
- /install rendered the same ConnectGuide as the project home, but
  wrapped in the .onboard card: x=652 w=560 top=186 against home's
  x=492 w=880 top=96 — two sidebar items apart, same component, three
  different numbers. It renders directly now. .onboard stays what it
  is, the empty-state hero card.
- History was `app`, so `.htime { margin-left: auto }` stranded each
  timestamp ~600px from its path. It's a listing — same rows as the
  folder view — so it belongs in `read` alongside it.
- --hero-top: 10vh is viewport-relative in the wrong direction: 84px on
  a 390x844 phone against 80px on a 1280x800 desktop, i.e. the smallest
  screen paid the most. Now clamp(32px, 8vh, 88px).

Also fixes the Copy button in the guide's code blocks: it was absolutely
positioned over a scrolling box, so its 72px of reserved padding
scrolled away with the content and the button landed on top of the
command (visible mid-token at 560px and on mobile). The block is a grid
now — code scrolls in its own track, the button can't overlap it.

layout.spec.ts gains three assertions: /install and home render the
guide identically, no chart scales past ~1.0x at 1600, and the tier map
matches the new assignments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 15:08:30 -07:00
Snow LeeandClaude Opus 4.8 6cc61292ee refactor(web): one column system for every view
Every route invented its own content column: widths ran 560px to
unbounded (704 / 760 / 860 / 936 / 560), half of them uncentered because
they set max-width with no auto margins, and markdown pages carried the
constraint on #content itself so the 40px gutter came out of the reading
measure — .md text ran 624px against the folder listing's 704px directly
beside it in the tree.

Now there is exactly one primitive:

- #content owns scrolling and the page gutter, never a width.
- <Page width="read|app|wide"> (shell.tsx) owns width and centering, one
  per view, driven by CSS tokens --page-read/-app/-wide (704/880/1200).
  read = prose + listings, app = structured views, wide = data-dense.
- .markdown goes back to being typography only; the column around it is
  .page.read, which also retires the #content.markdown min-width hack.
- Views declare no layout: .guide/.insights/.history/.admin/.dirlist/
  .markdown lost their max-widths, Browser picks the width per route.
- Short centered states (empty, loading, not-found, no-preview, onboard)
  shared one --hero-top instead of 8/12/15/22vh apiece.

Insights moves to wide — its treemap and coverage matrix were cramped at
760. Everything else lines up: app pages at 880, read pages at 704, same
edges on every route.

e2e/layout.spec.ts locks it in: one .page per route, widths resolve to
the tokens, same-width routes share edges, #content never constrains
width, and no view re-declares a column inside .page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 14:50:38 -07:00
Snow LeeandClaude Opus 4.8 5c6099177b feat: bdrive skill install + one-paste Codex/Hermes setup guide
The hub's install guide told Codex and Hermes users to run four CLI
commands by hand, and the one people skipped — `bdrive hooks install` —
is exactly the one that makes files sync at turn boundaries. Hand the
setup to the agent instead, the way the Claude tab hands it to the
plugin.

- `bdrive skill install` (internal/agentskills, plugin/embed.go): the
  binary now carries the beardrive skill and writes it to any agent that
  reads SKILL.md — ~/.{claude,codex,gemini,hermes}/skills/beardrive/.
  User-level on purpose: the skill is about the CLI, not one folder, and
  a synced project folder should never carry it. Idempotent; refreshes a
  stale copy after a CLI upgrade. Bare `bdrive skill` prints the table,
  mirroring `bdrive hooks`.
- Guide's Codex/Hermes tabs are now a single paste, no terminal: the
  prompt has the agent install the CLI, keep the skill, sign in, init,
  and register hooks. The commands ride inside the prompt because these
  agents ship no BearDrive knowledge (Claude's tab is terse only because
  the plugin carries it). `login --device` there — a browser-callback
  sign-in is invisible to an agent mid-turn, while the device flow gives
  it a code and URL to relay. Plain commands live on in an "or run it
  yourself" fallback.
- Docs realigned: README, SKILL.md, /beardrive:install, self-hosting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 14:30:06 -07:00
Snow LeeandClaude Fable 5 7f17c1be98 fix(web): design round-2 polish — mobile 44px vault buttons, uncapped mobile nav, faint informational text, centered mobile Revoke
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:49:30 -07:00
Snow LeeandClaude Fable 5 e5aeca675b fix(web): design-review round 1 — overlay double-centering, tree file/dir classes, palette chrome, table cards, AA labels, mobile targets
- translate:none on .modal/#palette (Tailwind v4 translate utility stacked on
  the house transform, shifting every overlay and clipping the palette input)
- virtualized tree rows regain dir/file classes (file rows had fold chevrons)
- palette input: single icon, no shadcn border/ring leak
- members/shares tables wrapped in the .admin-list card vocabulary
- section labels ghost→faint (3.6:1 → 5.75:1 AA); mobile 44px icon buttons,
  projects-section clipping; switcher opens below trigger (popper)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:35:11 -07:00
Snow LeeandClaude Fable 5 389c4ba550 fix: drop stray worktree gitlink (gitignore .claude/worktrees/); sonner radius token
CTO review findings 1+4: the agent-worktree gitlink re-staged by add -A would
break --recurse-submodules clones; sonner referenced undefined --radius.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:21:09 -07:00
Snow LeeandClaude Fable 5 fad9ae4f6b feat(web): react-table admin tables + RHF/zod forms; docs reflect the Stack A dependency set
Members and shares-audit render through @tanstack/react-table (sortable,
spec-first); org rename, hub signup policy, and onboarding create/join are
react-hook-form + zod with inline errors replacing toast-on-typo. 55/55.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:15:24 -07:00
Snow LeeandClaude Fable 5 2e835d266d feat(web): file tree virtualized with @tanstack/react-virtual
Only the visible window renders (collapsed subtrees not at all — the ~5k-file
DOM cliff from the CTO review is gone). Flat rows keep data-path/.active
contract, nesting guide lines, mobile 44px rows; scroll-into-view moves into
FileTree via scrollToIndex. Fold behavior pinned by spec first. 54/54.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:08:39 -07:00
Snow LeeandClaude Fable 5 2993049bf4 feat(web): buttons on shadcn Button — house variants (primary/danger/subtle/toolbar) keep the exact look
cva variants map to the existing .pbtn/.danger-btn/.ai-btn/.btn classes, so
style.css owns appearance while shadcn adds focus-ring/disabled plumbing.
Migrated: modals, ShareDialog, EmptyState, Browser topbar. 53/53.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 13:05:13 -07:00
Snow LeeandClaude Fable 5 b7ce76b72e feat(web): palette on cmdk in a Radix Dialog — house fuzzy scorer kept (shouldFilter=false)
cmdk owns keyboard nav/selection/aria; matching, stemming, highlighting and
ordering unchanged. Specs target #palette input (cmdk owns the input id).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:57:08 -07:00
Snow LeeandClaude Fable 5 4a4e13f14c feat(web): modals on Radix Dialog — same imperative API, same classes, Radix-owned dismissal
modalPrompt/modalConfirm/ShareDialog render in DialogContent (.modal kept for
specs/styling); Escape pinned by spec first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:51:59 -07:00
Snow LeeandClaude Fable 5 cb06737af7 feat(web): project switcher on shadcn Select — specs moved to behavioral contract first
Trigger keeps #project-select + proj-mark; items portal'd with keyboard nav
from Radix; picking navigates (spec: open, both projects listed, pick → URL).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:49:19 -07:00
Snow LeeandClaude Fable 5 c1db12e75e feat(web): account menu on Radix DropdownMenu (non-modal for legacy click-through parity)
Ids preserved (#account-btn/#account-menu/#menu-*/#signout); Radix owns
Escape/outside dismissal + focus; dismissal behavior pinned by spec first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:47:47 -07:00
Snow LeeandClaude Fable 5 fe8911861c feat(web): search tooltip on Radix (shadcn) — portal'd, house-styled tipcard; provider at root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:39:33 -07:00
Snow LeeandClaude Fable 5 65c4a29bf8 feat(web): toasts on sonner behind the same toast() API; specs pin behavior via expectToast
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:37:46 -07:00
Snow LeeandClaude Fable 5 407c443a40 feat(web): Stack A foundation — Tailwind v4 + shadcn/ui wired, zero visual change
Tailwind v4 via @tailwindcss/vite, theme+utilities only (no preflight) so
legacy style.css keeps owning base styles during migration; BearDrive tokens
mapped into @theme incl. shadcn semantic slots (primary=honey, popover=raise…).
shadcn components copied in (button, tooltip, dropdown-menu, dialog, select,
command, sonner, input, label, form, table) with radix-ui/cmdk/sonner/cva;
sonner pinned dark (next-themes dropped). Stack A libs installed:
react-virtual, react-table, react-hook-form + zod. 50/50 e2e unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 12:34:52 -07:00
Snow LeeandClaude Fable 5 703dda4dbd agents: beardrive-cto — architecture/reusability/scalability reviewer for backend + frontend
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:49:45 -07:00
Snow LeeandClaude Fable 5 4c59bbc794 fix(web): search tooltip painted under the section border — lift the header's stacking context
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:44:59 -07:00
Snow LeeandClaude Fable 5 7ddb211fcf feat(web): scoped insights/history keep the file or folder selected in the tree
On /insights/<path> and /history/<path> the tree highlights (and unfolds to)
the target; Dashboard/History menu items light up only for their root,
project-wide views. 50/50 e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:41:35 -07:00
Snow LeeandClaude Fable 5 d18ad1fa91 test(web): regression specs — ⋯ Insights scopes to the open file and selected folder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:36:56 -07:00
Snow LeeandClaude Fable 5 288b426e82 fix(web): search tooltip clipped by the sidebar edge — right-align it
#sidebar is overflow:hidden; the centered card poked past its right edge.
The tooltip now grows leftward from the button, arrow anchored beneath it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:27:01 -07:00
Snow LeeandClaude Fable 5 3f974d96e4 feat(web): search moves beside the brand in the sidebar header, with a ⌘K hover tooltip
Icon-only trigger in the vault header (Linear-style); a tiny search.ts
emitter asks Browser's palette to open — no plumbing through the shell.
Custom tooltip card (label + kbd chip, arrow) on hover/focus. Topbar search
and its centered styles removed. 47/47 e2e incl. header-trigger spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:24:07 -07:00
Snow LeeandClaude Fable 5 75f2b0bfe9 feat(web): topbar cleanup — centered 2x search, icon-only Share, History/Download into the ⋯ menu
- search control sits centered in the topbar at ~2x width, kbd right-aligned
  (static again on mobile)
- Share is icon-only
- History button shows only on the project home: gone from dashboard/history
  (and other view routes) and whenever a file/folder is selected — the ⋯
  menu and sidebar carry it
- Download is ⋯-menu-only; a hidden anchor keeps the browser download flow
46/46 e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:16:50 -07:00
Snow LeeandClaude Fable 5 317045cecb feat(web): History in the project menu, after Installation
Navigates to the existing /history route; active state follows the URL.
46/46 e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
2026-07-19 11:07:14 -07:00