From 6d628f8ef28cf8c0fb661a1e8abd4278d2b866df Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:50:47 +0000 Subject: [PATCH 01/15] feat(mobile-nav): add Files tab, active-only tab labels Append Files after Git to preserve the existing six tab positions and mirror the desktop right-sidebar grouping. Show text only for the selected tab while retaining accessible names for icon-only tabs. --- packages/ui/src/components/Navbar.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Navbar.tsx b/packages/ui/src/components/Navbar.tsx index 295dd5d82cb..fae1799a891 100644 --- a/packages/ui/src/components/Navbar.tsx +++ b/packages/ui/src/components/Navbar.tsx @@ -7,6 +7,7 @@ import { Terminal as TerminalIcon, Desktop as DesktopIcon, GitFork as GitForkIcon, + FolderSimple as FolderIcon, List as ListIcon, Gear as GearIcon, CaretLeft as CaretLeftIcon, @@ -90,13 +91,15 @@ function NavbarIconButton({ ); } +// Keep in lockstep with MobileTab in packages/web-core/src/shared/stores/useUiPreferencesStore.ts — ui can't depend on web-core, so these two unions are intentionally duplicated. export type MobileTabId = | 'workspaces' | 'chat' | 'changes' | 'logs' | 'preview' - | 'git'; + | 'git' + | 'files'; export const MOBILE_TABS: { id: MobileTabId; icon: Icon; label: string }[] = [ { id: 'workspaces', icon: LayoutIcon, label: 'Wksps' }, @@ -105,6 +108,7 @@ export const MOBILE_TABS: { id: MobileTabId; icon: Icon; label: string }[] = [ { id: 'logs', icon: TerminalIcon, label: 'Logs' }, { id: 'preview', icon: DesktopIcon, label: 'Preview' }, { id: 'git', icon: GitForkIcon, label: 'Git' }, + { id: 'files', icon: FolderIcon, label: 'Files' }, ]; export interface NavbarBreadcrumbItem { @@ -264,6 +268,7 @@ export function Navbar({ From 9fc0d68fbf29048eee4ad6a86ce3467926547d10 Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:50:54 +0000 Subject: [PATCH 02/15] feat(mobile-nav): add files to MobileTab, drop unsafe cast Keep the web-core tab union aligned with the UI tab ids. Assign RightMainPanelMode directly so TypeScript rejects future modes that are not also valid mobile tabs. --- .../src/shared/stores/useUiPreferencesStore.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/web-core/src/shared/stores/useUiPreferencesStore.ts b/packages/web-core/src/shared/stores/useUiPreferencesStore.ts index 4e684c5d508..e8b02c2c6ce 100644 --- a/packages/web-core/src/shared/stores/useUiPreferencesStore.ts +++ b/packages/web-core/src/shared/stores/useUiPreferencesStore.ts @@ -9,18 +9,21 @@ export const RIGHT_MAIN_PANEL_MODES = { PREVIEW: 'preview', } as const; +// RightMainPanelMode must stay a subset of MobileTab. The assignments below intentionally rely on structural typing (no cast), so adding a mode without a matching mobile tab fails to compile. export type RightMainPanelMode = (typeof RIGHT_MAIN_PANEL_MODES)[keyof typeof RIGHT_MAIN_PANEL_MODES]; export type LayoutMode = 'workspaces' | 'kanban'; +// Keep in lockstep with MobileTabId in packages/ui/src/components/Navbar.tsx — web-core can't be imported by ui, so these two unions are intentionally duplicated. export type MobileTab = | 'workspaces' | 'chat' | 'changes' | 'logs' | 'preview' - | 'git'; + | 'git' + | 'files'; export type MobileFontScale = 'default' | 'small' | 'smaller'; export const DEFAULT_CREATE_DRAFT_WORKSPACE_BY_DEFAULT = false; @@ -581,8 +584,7 @@ export const useUiPreferencesStore = create()((set, get) => ({ : isWideScreen() ? state.isLeftSidebarVisible : false, - ...(isMobile && - !isCurrentlyActive && { mobileActiveTab: mode as MobileTab }), + ...(isMobile && !isCurrentlyActive && { mobileActiveTab: mode }), }); }, @@ -605,7 +607,7 @@ export const useUiPreferencesStore = create()((set, get) => ({ ? state.isLeftSidebarVisible : false, }), - ...(isMobile && mode !== null && { mobileActiveTab: mode as MobileTab }), + ...(isMobile && mode !== null && { mobileActiveTab: mode }), }); }, From 66d897730beb82d82f3bb01a87829bb6274b8b31 Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:51:02 +0000 Subject: [PATCH 03/15] feat(mobile-nav): render Files pane on mobile Mount the local workspace files container as the seventh mobile pane after Git. Keep it mounted across tab switches and mirror the existing local-only host guard. --- .../src/pages/workspaces/WorkspacesLayout.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx b/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx index 581b6f65ccf..8b0959e07fd 100644 --- a/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx +++ b/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx @@ -12,6 +12,7 @@ import type { CreateModeInitialState } from '@/shared/types/createMode'; import { useWorkspaceContext } from '@/shared/hooks/useWorkspaceContext'; import { usePageTitle } from '@/shared/hooks/usePageTitle'; import { useIsMobile } from '@/shared/hooks/useIsMobile'; +import { useHostId } from '@/shared/providers/HostIdProvider'; import { useMobileActiveTab } from '@/shared/stores/useUiPreferencesStore'; import { cn } from '@/shared/lib/utils'; import { CreateModeProvider } from '@/features/create-mode/model/CreateModeProvider'; @@ -30,6 +31,7 @@ import { type WorkspacesMainContainerHandle, } from './WorkspacesMainContainer'; import { RightSidebar } from './RightSidebar'; +import { WorkspaceFilesContainer } from './WorkspaceFilesContainer'; import { ChangesPanelContainer } from './ChangesPanelContainer'; import { CreateChatBoxContainer } from '@/shared/components/CreateChatBoxContainer'; import { PreviewBrowserContainer } from './PreviewBrowserContainer'; @@ -120,6 +122,7 @@ export function WorkspacesLayout() { const isMobile = useIsMobile(); const [mobileTab] = useMobileActiveTab(); + const hostId = useHostId(); const mainContainerRef = useRef(null); useLayoutEffect(() => { @@ -355,6 +358,18 @@ export function WorkspacesLayout() { /> )} + + {/* Files tab */} +
+ {selectedWorkspace?.id && !hostId && ( + + )} +
From 1c1d4a2e65abfaae02eb8ba07fe8aaf457d9e8cc Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:51:09 +0000 Subject: [PATCH 04/15] fix(remote-nav): exclude Files tab from remote mobile strip Keep the shared Files tab out of remote navigation because workspace filesystem browsing remains local-only. --- packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx b/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx index 9a60bfbc615..cee75031772 100644 --- a/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx +++ b/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx @@ -27,7 +27,9 @@ export function RemoteNavbarContainer({ const remoteMobileTabs = useMemo( () => - MOBILE_TABS.filter((t) => t.id !== "preview" && t.id !== "workspaces"), + MOBILE_TABS.filter( + (t) => t.id !== "preview" && t.id !== "workspaces" && t.id !== "files", + ), [], ); From 3debe850150451326790c49dd7b11fc2b9c8b6ac Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:57:29 +0000 Subject: [PATCH 05/15] refactor(mobile-nav): drop redundant MobileTabId cast in NavbarContainer --- .../components/ui-new/containers/NavbarContainer.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx b/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx index f496755ce84..2241c73f795 100644 --- a/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx +++ b/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx @@ -5,11 +5,7 @@ import { useActions } from '@/shared/hooks/useActions'; import { useSyncErrorContext } from '@/shared/hooks/useSyncErrorContext'; import { useUserOrganizations } from '@/shared/hooks/useUserOrganizations'; import { useOrganizationStore } from '@/shared/stores/useOrganizationStore'; -import { - Navbar, - type NavbarSectionItem, - type MobileTabId, -} from '@vibe/ui/components/Navbar'; +import { Navbar, type NavbarSectionItem } from '@vibe/ui/components/Navbar'; import { Tooltip } from '@vibe/ui/components/Tooltip'; import { AppBarUserPopoverContainer } from './AppBarUserPopoverContainer'; import { AppBarNotificationBellContainer } from '@/pages/workspaces/AppBarNotificationBellContainer'; @@ -327,7 +323,7 @@ export function NavbarContainer({ onOpenCommandBar={handleOpenCommandBar} onOpenSettings={handleOpenSettings} onNavigateBack={handleNavigateBack} - mobileActiveTab={mobileActiveTab as MobileTabId} + mobileActiveTab={mobileActiveTab} onMobileTabChange={(tab) => setMobileActiveTab(tab)} /> ); From 2add7575f905be242d9b57b029b4b7425eb96b8d Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:58:01 +0000 Subject: [PATCH 06/15] refactor(mobile-nav): drop redundant MobileTabId cast in RemoteNavbarContainer --- .../remote-web/src/app/layout/RemoteNavbarContainer.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx b/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx index cee75031772..a60c83c9bc1 100644 --- a/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx +++ b/packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx @@ -1,10 +1,6 @@ import { useCallback, useEffect, useMemo, type ReactNode } from "react"; import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; -import { - MOBILE_TABS, - Navbar, - type MobileTabId, -} from "@vibe/ui/components/Navbar"; +import { MOBILE_TABS, Navbar } from "@vibe/ui/components/Navbar"; import { SettingsDialog } from "@/shared/dialogs/settings/SettingsDialog"; import { CommandBarDialog } from "@/shared/dialogs/command-bar/CommandBarDialog"; import { useMobileActiveTab } from "@/shared/stores/useUiPreferencesStore"; @@ -85,7 +81,7 @@ export function RemoteNavbarContainer({ mobileShowBack={mobileShowBack} onOpenSettings={handleOpenSettings} onOpenCommandBar={handleOpenCommandBar} - mobileActiveTab={mobileActiveTab as MobileTabId} + mobileActiveTab={mobileActiveTab} onMobileTabChange={(tab) => setMobileActiveTab(tab)} mobileTabs={remoteMobileTabs} showMobileTabs={isOnWorkspaceView} From c4805e90eaa640a65417c28da8a1fa3615421a10 Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 20:58:33 +0000 Subject: [PATCH 07/15] refactor(mobile-nav): drop redundant MobileTab cast on default state --- packages/web-core/src/shared/stores/useUiPreferencesStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-core/src/shared/stores/useUiPreferencesStore.ts b/packages/web-core/src/shared/stores/useUiPreferencesStore.ts index e8b02c2c6ce..f059f8478e4 100644 --- a/packages/web-core/src/shared/stores/useUiPreferencesStore.ts +++ b/packages/web-core/src/shared/stores/useUiPreferencesStore.ts @@ -493,7 +493,7 @@ export const useUiPreferencesStore = create()((set, get) => ({ listViewStatusFilter: null, // Mobile tab state - mobileActiveTab: 'chat' as MobileTab, + mobileActiveTab: 'chat', // Mobile font scale mobileFontScale: loadMobileFontScale(), From 7e725064eb0bbb06b5bdc51f82116030e030e4ce Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 21:19:36 +0000 Subject: [PATCH 08/15] fix(mobile-nav): active tab label never rendered (hidden+inline both applied) cn is a plain clsx wrapper, so cn("hidden", isActive && "inline") emitted both conflicting Tailwind utilities together. In this build, .hidden won the stylesheet cascade and kept every tab icon-only regardless of active state. --- packages/ui/src/components/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Navbar.tsx b/packages/ui/src/components/Navbar.tsx index fae1799a891..ed396334737 100644 --- a/packages/ui/src/components/Navbar.tsx +++ b/packages/ui/src/components/Navbar.tsx @@ -281,7 +281,7 @@ export function Navbar({ className="size-icon-sm" weight={isActive ? 'fill' : 'regular'} /> - + {tab.label} From 82e7e47faefb7fcfc0bd68a6bb8d5f0cc9ac1f91 Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 21:21:49 +0000 Subject: [PATCH 09/15] docs(ui): warn that cn() won't dedupe conflicting Tailwind classes --- packages/ui/src/lib/cn.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ui/src/lib/cn.ts b/packages/ui/src/lib/cn.ts index b4b4fa298c7..cc4d0c930c2 100644 --- a/packages/ui/src/lib/cn.ts +++ b/packages/ui/src/lib/cn.ts @@ -1,5 +1,9 @@ import { type ClassValue, clsx } from 'clsx'; +// Intentionally bare clsx (no tailwind-merge) until the Tailwind v4 migration, +// matching web-core's cn. Never combine conflicting utilities in one cn() call; +// pass mutually exclusive values (for example, with a ternary), because cn() +// will not deduplicate or resolve which one wins. export function cn(...inputs: ClassValue[]) { return clsx(inputs); } From 3744c891d169e093874c97bd145df867a6be1e6d Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 21:51:20 +0000 Subject: [PATCH 10/15] fix(mobile-nav): filter Files tab on host-scoped local routes --- .../ui-new/containers/NavbarContainer.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx b/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx index 2241c73f795..ea8dde986ee 100644 --- a/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx +++ b/packages/web-core/src/shared/components/ui-new/containers/NavbarContainer.tsx @@ -5,7 +5,12 @@ import { useActions } from '@/shared/hooks/useActions'; import { useSyncErrorContext } from '@/shared/hooks/useSyncErrorContext'; import { useUserOrganizations } from '@/shared/hooks/useUserOrganizations'; import { useOrganizationStore } from '@/shared/stores/useOrganizationStore'; -import { Navbar, type NavbarSectionItem } from '@vibe/ui/components/Navbar'; +import { + MOBILE_TABS, + Navbar, + type MobileTabId, + type NavbarSectionItem, +} from '@vibe/ui/components/Navbar'; import { Tooltip } from '@vibe/ui/components/Tooltip'; import { AppBarUserPopoverContainer } from './AppBarUserPopoverContainer'; import { AppBarNotificationBellContainer } from '@/pages/workspaces/AppBarNotificationBellContainer'; @@ -33,6 +38,7 @@ import { CommandBarDialog } from '@/shared/dialogs/command-bar/CommandBarDialog' import { SettingsDialog } from '@/shared/dialogs/settings/SettingsDialog'; import { useAppNavigation } from '@/shared/hooks/useAppNavigation'; import { getRemoteAuthDegradedMessage } from '@/shared/lib/auth/remoteAuthDegraded'; +import { useHostId } from '@/shared/providers/HostIdProvider'; /** * Check if a NavbarItem is a divider @@ -113,6 +119,7 @@ export function NavbarContainer({ mobileMode?: boolean; }) { const { t } = useTranslation('common'); + const hostId = useHostId(); const { executeAction } = useActions(); const { workspace: selectedWorkspace, isCreateMode } = useWorkspaceContext(); const syncErrorContext = useSyncErrorContext(); @@ -122,6 +129,10 @@ export function NavbarContainer({ const { isSignedIn } = useAuth(); const appNavigation = useAppNavigation(); const [mobileActiveTab, setMobileActiveTab] = useMobileActiveTab(); + const mobileTabs = useMemo( + () => (hostId ? MOBILE_TABS.filter((t) => t.id !== 'files') : MOBILE_TABS), + [hostId] + ); const { data: orgsData } = useUserOrganizations(); const organizations = useMemo( @@ -324,7 +335,8 @@ export function NavbarContainer({ onOpenSettings={handleOpenSettings} onNavigateBack={handleNavigateBack} mobileActiveTab={mobileActiveTab} - onMobileTabChange={(tab) => setMobileActiveTab(tab)} + onMobileTabChange={(tab: MobileTabId) => setMobileActiveTab(tab)} + mobileTabs={mobileTabs} /> ); } From e8bbfa52331051b0fd255d1b9fd202843bac7d46 Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 21:51:33 +0000 Subject: [PATCH 11/15] docs(mobile-nav): lockstep comment on the mobile pane registry --- packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx b/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx index 8b0959e07fd..084d62b1a3b 100644 --- a/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx +++ b/packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx @@ -232,6 +232,11 @@ export function WorkspacesLayout() { ); // ── Mobile layout ────────────────────────────────────────────────── + // These seven panes must stay in lockstep with MobileTabId + // (packages/ui/src/components/Navbar.tsx) and MobileTab + // (packages/web-core/src/shared/stores/useUiPreferencesStore.ts). Adding a + // tab to those unions without a matching pane here compiles cleanly but + // silently renders a dead tab button. // Uses `hidden` CSS class (NOT conditional rendering) to preserve // WebSocket connections and scroll positions across tab switches. if (isMobile) { From 2baf88c81607c7405ff15238be8e52b689ac055c Mon Sep 17 00:00:00 2001 From: Miguel Rasero Date: Tue, 21 Jul 2026 21:51:42 +0000 Subject: [PATCH 12/15] feat(mobile-nav): aria-current + pronounceable Workspaces aria-label --- packages/ui/src/components/Navbar.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Navbar.tsx b/packages/ui/src/components/Navbar.tsx index ed396334737..e70de556edd 100644 --- a/packages/ui/src/components/Navbar.tsx +++ b/packages/ui/src/components/Navbar.tsx @@ -91,7 +91,9 @@ function NavbarIconButton({ ); } -// Keep in lockstep with MobileTab in packages/web-core/src/shared/stores/useUiPreferencesStore.ts — ui can't depend on web-core, so these two unions are intentionally duplicated. +// Keep in lockstep with MobileTab in +// packages/web-core/src/shared/stores/useUiPreferencesStore.ts — ui can't +// depend on web-core, so these two unions are intentionally duplicated. export type MobileTabId = | 'workspaces' | 'chat' @@ -268,7 +270,12 @@ export function Navbar({