Skip to content

feat(frontend): rebuild the workspace shell on shadcn sidebar + site header - #570

Open
Ashin-LX-98 wants to merge 14 commits into
developfrom
reui
Open

feat(frontend): rebuild the workspace shell on shadcn sidebar + site header#570
Ashin-LX-98 wants to merge 14 commits into
developfrom
reui

Conversation

@Ashin-LX-98

Copy link
Copy Markdown
Collaborator

What

Rebuilds the workspace desktop shell around the ReUI app-shell-12 block, without installing the block itself.

App shell

  • Sidebar — icon-rail collapsible nav (collapsible="icon") built from Brand / NavMain / NavAgents / NavSecondary, plus the app-shell-12 rail handle (two bars that splay on hover with a Collapse/Expand label). Replaces sidebar.tsx / sidebar-content.tsx / sidebar-header.tsx; nav counts and the agent list keep their previous behaviour.
  • Site header — new h-12 bar with breadcrumbs (view + current thread/file), a Cmd-K command palette over threads/files/agents, a notifications popover reusing the Inbox card, and the account menu (theme, copy token, claim workspace, settings, sign in/out). Mobile gets the notifications + account menus in MobileHeader so nothing that lived in the old sidebar footer is lost.
  • SidebarProvider is controlled by the existing useLayout().isSidebarOpen, so every other caller is untouched.

Registry components landed by hand

npx shadcn add sidebar breadcrumb popover command skeleton would have overwritten 8 existing files (dialog, button, sheet, tooltip, input, textarea, separator, hooks/use-mobile) — dialog.tsx carries this repo's pinned header/footer contract and button.tsx differs from upstream by ~480 lines. So the six new files were extracted from the registry JSON and adapted instead: registry import paths rewritten, the broken IconPlaceholder import swapped for lucide icons, and size="icon-sm" mapped to this Button's size="icon". --sidebar-* tokens (light + dark) were added to globals.css; cmdk is the only new dependency.

Layout & dialogs

  • Panes now sit flush against the rail with 1px dividers instead of floating rounded-xl cards, and every pane header is h-12 with a shared bottom border, so the site header, list pane and detail pane line up.
  • share-dialog, create-routine-dialog and invitation-dialog now follow the dialog spec — actions live in a pinned DialogFooter, DialogBody is the only scroll region, and hardcoded zinc-*/blue-500/red-500 are replaced by design tokens. Native confirm/prompt calls move to the shared dialogs provider.

Notes

  • data-active={isActive} in the upstream sidebar.tsx renders data-active="false", which Tailwind v4's data-active: variant still matches — every menu item rendered as selected. Fixed to isActive || undefined.
  • The radix-nova CommandDialog does not include a cmdk root, so SearchMenu wraps its children in <Command> itself.
  • components.json is added with the @reui registry; the license key stays in .env.local (gitignored).

Testing

  • npx tsc --noEmit clean
  • npm run build succeeds
  • Interactive verification (rail collapse, Cmd-K navigation, notification read-sync, light/dark) needs a real workspace — reviewer should click through those.

🤖 Generated with Claude Code

…header

- App shell: icon-rail sidebar (brand, nav, agents, rail toggle) plus a new
  h-12 site header with breadcrumbs, cmd-K search, notifications and the
  account menu. Replaces sidebar/sidebar-content/sidebar-header; the settings
  dialog is extracted so the header menu can own it.
- Lands sidebar, breadcrumb, popover, command, input-group and skeleton from
  the shadcn radix-nova registry by hand — the CLI would have overwritten eight
  existing ReUI components (dialog, button, sheet, ...). Adds the --sidebar-*
  tokens and cmdk.
- Panes sit flush against the rail with 1px dividers instead of floating cards,
  and every pane header is h-12 with a shared bottom border.
- Share, create-routine and invitation dialogs follow the pinned
  header/body/footer spec and use design tokens instead of hardcoded zinc/blue.
