feat(frontend): rebuild the workspace shell on shadcn sidebar + site header - #570
Open
Ashin-LX-98 wants to merge 14 commits into
Open
feat(frontend): rebuild the workspace shell on shadcn sidebar + site header#570Ashin-LX-98 wants to merge 14 commits into
Ashin-LX-98 wants to merge 14 commits into
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
zomux
approved these changes
Jul 28, 2026
zomux
left a comment
Contributor
There was a problem hiding this comment.
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.tsxhandleNavigate 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.tsxcollaborators list keeps a nestedmax-h-40 overflow-y-autoscroll 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Rebuilds the workspace desktop shell around the ReUI app-shell-12 block, without installing the block itself.
App shell
collapsible="icon") built fromBrand/NavMain/NavAgents/NavSecondary, plus the app-shell-12 rail handle (two bars that splay on hover with a Collapse/Expand label). Replacessidebar.tsx/sidebar-content.tsx/sidebar-header.tsx; nav counts and the agent list keep their previous behaviour.h-12bar 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 inMobileHeaderso nothing that lived in the old sidebar footer is lost.SidebarProvideris controlled by the existinguseLayout().isSidebarOpen, so every other caller is untouched.Registry components landed by hand
npx shadcn add sidebar breadcrumb popover command skeletonwould have overwritten 8 existing files (dialog,button,sheet,tooltip,input,textarea,separator,hooks/use-mobile) —dialog.tsxcarries this repo's pinned header/footer contract andbutton.tsxdiffers from upstream by ~480 lines. So the six new files were extracted from the registry JSON and adapted instead: registry import paths rewritten, the brokenIconPlaceholderimport swapped for lucide icons, andsize="icon-sm"mapped to this Button'ssize="icon".--sidebar-*tokens (light + dark) were added toglobals.css;cmdkis the only new dependency.Layout & dialogs
rounded-xlcards, and every pane header ish-12with a shared bottom border, so the site header, list pane and detail pane line up.share-dialog,create-routine-dialogandinvitation-dialognow follow the dialog spec — actions live in a pinnedDialogFooter,DialogBodyis the only scroll region, and hardcodedzinc-*/blue-500/red-500are replaced by design tokens. Nativeconfirm/promptcalls move to the shared dialogs provider.Notes
data-active={isActive}in the upstreamsidebar.tsxrendersdata-active="false", which Tailwind v4'sdata-active:variant still matches — every menu item rendered as selected. Fixed toisActive || undefined.CommandDialogdoes not include a cmdk root, soSearchMenuwraps its children in<Command>itself.components.jsonis added with the@reuiregistry; the license key stays in.env.local(gitignored).Testing
npx tsc --noEmitcleannpm run buildsucceeds🤖 Generated with Claude Code