* 🔒 fix(acceptance): close the holes the open discussion opened
Opening an acceptance's discussion to whoever holds its public link widened
three things it should not have.
An agent belongs to its workspace, not to its readers. The author-agent gate
was scoped by the acceptance's workspace, which answers "is this agent
visible there", not "may this caller use it" — and the discussion payload
hands a visitor the ids to pick from. It is now scoped to the workspace only
for callers who actually belong to it; everyone else falls back to their own
agents.
Read-only means read-only. A `viewer` of the acceptance's own workspace was
swept in by "anyone signed in who can read it" and could write on a private
delivery, unlike the same role on topic and document comments. Commenting
now needs the public link or a non-viewer seat.
An open discussion needs a way to close an unwanted remark. Its author is
exactly who will not remove it, so the acceptance's reviewers may now take
one down — the same tombstone the author's own deletion leaves — and a
visitor gets a per-minute ceiling on how much they can land.
Two client reads of `canDelete` as "I wrote this" are replaced by the author
id, since that flag now also turns on for a moderator: whether a thread may
be closed, and whose approval the reviewer bar is showing. The read-only
line no longer flashes while the permission answer is in flight, and the
sign-in return URL moves to a shared helper.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 🔒 fix(acceptance): do not call an outside reader a departed teammate
The `former` tag infers "left the workspace" from "is not a member now".
That only holds on a delivery nobody outside could write on. With the link
public, most people in the discussion never had a seat, so the tag is
dropped there rather than guessed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 🐛 fix(acceptance): charge the visitor ceiling where the row is written
Codex review of the open-discussion hardening, three findings.
The ceiling was counted before the write and enforced after it, so a burst
of parallel requests each read the same under-limit count and all landed —
which is the shape a flood actually arrives in. It now travels into the
write: the transaction takes an advisory lock on (acceptance, author),
inserts, counts, and rolls itself back when it is the one over the line.
Everybody else still writes in parallel.
The same move fixes a retry being punished for a remark it already made. A
replay of the tenth comment's idempotency key inserts nothing, so it is no
longer counted and answers with the row it wrote the first time, instead of
a refusal for a lost response.
`canResolveThread` reads the signed-in identity, which arrives after the
first paint, but the overlay memo did not depend on it — a thread could keep
the answer computed while nobody was signed in until an unrelated comment
changed. It is a `useCallback` on what it reads, and the memo depends on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 🐛 fix(workbench): let the SSR electron stub answer the gateway device question
Five shared store modules read getElectronStoreState().gatewayDeviceInfo to
decide whether a working directory is local. They enter the workbench SSR graph
through the acceptance check route, where the stub had no such key, so the stub
surface guard failed the build. Give the stub the key with no device, which is
the truthful answer on the server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Every manual rolldown group gets a $initial-tagged variant so first-screen
modules never share a chunk with lazy ones, an app-initial catch-all folds
the rest of the entry's static graph into one chunk, and dependency capture
stops being recursive so lazy groups cannot drag first-screen modules into a
chunk the entry then has to preload.
Web production build: 71 eager chunks / 2299 KB gzip -> 10 / 2162 KB.
Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
* 👷 ci(share,workbench): upload PR previews to sibling -preview Workers
Cloudflare rolls back only to the 100 most recently uploaded versions of a
script, and PR preview uploads landed on the production scripts, so a busy
day pushed every real deployment out of the rollback window. Previews now
go to lobehub-share-preview and lobehub-workbench-preview; the preview
aliases move with them (pr<N>-lobehub-share-preview...).
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
* 📝 docs(split-micro-app): previews upload to a sibling -preview Worker
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
* 👷 ci(share,workbench): stamp actor, commit and run on every Worker version
Cloudflare records only the API token's owner as a version's author, so a
deploy from CI or a laptop was unattributable. wrangler deploy and PR preview
uploads now carry --tag <sha7> and --message "<actor> <repo>@<sha7> run <id>"
(local deploys: "<git email> local@<sha7>"), which 鳥居番's Deploys page
already shows per version.
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
* 📝 docs(split-micro-app): 鳥居番 Deploys page has Releases and Previews tabs
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
* 📝 docs(split-micro-app): releases and previews live on the target's Worker card
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
* 📝 docs(split-micro-app): Set override writes the staging config directly
Claude-Session: https://claude.ai/code/session_01BKMAksDxdePWPW4kgr9KpM
The antd-based wrappers are deprecated since @lobehub/ui@5.33.0. This moves every call site to the base-ui subpath.
Beyond the import path, the migration required:
- ActionIcon now renders a native <button> instead of a div: dropped the hardcoded 'nativeButton: false' in ActionDropdown's trigger props (it overrode the library's own inference, so Base UI applied role/aria-disabled to a real button), and retyped the dictation control refs to HTMLButtonElement
- Avatar is no longer antd's: replaced src/icon with avatar, dropped gap (no equivalent)
- Tag is no longer antd's: dropped bordered={false}, which antd v6 already ignored whenever variant was set — and the wrapper always set it
- prop types (ActionIconProps, AvatarProps) now come from the base-ui subpath, since the legacy ones carry antd props and a div ref
- seven files that still imported Button from @lobehub/ui or antd are migrated too, since touching them surfaced the existing no-restricted-imports error: variant=filled -> type=fill, variant=outlined + color=default -> type=default
Verified: type-check clean, 503 tests pass across ChatInput/NavPanel/User, and the migrated surfaces render with no Base UI warnings.
The product surface is the Acceptance domain: every route is /acceptance, and what lived under features/Verify was either the acceptance page itself or one of its sub-objects — a round's report, the in-chat round card, the criteria editor. 'Verify' no longer named anything of its own, and the page sat at features/Verify/Acceptance, one level deeper than its siblings.
New layout, grouped by sub-object instead of nesting Acceptance/Acceptance:
- Viewer/ the acceptance page atoms (was Verify/Acceptance)
- Workspace/ the acceptance list panel (was Verify/Acceptance/Workspace) + useReportPanelExpand
- Report/ ReportViewer and the evidence/visualization renderers (was Verify/components)
- Run/ RunResult and CheckerDock, the in-chat round card
- CriterionList/, hooks, utils, routeMeta unchanged at the root
Removes the dead VerifyWorkspace / ReportListPanel / EmptyDetail trio (zero consumers since AcceptanceWorkspace replaced it). Pure move otherwise: git detects 90 renames; imports rewritten across src, apps/workbench and packages, plus the path literals in the workbench stub tests and two skill docs.
Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(share): extract share pages into a standalone SSR micro app
`/share/t/:id` and `/share/page/:id` were client-only routes inside the main
SPA, so a shared link opened on a blank shell and link previews carried the
generic app card. They now live in `apps/share`, built the same way as
`apps/workbench`: a React Router framework-mode app that server-renders on a
Cloudflare Worker for cloud, plus a plain SPA build that Next serves for
self-hosted deployments.
What the server renders is the shell, the hero and a complete meta set — title,
description and the OG/Twitter card — resolved from the route loader through a
per-request tRPC client. The conversation and page-viewer stacks stay on the
client: `clientOnly()` gates them and, in the SSR environment, a resolver stub
cuts every `*.client` module out of the worker graph entirely. Gating the render
alone does not help, because the dynamic import still pulls the graph into the
bundle.
Keeping that worker deployable took three more cuts, each found by tracing the
module graph rather than guessing:
- `@/store/user` is stubbed for SSR — `GroupAvatar` reads the viewer profile,
and the store drags the model catalog and every provider behind it (39MB→19MB)
- `shiki` and `shiki/wasm` are stubbed for SSR — the `@lobehub/ui` barrel reads
the bundled-language catalog at module scope, registering ~400 grammars for a
highlighter the server never runs (19MB→7.6MB, 1.78MB gzip)
- SSR i18n resolves only the namespaces the shell renders; the generic loader
would register every locale x namespace pair as a worker chunk
The conversation list keeps its `headerSlot` while it shows the skeleton. It
returned early before, so mounting the list to fetch blanked a title the server
had already rendered.
`/share` is removed from the desktop and mobile routers and rewritten by the
Next middleware to `/spa-share/<locale>/…`, mirroring how `/verify` reaches
workbench. The cloud worker is published from lobehub-cloud, which builds this
same app against its business overlay, so this repo ships no deploy workflow
for it.
* 🐛 fix(share): preload the error namespace so the boundary renders translated
The root ErrorBoundary reaches for the `error` namespace, which SSR did not
bundle, so a failed share page rendered `error.title error.desc error.retry`
verbatim. Fetching it on demand does not fix that case: a chunk-load failure is
exactly when the client can never load the dictionary to correct itself.
* 🐛 fix(share): drop the dev template fetch that served the main SPA
The handler asked the main Vite server for `/index.share.html`, which does not
exist at the repo root. Vite answers that with the HTML fallback — 200, main SPA
shell — so `/share/*` in `bun run dev` silently booted an app that no longer has
those routes. Share is developed against its own Vite server, so the branch goes
rather than the file being added.
`spaDevShells.test.ts` guards both directions: every remaining dev template must
exist at the root and point at its own app entry, and share must not reintroduce
one.
* 📝 docs(skill): record the cloud-overlay split in split-micro-app
Share is the second micro app and the first whose rendering code lives in the
lobehub-cloud business overlay, so the skill needed the ownership rules that
decides which repo builds and deploys, and the Vite 8 tsconfig-nearest-importer
landmine that makes an overlay build ship the open-source fallback with a green
build.
Also folds in what the extraction turned up: the full Docker chain, the dev shell
that answers 200 with the wrong app, shiki and @lobehub/ui bundle cuts, the error
namespace, hydration and virtualized-prefill rules, the asset prefix convention,
and the prod promote gate.
* ✨ feat(acceptance): restore in-app workspace and assemble the public page from atoms
Keep the full owner workspace in the main SPA and compose the workbench share page from read-only capability atoms so owner actions stay out of the public bundle.
* 📝 docs(compose-atoms): generalize the skill around grain and sink-state
Make the kit apply to any heavy domain feature, and keep focus review filling the in-app frame.
* ⚡ perf(workbench): slim the public page and stop shipping shiki in the bundle
Keep the share surface to identity only, shim the browser lambda client to cookies, stub shiki on SSR, and load grammars from esm.sh on the client.
* ✨ feat(acceptance): show goal and check inventory on workbench detail
* 🐛 fix(acceptance): handle null acceptanceId in useAcceptanceBundle
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(workbench): put a go-to-app CTA on the public header
* ✅ test(workbench): fail the build when the graph uses stub-missing APIs
Keep workbench stubs as an explicit surface and scan the module graph on
build:rr so a later call to a simplified-away method is a build failure.
* 🐛 fix(acceptance): type verdict helpers with TFunction
Database CI lint failed because useTranslation('verify') is not assignable
to the hand-written (key: string) => string helper signatures.
---------
Signed-off-by: Innei <tukon479@gmail.com>
CI was building the worker and wrangler-deploying without `bun run deploy`,
so hashed `/workbench/` assets never reached R2. Run the full deploy script
and inject the shared `ASSET_S3_*` web-assets credentials.
* ♻️ refactor(spa): extract verify/acceptance into apps/workbench
Move the Workbench SPA into apps/workbench and serve /verify and
/acceptance from that runtime for every web UA. The main SPA no
longer registers those page routes, so they drop out of its bundle.
* 🔧 chore(docker): wire the workbench workspace into image builds
Copy apps/workbench/package.json before pnpm i so Docker can resolve
the extracted SPA. Keep generateSpaTemplates optional for Cloud, and
require the HTML only when WORKBENCH_REQUIRED=1 (build:docker).
* ♻️ refactor(workbench): hard-navigate only from markdown entity links
Keep Link and useWorkspaceAwareNavigate in-router. Markdown verify,
acceptance, and mobile agent-doc links document-navigate. Drop the
__WORKBENCH__ Vite define; Electron still skips hard-nav for portals.
* ♻️ refactor(verify): slot-inject origin conversation and decouple shared components
* 🐛 fix(i18n): make vite glob loaders independent of the vite root
* ✨ feat(workbench): React Router framework mode SSR on Cloudflare Workers
* 🐛 fix(verify): render viewer-timezone timestamps only after hydration
* 💚 ci(workbench): deploy with affected detection and PR build verification
* 📝 docs(skills): micro-app split and gateway routing playbook
* 🐛 fix(workbench): track app/lib swallowed by the root lib/ ignore rules
* ✨ feat(workbench): signed-in acceptance list sidebar on detail pages
* 🐛 fix(workbench): survive shared-origin gateway quirks in client navigation
* 💄 style(workbench): surface error detail on the boundary page
* 💄 style(workbench): adopt the shared ErrorCapture page for the root boundary
* 💄 style(verify): drop the app-home back arrow from the acceptance list panel
* 🔧 chore(workbench): persist only error logs in wrangler observability
* 📝 docs(skills): record Docker vs Cloud hosting for micro apps
OSS Docker still builds and serves the SPA in-image. Cloud Next must
not build, rewrite, or CDN-upload it — gateway routes to the Worker.
* 💄 style(workbench): pin the acceptance logo while the list panel slides
* 🐛 fix(workbench): only show the acceptance sidebar when signed in
* 🐛 fix(verify): drop namespace prefix on bound verify translations