- Confirm and prompt flows move to the shared dialogs provider.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openagents-workspace Ready Ready Preview Jul 31, 2026 12:46pm
openagents-workspace (beta) Ready Ready Preview Jul 31, 2026 12:46pm

Request Review

@zomux zomux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve with minor changes — solid, well-documented app-shell refactor; mostly presentational and tsc/build pass.

Would like addressed before merge:

  • command.tsx: DialogHeader/DialogTitle are rendered as siblings of DialogContent rather than inside it. aria wiring still works via Radix context, but Dialog.Root always renders children, so this leaves a persistent sr-only heading in the DOM when the palette is closed and diverges from the idiomatic shadcn pattern. Move the header inside DialogContent.

Worth a manual check:

  • notifications-menu.tsx handleNavigate only navigates when notification.channelName exactly equals a sessionId — if those are different id spaces, clicking a notification silently no-ops. Please click through against real notifications.

Nits (non-blocking):

  • settings-dialog.tsx collaborators list keeps a nested max-h-40 overflow-y-auto scroll region, contradicting the 'DialogBody is the only scroll region' convention applied to invitation-dialog.
  • components.json adds a third-party registry proxy (proxy.collectui.pro) with a bearer license key — only matters for future npx shadcn add, but confirm the proxy/license is trusted.
  • Controlled SidebarProvider still writes the sidebar_state cookie that's never read back; mobile mounts a second global Cmd+B listener toggling an unused openMobile.

Verified fine: no Cmd+K/Cmd+B collision; showCloseButton / NotificationCard.onNavigate / useIsMobile all exist on-branch; rail CSS vars inherit into the fixed handle; AppSidebar gated behind !isDetailExpanded; data-active v4 fix is correct.

Replaces the sidebar + site-header shell with ReUI's app-shell-4 pattern: one
`collapsible="icon"` shell holding two inner sidebars — a permanent icon rail
and a 440px list panel — beside a single action header.

- Rail (nav-rail) owns view navigation, agent presence and the global tools
  (search, notifications, account). Replaces nav-main/nav-agents/brand on
  desktop; the mobile sheet still uses them.
- List panel (list-panel) hosts the per-view list. Threads is rebuilt on the
  MailItem pattern: header + search row + filter tabs (All/Starred/Archived/
  DMs) + rich preview rows. Archived and DM sections become tabs.
- One action header (app-header) replaces every per-view header bar: expand
  control, prev/next navigation through the current list, the selected item's
  title, and a toolbar each view fills through a portal (AppHeaderActions), so
  view logic stays in the view. Deletes site-header.
- Knowledge is split into list + detail so it uses the shell instead of
  re-implementing a split view, which also gives it collapse for free. Skills
  and FileGrid fold their title rows into the header.
- Threads gain real unread state. The backend has no per-thread read marker,
  so it is derived client-side from activity vs. last-opened, persisted in
  localStorage and baselined on first visit. lastEventAt comes back null on
  some workspaces, so activity falls back to createdAt plus an observed
  preview-change signal. The rail dot and row dots now mean unread instead of
  "list is non-empty" and "running".
- List collapse is remembered per view in a cookie, so switching views or
  reloading no longer resets it. The sidebar's own sidebar_state cookie was
  never read back under controlled usage.

Also lands the radix-nova tabs component by hand and fixes two defects it
surfaced: tabs shipped `data-active:` variants that never match Radix's
`data-state="active"`, and the rail's icon buttons had no accessible name.
…fixes

Follow-up pass on the app-shell-4 workspace shell.

Theme
- --primary goes neutral (zinc-950 light / zinc-50 dark), replacing the blue
  brand colour, and every hardcoded blue accent follows: badges, inbox and
  notification unread states, spinners, tool-call and file icons, browser
  chrome, landing-page steps, the thread shimmer. Vendor brand colours
  (Google, Gemini, DeepSeek) stay as they identify third-party products.
