From 6843ec23f81b24dc179a6b7c3befd86e91eb0ec2 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 09:44:25 +0300 Subject: [PATCH 1/9] add sidebar environment visibility toggles --- FORK.md | 1 + apps/web/src/components/Sidebar.tsx | 151 ++++++++++++++++++++++++++-- apps/web/src/uiStateStore.test.ts | 1 + apps/web/src/uiStateStore.ts | 50 +++++++++ 4 files changed, 193 insertions(+), 10 deletions(-) diff --git a/FORK.md b/FORK.md index dee503d3e8d..465d07ed3dc 100644 --- a/FORK.md +++ b/FORK.md @@ -45,5 +45,6 @@ This repository is a fork of `pingdotgg/t3code`. Keep this file focused on fork ### UX Changes - Desktop context-menu style is configurable. +- Sidebar environments can be hidden or shown dynamically from the project toolbar. - Threads can be archived with middle click. - Terminal selection has a copy action. diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 64d1c00a168..7803db1bdbc 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -150,6 +150,7 @@ import { import { Input } from "./ui/input"; import { Menu, + MenuCheckboxItem, MenuGroup, MenuPopup, MenuRadioGroup, @@ -243,6 +244,13 @@ const PROJECT_GROUPING_MODE_LABELS: Record = const SIDEBAR_ICON_ACTION_BUTTON_CLASS = "inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-[calc(--spacing(1)-1px)] text-muted-foreground/60 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"; +interface SidebarEnvironmentVisibilityOption { + environmentId: string; + label: string; + visible: boolean; + projectCount: number; +} + function SidebarThreadDetailPrewarmer({ threadRef }: { readonly threadRef: ScopedThreadRef }) { useEnvironmentThread(threadRef.environmentId, threadRef.threadId); return null; @@ -2716,6 +2724,71 @@ function ProjectSortMenu({ ); } +function EnvironmentVisibilityMenu({ + environments, + hiddenCount, + onVisibilityChange, +}: { + environments: readonly SidebarEnvironmentVisibilityOption[]; + hiddenCount: number; + onVisibilityChange: (environmentId: string, visible: boolean) => void; +}) { + if (environments.length <= 1) { + return null; + } + + const visibleEnvironmentCount = environments.filter((environment) => environment.visible).length; + + return ( + + + + } + > + + + + {hiddenCount > 0 + ? `${hiddenCount} hidden environment${hiddenCount === 1 ? "" : "s"}` + : "Environments"} + + + + +
Environments
+ {environments.map((environment) => { + const isLastVisibleEnvironment = environment.visible && visibleEnvironmentCount <= 1; + return ( + { + if (isLastVisibleEnvironment && checked !== true) { + return; + } + onVisibilityChange(environment.environmentId, checked === true); + }} + > + + {environment.label} + + {environment.projectCount} project{environment.projectCount === 1 ? "" : "s"} + + + + ); + })} +
+
+
+ ); +} + function SortableProjectItem({ projectId, disabled = false, @@ -2857,7 +2930,10 @@ interface SidebarProjectsContentProps { threadSortOrder: SidebarThreadSortOrder; projectGroupingMode: SidebarProjectGroupingMode; threadPreviewCount: SidebarThreadPreviewCount; + environmentVisibilityOptions: readonly SidebarEnvironmentVisibilityOption[]; + hiddenEnvironmentCount: number; updateSettings: ReturnType; + setSidebarEnvironmentVisible: (environmentId: string, visible: boolean) => void; openAddProject: () => void; isManualProjectSorting: boolean; projectDnDSensors: ReturnType; @@ -2898,7 +2974,10 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( threadSortOrder, projectGroupingMode, threadPreviewCount, + environmentVisibilityOptions, + hiddenEnvironmentCount, updateSettings, + setSidebarEnvironmentVisible, openAddProject, isManualProjectSorting, projectDnDSensors, @@ -2950,6 +3029,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( }, [updateSettings], ); + const handleEnvironmentVisibilityChange = useCallback( + (environmentId: string, visible: boolean) => { + setSidebarEnvironmentVisible(environmentId, visible); + }, + [setSidebarEnvironmentVisible], + ); return ( @@ -3016,6 +3101,11 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( onProjectGroupingModeChange={handleProjectGroupingModeChange} onThreadPreviewCountChange={handleThreadPreviewCountChange} /> + store.projectExpandedById); const projectOrder = useUiStateStore((store) => store.projectOrder); + const sidebarEnvironmentHiddenById = useUiStateStore( + (store) => store.sidebarEnvironmentHiddenById, + ); const reorderProjects = useUiStateStore((store) => store.reorderProjects); + const setSidebarEnvironmentVisible = useUiStateStore( + (store) => store.setSidebarEnvironmentVisible, + ); const navigate = useNavigate(); const pathname = useLocation({ select: (loc) => loc.pathname }); const isOnSettings = pathname.startsWith("/settings"); @@ -3178,6 +3274,24 @@ export default function Sidebar() { ), [environments], ); + const environmentVisibilityOptions = useMemo(() => { + const projectCountsByEnvironmentId = new Map(); + for (const project of projects) { + projectCountsByEnvironmentId.set( + project.environmentId, + (projectCountsByEnvironmentId.get(project.environmentId) ?? 0) + 1, + ); + } + return environments.map((environment) => ({ + environmentId: environment.environmentId, + label: environment.label, + visible: sidebarEnvironmentHiddenById[environment.environmentId] !== true, + projectCount: projectCountsByEnvironmentId.get(environment.environmentId) ?? 0, + })); + }, [environments, projects, sidebarEnvironmentHiddenById]); + const hiddenEnvironmentCount = environmentVisibilityOptions.filter( + (environment) => !environment.visible, + ).length; const orderedProjects = useMemo(() => { return orderItemsByPreferredIds({ items: projects, @@ -3189,30 +3303,44 @@ export default function Sidebar() { ], }); }, [projectOrder, projects]); + const visibleOrderedProjects = useMemo( + () => + orderedProjects.filter( + (project) => sidebarEnvironmentHiddenById[project.environmentId] !== true, + ), + [orderedProjects, sidebarEnvironmentHiddenById], + ); + const visibleSidebarThreads = useMemo( + () => + sidebarThreads.filter( + (thread) => sidebarEnvironmentHiddenById[thread.environmentId] !== true, + ), + [sidebarEnvironmentHiddenById, sidebarThreads], + ); // Build a mapping from physical project key → logical project key for // cross-environment grouping. Projects that share a repositoryIdentity // canonicalKey are treated as one logical project in the sidebar. const physicalToLogicalKey = useMemo(() => { return buildPhysicalToLogicalProjectKeyMap({ - projects: orderedProjects, + projects: visibleOrderedProjects, settings: projectGroupingSettings, }); - }, [orderedProjects, projectGroupingSettings]); + }, [visibleOrderedProjects, projectGroupingSettings]); const projectPhysicalKeyByScopedRef = useMemo( () => new Map( - orderedProjects.map((project) => [ + visibleOrderedProjects.map((project) => [ scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), derivePhysicalProjectKey(project), ]), ), - [orderedProjects], + [visibleOrderedProjects], ); const sidebarProjects = useMemo(() => { return buildSidebarProjectSnapshots({ - projects: orderedProjects, + projects: visibleOrderedProjects, settings: projectGroupingSettings, primaryEnvironmentId, resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null, @@ -3221,7 +3349,7 @@ export default function Sidebar() { }, [ environmentLabelById, desktopLocalEnvironmentIds, - orderedProjects, + visibleOrderedProjects, projectGroupingSettings, primaryEnvironmentId, ]); @@ -3259,7 +3387,7 @@ export default function Sidebar() { // are displayed together. const threadsByProjectKey = useMemo(() => { const next = new Map(); - for (const thread of sidebarThreads) { + for (const thread of visibleSidebarThreads) { const physicalKey = projectPhysicalKeyByScopedRef.get( scopedProjectKey(scopeProjectRef(thread.environmentId, thread.projectId)), @@ -3273,7 +3401,7 @@ export default function Sidebar() { } } return next; - }, [sidebarThreads, physicalToLogicalKey, projectPhysicalKeyByScopedRef]); + }, [visibleSidebarThreads, physicalToLogicalKey, projectPhysicalKeyByScopedRef]); const getCurrentSidebarShortcutContext = useCallback( () => ({ terminalFocus: isTerminalFocused(), @@ -3382,8 +3510,8 @@ export default function Sidebar() { }, []); const visibleThreads = useMemo( - () => sidebarThreads.filter((thread) => thread.archivedAt === null), - [sidebarThreads], + () => visibleSidebarThreads.filter((thread) => thread.archivedAt === null), + [visibleSidebarThreads], ); const sortedProjects = useMemo(() => { const sortableProjects = sidebarProjects.map((project) => ({ @@ -3730,7 +3858,10 @@ export default function Sidebar() { threadSortOrder={sidebarThreadSortOrder} projectGroupingMode={sidebarProjectGroupingMode} threadPreviewCount={sidebarThreadPreviewCount} + environmentVisibilityOptions={environmentVisibilityOptions} + hiddenEnvironmentCount={hiddenEnvironmentCount} updateSettings={updateSettings} + setSidebarEnvironmentVisible={setSidebarEnvironmentVisible} openAddProject={openAddProjectCommandPalette} isManualProjectSorting={isManualProjectSorting} projectDnDSensors={projectDnDSensors} diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts index 0fbbd79ec27..ac6b88e594c 100644 --- a/apps/web/src/uiStateStore.test.ts +++ b/apps/web/src/uiStateStore.test.ts @@ -21,6 +21,7 @@ function makeUiState(overrides: Partial = {}): UiState { return { projectExpandedById: {}, projectOrder: [], + sidebarEnvironmentHiddenById: {}, threadLastVisitedAtById: {}, threadChangedFilesExpandedById: {}, defaultAdvertisedEndpointKey: null, diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts index 4a97f0542b4..a985437ea9b 100644 --- a/apps/web/src/uiStateStore.ts +++ b/apps/web/src/uiStateStore.ts @@ -19,6 +19,7 @@ const LEGACY_PERSISTED_STATE_KEYS = [ export interface PersistedUiState { projectExpandedById?: Record; projectOrder?: string[]; + sidebarEnvironmentHiddenById?: Record; threadLastVisitedAtById?: Record; collapsedProjectCwds?: string[]; expandedProjectCwds?: string[]; @@ -30,6 +31,7 @@ export interface PersistedUiState { export interface UiProjectState { projectExpandedById: Record; projectOrder: string[]; + sidebarEnvironmentHiddenById: Record; } export interface UiThreadState { @@ -46,6 +48,7 @@ export interface UiState extends UiProjectState, UiThreadState, UiEndpointState const initialState: UiState = { projectExpandedById: {}, projectOrder: [], + sidebarEnvironmentHiddenById: {}, threadLastVisitedAtById: {}, threadChangedFilesExpandedById: {}, defaultAdvertisedEndpointKey: null, @@ -81,6 +84,17 @@ function sanitizeBooleanRecord(value: unknown): Record { ); } +function sanitizeTrueRecord(value: unknown): Record { + if (!value || typeof value !== "object") { + return {}; + } + return Object.fromEntries( + Object.entries(value).filter( + (entry): entry is [string, true] => entry[0].length > 0 && entry[1] === true, + ), + ); +} + function sanitizeTimestampRecord(value: unknown): Record { if (!value || typeof value !== "object") { return {}; @@ -123,6 +137,7 @@ export function parsePersistedState(parsed: PersistedUiState): UiState { return { projectExpandedById, projectOrder, + sidebarEnvironmentHiddenById: sanitizeTrueRecord(parsed.sidebarEnvironmentHiddenById), threadLastVisitedAtById: sanitizeTimestampRecord(parsed.threadLastVisitedAtById), threadChangedFilesExpandedById: sanitizePersistedThreadChangedFilesExpanded( parsed.threadChangedFilesExpandedById, @@ -208,6 +223,7 @@ export function persistState(state: UiState): void { JSON.stringify({ projectExpandedById, projectOrder: state.projectOrder, + sidebarEnvironmentHiddenById: state.sidebarEnvironmentHiddenById, threadLastVisitedAtById: state.threadLastVisitedAtById, defaultAdvertisedEndpointKey: state.defaultAdvertisedEndpointKey, threadChangedFilesExpandedById, @@ -411,12 +427,44 @@ export function reorderProjects( }; } +export function setSidebarEnvironmentVisible( + state: UiState, + environmentId: string, + visible: boolean, +): UiState { + if (environmentId.length === 0) { + return state; + } + if (visible) { + if (state.sidebarEnvironmentHiddenById[environmentId] !== true) { + return state; + } + const sidebarEnvironmentHiddenById = { ...state.sidebarEnvironmentHiddenById }; + delete sidebarEnvironmentHiddenById[environmentId]; + return { + ...state, + sidebarEnvironmentHiddenById, + }; + } + if (state.sidebarEnvironmentHiddenById[environmentId] === true) { + return state; + } + return { + ...state, + sidebarEnvironmentHiddenById: { + ...state.sidebarEnvironmentHiddenById, + [environmentId]: true, + }, + }; +} + interface UiStateStore extends UiState { markThreadVisited: (threadId: string, visitedAt: string) => void; markThreadUnread: (threadId: string, latestTurnCompletedAt: string | null | undefined) => void; setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void; setDefaultAdvertisedEndpointKey: (key: string | null) => void; setProjectExpanded: (projectIds: string | readonly string[], expanded: boolean) => void; + setSidebarEnvironmentVisible: (environmentId: string, visible: boolean) => void; reorderProjects: ( currentProjectOrder: readonly string[], draggedProjectIds: readonly string[], @@ -436,6 +484,8 @@ export const useUiStateStore = create((set) => ({ set((state) => setDefaultAdvertisedEndpointKey(state, key)), setProjectExpanded: (projectIds, expanded) => set((state) => setProjectExpanded(state, projectIds, expanded)), + setSidebarEnvironmentVisible: (environmentId, visible) => + set((state) => setSidebarEnvironmentVisible(state, environmentId, visible)), reorderProjects: (currentProjectOrder, draggedProjectIds, targetProjectIds) => set((state) => reorderProjects(state, currentProjectOrder, draggedProjectIds, targetProjectIds), From 078b981fe434aeac67adb02a1bd06256cca09f12 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 09:54:27 +0300 Subject: [PATCH 2/9] fix sidebar environment state assertions --- apps/web/src/uiStateStore.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts index ac6b88e594c..99d4b82645f 100644 --- a/apps/web/src/uiStateStore.test.ts +++ b/apps/web/src/uiStateStore.test.ts @@ -169,6 +169,7 @@ describe("parsePersistedState", () => { logical: false, }, projectOrder: ["physical-b", "physical-a"], + sidebarEnvironmentHiddenById: {}, threadLastVisitedAtById: { "environment:thread-1": "2026-02-25T12:35:00.000Z", }, @@ -253,6 +254,7 @@ describe("uiStateStore persistence", () => { logical: false, }, projectOrder: ["physical-b", "physical-a"], + sidebarEnvironmentHiddenById: {}, threadLastVisitedAtById: { "environment:thread-1": "2026-02-25T12:35:00.000Z", }, @@ -275,6 +277,7 @@ describe("uiStateStore persistence", () => { logical: false, }, projectOrder: ["physical-b", "physical-a"], + sidebarEnvironmentHiddenById: {}, threadLastVisitedAtById: { "environment:thread-1": "2026-02-25T12:35:00.000Z", }, From 097ff171dfaf4675674b6bc2806407d94296af75 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:02:36 +0300 Subject: [PATCH 3/9] fix sidebar environment edge cases --- apps/web/src/components/Sidebar.logic.test.ts | 14 ++++++ apps/web/src/components/Sidebar.logic.ts | 2 +- apps/web/src/components/Sidebar.tsx | 43 +++++++++++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 40af65fa06e..5476f5e3e41 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -482,6 +482,20 @@ describe("resolveAdjacentThreadId", () => { direction: "previous", }), ).toBe(threads[2]); + expect( + resolveAdjacentThreadId({ + threadIds: threads, + currentThreadId: ThreadId.make("hidden-thread"), + direction: "next", + }), + ).toBe(threads[0]); + expect( + resolveAdjacentThreadId({ + threadIds: threads, + currentThreadId: ThreadId.make("hidden-thread"), + direction: "previous", + }), + ).toBe(threads[2]); expect( resolveAdjacentThreadId({ threadIds: threads, diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 4e7614ed551..cd9d4c807ad 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -307,7 +307,7 @@ export function resolveAdjacentThreadId(input: { const currentIndex = threadIds.indexOf(currentThreadId); if (currentIndex === -1) { - return null; + return direction === "previous" ? (threadIds.at(-1) ?? null) : (threadIds[0] ?? null); } if (direction === "previous") { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 7803db1bdbc..7f3b1ac80d5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3282,10 +3282,13 @@ export default function Sidebar() { (projectCountsByEnvironmentId.get(project.environmentId) ?? 0) + 1, ); } + const canHideSidebarEnvironments = environments.length > 1; return environments.map((environment) => ({ environmentId: environment.environmentId, label: environment.label, - visible: sidebarEnvironmentHiddenById[environment.environmentId] !== true, + visible: + !canHideSidebarEnvironments || + sidebarEnvironmentHiddenById[environment.environmentId] !== true, projectCount: projectCountsByEnvironmentId.get(environment.environmentId) ?? 0, })); }, [environments, projects, sidebarEnvironmentHiddenById]); @@ -3306,16 +3309,18 @@ export default function Sidebar() { const visibleOrderedProjects = useMemo( () => orderedProjects.filter( - (project) => sidebarEnvironmentHiddenById[project.environmentId] !== true, + (project) => + environments.length <= 1 || sidebarEnvironmentHiddenById[project.environmentId] !== true, ), - [orderedProjects, sidebarEnvironmentHiddenById], + [environments.length, orderedProjects, sidebarEnvironmentHiddenById], ); const visibleSidebarThreads = useMemo( () => sidebarThreads.filter( - (thread) => sidebarEnvironmentHiddenById[thread.environmentId] !== true, + (thread) => + environments.length <= 1 || sidebarEnvironmentHiddenById[thread.environmentId] !== true, ), - [sidebarEnvironmentHiddenById, sidebarThreads], + [environments.length, sidebarEnvironmentHiddenById, sidebarThreads], ); // Build a mapping from physical project key → logical project key for @@ -3353,6 +3358,28 @@ export default function Sidebar() { projectGroupingSettings, primaryEnvironmentId, ]); + const sidebarProjectsForProjectOrder = useMemo(() => { + return buildSidebarProjectSnapshots({ + projects: orderedProjects, + settings: projectGroupingSettings, + primaryEnvironmentId, + resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null, + isDesktopLocalEnvironment: (environmentId) => desktopLocalEnvironmentIds.has(environmentId), + }); + }, [ + environmentLabelById, + desktopLocalEnvironmentIds, + orderedProjects, + projectGroupingSettings, + primaryEnvironmentId, + ]); + const sidebarProjectForProjectOrderByKey = useMemo( + () => + new Map( + sidebarProjectsForProjectOrder.map((project) => [project.projectKey, project] as const), + ), + [sidebarProjectsForProjectOrder], + ); const sidebarProjectByKey = useMemo( () => new Map(sidebarProjects.map((project) => [project.projectKey, project] as const)), @@ -3464,8 +3491,8 @@ export default function Sidebar() { dragInProgressRef.current = false; const { active, over } = event; if (!over || active.id === over.id) return; - const activeProject = sidebarProjects.find((project) => project.projectKey === active.id); - const overProject = sidebarProjects.find((project) => project.projectKey === over.id); + const activeProject = sidebarProjectForProjectOrderByKey.get(String(active.id)); + const overProject = sidebarProjectForProjectOrderByKey.get(String(over.id)); if (!activeProject || !overProject) return; const activeMemberKeys = activeProject.memberProjects.map( (member) => member.physicalProjectKey, @@ -3473,7 +3500,7 @@ export default function Sidebar() { const overMemberKeys = overProject.memberProjects.map((member) => member.physicalProjectKey); reorderProjects(orderedProjects.map(getProjectOrderKey), activeMemberKeys, overMemberKeys); }, - [orderedProjects, sidebarProjectSortOrder, reorderProjects, sidebarProjects], + [orderedProjects, sidebarProjectForProjectOrderByKey, sidebarProjectSortOrder, reorderProjects], ); const handleProjectDragStart = useCallback( From aaaf7f29d5f45e10c4ab8085424cefa625a2cb58 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:05:41 +0300 Subject: [PATCH 4/9] fix sidebar hidden environment empty state --- apps/web/src/components/Sidebar.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 7f3b1ac80d5..8be44d129c6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2958,7 +2958,6 @@ interface SidebarProjectsContentProps { suppressProjectClickAfterDragRef: React.RefObject; suppressProjectClickForContextMenuRef: React.RefObject; attachProjectListAutoAnimateRef: (node: HTMLElement | null) => void; - projectsLength: number; } const SidebarProjectsContent = memo(function SidebarProjectsContent( @@ -3002,7 +3001,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( suppressProjectClickAfterDragRef, suppressProjectClickForContextMenuRef, attachProjectListAutoAnimateRef, - projectsLength, } = props; const handleProjectSortOrderChange = useCallback( @@ -3198,7 +3196,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( )} - {projectsLength === 0 && ( + {sortedProjects.length === 0 && (
No projects yet
@@ -3913,7 +3911,6 @@ export default function Sidebar() { suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef} suppressProjectClickForContextMenuRef={suppressProjectClickForContextMenuRef} attachProjectListAutoAnimateRef={attachProjectListAutoAnimateRef} - projectsLength={projects.length} /> From 0fae4a7513a82453d1c35b8fbb9de3e3cf6bc819 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:12:32 +0300 Subject: [PATCH 5/9] fix sidebar empty project state --- apps/web/src/components/Sidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 8be44d129c6..7f3b1ac80d5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2958,6 +2958,7 @@ interface SidebarProjectsContentProps { suppressProjectClickAfterDragRef: React.RefObject; suppressProjectClickForContextMenuRef: React.RefObject; attachProjectListAutoAnimateRef: (node: HTMLElement | null) => void; + projectsLength: number; } const SidebarProjectsContent = memo(function SidebarProjectsContent( @@ -3001,6 +3002,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( suppressProjectClickAfterDragRef, suppressProjectClickForContextMenuRef, attachProjectListAutoAnimateRef, + projectsLength, } = props; const handleProjectSortOrderChange = useCallback( @@ -3196,7 +3198,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( )} - {sortedProjects.length === 0 && ( + {projectsLength === 0 && (
No projects yet
@@ -3911,6 +3913,7 @@ export default function Sidebar() { suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef} suppressProjectClickForContextMenuRef={suppressProjectClickForContextMenuRef} attachProjectListAutoAnimateRef={attachProjectListAutoAnimateRef} + projectsLength={projects.length} /> From 1463e9b36c7f357ead76192fd603c959e0e48d0e Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:18:20 +0300 Subject: [PATCH 6/9] fix sidebar thread shortcut traversal --- apps/web/src/components/Sidebar.logic.test.ts | 4 ++-- apps/web/src/components/Sidebar.logic.ts | 2 +- apps/web/src/components/Sidebar.tsx | 13 ++++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 5476f5e3e41..2b62dae8ae4 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -488,14 +488,14 @@ describe("resolveAdjacentThreadId", () => { currentThreadId: ThreadId.make("hidden-thread"), direction: "next", }), - ).toBe(threads[0]); + ).toBeNull(); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: ThreadId.make("hidden-thread"), direction: "previous", }), - ).toBe(threads[2]); + ).toBeNull(); expect( resolveAdjacentThreadId({ threadIds: threads, diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index cd9d4c807ad..4e7614ed551 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -307,7 +307,7 @@ export function resolveAdjacentThreadId(input: { const currentIndex = threadIds.indexOf(currentThreadId); if (currentIndex === -1) { - return direction === "previous" ? (threadIds.at(-1) ?? null) : (threadIds[0] ?? null); + return null; } if (direction === "previous") { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 7f3b1ac80d5..b5555ea50fb 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3661,7 +3661,18 @@ export default function Sidebar() { const visibleThreadJumpLabelByKey = showThreadJumpHints ? threadJumpLabelByKey : EMPTY_THREAD_JUMP_LABELS; - const orderedSidebarThreadKeys = visibleSidebarThreadKeys; + const orderedSidebarThreadKeys = useMemo( + () => + sortedProjects.flatMap((project) => + sortThreads( + (threadsByProjectKey.get(project.projectKey) ?? []).filter( + (thread) => thread.archivedAt === null, + ), + sidebarThreadSortOrder, + ).map((thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id))), + ), + [sidebarThreadSortOrder, sortedProjects, threadsByProjectKey], + ); const prewarmedSidebarThreadKeys = useMemo( () => getSidebarThreadIdsToPrewarm(visibleSidebarThreadKeys), [visibleSidebarThreadKeys], From 0c769e1ed4e29add8554b86887e5d1d39dd1ac92 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:24:16 +0300 Subject: [PATCH 7/9] align thread traversal with visible rows --- apps/web/src/components/Sidebar.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index b5555ea50fb..7f3b1ac80d5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3661,18 +3661,7 @@ export default function Sidebar() { const visibleThreadJumpLabelByKey = showThreadJumpHints ? threadJumpLabelByKey : EMPTY_THREAD_JUMP_LABELS; - const orderedSidebarThreadKeys = useMemo( - () => - sortedProjects.flatMap((project) => - sortThreads( - (threadsByProjectKey.get(project.projectKey) ?? []).filter( - (thread) => thread.archivedAt === null, - ), - sidebarThreadSortOrder, - ).map((thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id))), - ), - [sidebarThreadSortOrder, sortedProjects, threadsByProjectKey], - ); + const orderedSidebarThreadKeys = visibleSidebarThreadKeys; const prewarmedSidebarThreadKeys = useMemo( () => getSidebarThreadIdsToPrewarm(visibleSidebarThreadKeys), [visibleSidebarThreadKeys], From b35519137d97b4b840756f1f8dadb34db885bb58 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:35:52 +0300 Subject: [PATCH 8/9] pin environment menu position --- apps/web/src/components/Sidebar.tsx | 7 ++++++- apps/web/src/components/ui/menu.tsx | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 7f3b1ac80d5..898892498d9 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2755,7 +2755,12 @@ function EnvironmentVisibilityMenu({ : "Environments"}
- +
Environments
{environments.map((environment) => { diff --git a/apps/web/src/components/ui/menu.tsx b/apps/web/src/components/ui/menu.tsx index 07e27d23f46..ee6a76a3da9 100644 --- a/apps/web/src/components/ui/menu.tsx +++ b/apps/web/src/components/ui/menu.tsx @@ -28,6 +28,7 @@ function MenuPopup({ alignOffset, side = "bottom", anchor, + collisionAvoidance, ...props }: MenuPrimitive.Popup.Props & { align?: MenuPrimitive.Positioner.Props["align"]; @@ -35,6 +36,7 @@ function MenuPopup({ alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; side?: MenuPrimitive.Positioner.Props["side"]; anchor?: MenuPrimitive.Positioner.Props["anchor"]; + collisionAvoidance?: MenuPrimitive.Positioner.Props["collisionAvoidance"]; }) { return ( @@ -43,6 +45,7 @@ function MenuPopup({ alignOffset={alignOffset} anchor={anchor} className="z-50" + collisionAvoidance={collisionAvoidance} data-slot="menu-positioner" side={side} sideOffset={sideOffset} From 3561e4fc07f65bde578e9011f8214eb357248120 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 1 Jul 2026 10:44:49 +0300 Subject: [PATCH 9/9] open environment menu to the right --- apps/web/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 898892498d9..fe3260e2cb5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2756,7 +2756,7 @@ function EnvironmentVisibilityMenu({