- Threads filter pills use the neutral fill instead of blue primary.

Shell
- The rail expands to show labels and collapses back to icons, persisted in
  the oa-rail-expanded cookie; the shell now owns its own width instead of
  relying on collapsible="icon", whose group rules squeezed every menu button
  to 32px and hid the labels on views without a list panel.
- The list keeps its collapse handle everywhere except Threads, where the
  message list is always wanted; the header's prev/next item buttons are gone.
- Persist list/rail preferences outside the state updater — writing a cookie
  from an updater is a render-phase side effect.

Chat
- The composer spans the same column as the messages instead of sitting in a
  narrower max-w-3xl box, and its padding is tightened.

Fixes
- The rail unread dot counted archived and routine sessions, so it stayed lit
  with nothing unread in the list; it now uses the list's own rule.
- Command palette: cmdk marks unselected items data-selected="false", which
  the bare data-selected: variant still matched — every row was highlighted.
  Match the value instead, and move the sr-only DialogTitle inside
  DialogContent so the dialog is labelled.
- Skill Hub: the view's title was never rendered on desktop (no titleInHeader),
  so the skill count never appeared; search moves into the header at a sane
  width and Upload becomes a primary button.
Rail: the trailing seam is now a DingTalk-style control — a round toggle
floating over the border at mid-height, on a strip that also drags. The
drag follows the pointer and snaps to one of the rail's two states, so
the width never lands somewhere in between. The old hover-behind-the-logo
toggle is gone; the list handle only renders while the list is open, so
the two controls can't stack on the same seam.

Also draws the rail/list seam with `border-border` — `border-sidebar-border`
is transparent in this theme, so that divider was never visible.

Chat: ChatGPT layout. One centered max-w-3xl column for messages and the
composer; your own turns are right-aligned bubbles, agent turns are full
width with a hover action bar. The composer is a single row — `+` menu
(attach file/image, create routine), textarea, send — with the standalone
attachment icons folded into the menu.

Threads: drop the "N agents" badge; the avatar stack already says it.
Chat
- Messages go full-bleed like Slack/Feishu instead of sitting in a fixed
  max-w-3xl column; only the gutters are fixed. Own-turn bubbles cap at
  min(85%, 42rem) so they don't stretch with the pane.
- The composer keeps a fixed measure but steps up on large displays
  (768 → 896 → 1152px): 768px is cramped on a 27" 4K where the pane is
  2000px+ wide.

Theme
- defaultTheme becomes "system" — enableSystem was already on, the default
  just never used it. The account menu's binary light/dark toggle becomes a
  System/Light/Dark submenu showing the current value.
- Thread rows: the selected outline was drawn with a zinc token, which is a
  cool grey and picks up a blue cast on both surfaces. Redraw it in plain
  black/white alpha, rounded, with the row's own divider transparent so the
  two don't collide.
- Tab count badges dropped appearance="light" — its secondary compound
  variant is bg-secondary/50 in dark, which washes out on the near-black
  surface. app-shell-4 uses the solid default; match it, and fill the active
  pill with `primary` as it does.

Dialogs
- One spacing scale across confirm/prompt, New Thread, Share and Create
  Routine: px-7 gutters, text-xl titles, 12px title/description gap and
  min-w-24 footer buttons.
- Sign out now asks first — it sits one row under "Workspace settings".
- New Thread: the agent list had its own max-h + overflow-y, so a long list
  scrolled inside a nested box and bypassed DialogBody's hidden-scrollbar
  scroll region.
- Skill detail was a hand-rolled overlay pinned to top-[5%]/bottom-[5%], so
  it filled 90vh no matter how little it held, with no focus trap, no Esc
  and no mobile drawer. Rebuilt on responsive-dialog.
- Create Routine: weekday chips get a softer fill than the interval presets —
  days start all-selected, and seven solid `primary` blocks read as a wall of
  white in dark mode. The active schedule segment gains a ring, since a drop
  shadow alone doesn't separate it from the track.

Layout
- Skill Hub used auto-fill,minmax(250px), which keeps packing columns as the
  viewport grows — nine ~260px cards per row on a 27". Explicit per-breakpoint
  counts hold each card at 335-390px anywhere.
- Command palette: 512 → 672px, 44 → 56px search row, roomier rows.
- Files, Routines and Browser list panels drew their own h-12 header; Files
  had no title row at all, so its search box sat where every other panel puts
  a title. All three now use the Threads/Knowledge shape.
- New-thread icon becomes MessageSquarePlus (SquarePen reads as "edit this
  list"), and the Routines title drops its icon.
Thread rows sat flush against the panel edge, so the selected row's inset
ring was drawn underneath the 8px scrollbar overlaying the viewport.

Pad the scroll viewport rather than the rows: it covers thread rows, DM rows
and the empty states in one place, and the gutter is identical whether or not
a row is selected, so selecting one doesn't shift the layout.
The frontend half of the Files rework. The endpoints behind the folder and
trash calls land separately in #577; everything here degrades to what it did
before until that ships.

Folders and trash:
- The folder panel owns the hierarchy: create, rename and delete a folder, each
  going through the new /files/folders routes and refetching, since one folder
  mutation rewrites many file records.
- Trash is its own section under the folder tree rather than a folder row — it
  isn't a place files live and you can't browse into it. Picking it swaps the
  detail pane wholesale via the layout's new filesSection.
- trash-view renders those entries; its rows are still mock data, and the
  restore/purge endpoints are what it reads next.

Type identity, which the grid and the list needed before anything else:
- file-utils gains a kind per file (pdf, sheet, image, code, …) plus the filter
  groups the type menu is built from — the same split the server now makes, so
  a filter menu and a listing can't disagree.
- Per-kind accent hues in globals.css, light and dark, shared by the grid
  tiles, list rows, the preview header and the ⌘K palette. A folder of uploads
  is a wall of near-identical names; colour sorts it before you read one.
- The preview pane is rebuilt around that kind: text and code with line
  numbers, CSV/TSV as a real table, images with zoom and rotate, PDFs through a
  blob URL so the browser's viewer takes over instead of downloading, and a
  named "can't preview this" state for the formats that have none.

Image thumbnails:
- An image now shows its own pixels instead of a glyph — 76px in the grid,
  28px in the list rows. Both sit in the same slot as the tile they replace, so
  nothing reflows as they load.
- The thumbnail is the full file (there's no resizing endpoint), so it's gated:
  a format browsers can actually decode (HEIC and TIFF keep the glyph), under
  8MB, lazily loaded, and a failure is remembered so a broken file isn't
  re-requested on every scroll.
Trash stops being mock rows and becomes the real thing, and the preview pane
grows viewers for the kinds it used to hand straight to the download button.

Trash
- Wired to the trash endpoints: list, restore, purge, empty. State lives in
  the workspace context rather than the view, because the folder panel counts
  it while the Trash view isn't mounted.
- Deleting a file or a folder now goes through POST /files/trash instead of the
  older DELETE routes. Those soft-delete without recording when or as part of
  what, so a deleted folder came back as N undated loose rows instead of one
  restorable entry. Copy follows: deletes say "moves to the Trash", not
  "permanently deleted".
- Rows use the same leading square as the file list, so trashed images show a
  thumbnail — what you decide a restore on.

Media and image viewers
- Audio and video get real players built on <audio>/<video>: skip, speed,
  volume, picture-in-picture, full screen, loop. All state mirrors the element's
  own events, so the keyboard, the OS media keys and PiP can't desync it.
- The audio waveform is measured off the decoded samples, gated to files under
  15MB. A decorative one built from random numbers would be making claims about
  a recording it has never heard.
- Nothing is operable until there's a duration to seek within: a play button
  that does nothing and a 0:00 / 0:00 readout both read as broken rather than
  as still coming, which is what a large file looks like for a while.
- Images get a toolbar, a canvas that scrolls at any zoom, sizing modes and
  paging through the folder's other pictures. Zoom is a percentage of the
  image's own pixels, which is what makes the overflow reachable.
- Shared chrome (side panel, toolbar, status footer) so the three read as one
  viewer with different middles. The panel holds what the header doesn't
  already say, and toggles.

Folders and shell
- Folder create/rename validate the name while it's being typed — a clash is
  caught in the dialog instead of by the server two seconds later.
- Files browse state (view, sort, filter, search) moved into the layout
  context: opening a file replaced the grid, so a filter set to find that file
  was thrown away by using it.
- One back icon across the Files headers, counts aligned down the folder panel,
  empty-state copy that stops wrapping mid-sentence.
- Dropped the new-thread button from the mobile header; starting a thread lives
  in the threads list, where the new thread will appear.

Needs a companion backend change to land before trashed images can render
their thumbnails: GET /v1/files/{file_id} currently 404s on status != active.
With no folder picked the pane sat empty telling you to pick one, spending the
whole width on an instruction. It now lists the 50 newest files in the
workspace — what you're usually after on opening Files is the thing you or an
agent just put there.

No new endpoint: every file is already loaded to draw the folder tree, so this
is a sort and a slice. (GET /v1/files is ordered by created_at desc and takes a
limit, if it ever needs to be a fetch.)

The listing is a starting point, not a place. There's no folder to upload into
and no subfolders to open, and every row carries the folder its file actually
lives in — a flat dump you could mistake for a folder is one you'd try to act
on. Sorting is fixed to newest-first, because that ordering is what "recent"
means; the sort control stays hidden until you're in a folder.

The grid/list switch moves out of the folder-only block: how you like to read a
listing isn't a fact about the folder, and it applies here too. The empty state
survives for a workspace with no files at all.
The GPT-style surface moved your own messages into right-aligned bubbles
with no avatar or name. In a shared workspace thread that reads wrong —
your turns dropped out of the column everyone else's messages live in.

Human messages render one way again: avatar + name + time on the left,
matching the agent turns. Own messages still read as "You".
The detail pane depended on the whole `knowledge` array, and
refreshKnowledge() calls setKnowledge() with a brand-new array even when
nothing changed — so every list mount, refresh click, delete and tab
switch threw away the rendered article and flashed a spinner. Depend on
the selected entry's updatedAt instead.

Also strip the leading H1 when it repeats the entry title (the article
header already renders it), fill the pane instead of sitting in a centred
column, and open up the heading hierarchy: MarkdownContent's scale is
tuned for chat bubbles, where an h2 at 16px against 15px body text reads
as bold text rather than a section break.
…s error page

A live tab had no health signal at all. The screenshot poll is the only
thing that ever set sessionDead, and it returns early whenever liveUrl is
set; validation ran once on mount, and its deps omitted liveUrl so a tab
whose data arrived after first render was never checked. Meanwhile an
expired share link just swaps in BrowserFabric's own "invalid or expired
share link" page inside the iframe — cross-origin, unreadable from here.
Tabs stayed "healthy" indefinitely while showing BF's error screen.

- Re-validate every 30s while a live URL is present, keyed on whether one
  exists rather than its value (validate can return a new URL, which
  would otherwise re-trigger the effect).
- Clear sessionDead when validation succeeds; it was only ever set, so a
  tab the backend auto-reconnected stayed marked dead forever.
- Check sessionDead before liveUrl when rendering: an expired tab still
  carries its stale share URL, so the iframe branch always won and the
  reconnect UI was unreachable.
sm:max-w-xl left the agent list and install command cramped; the existing
layout (two-column meta grid, wrapping tag rows) already adapts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants