diff --git a/src/App.tsx b/src/App.tsx index 39205ce1b..fe5c37677 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import styled from "@emotion/styled"; import { Global, css } from "@emotion/react"; import { GlobalColors } from "./styles/colors"; @@ -287,15 +287,38 @@ function AppInner() { [] ); + // Sort workspaces by recency (most recent first) + // This ensures navigation follows the visual order displayed in the sidebar + const sortedWorkspacesByProject = useMemo(() => { + const result = new Map(); + for (const [projectPath, config] of projects) { + result.set( + projectPath, + config.workspaces.slice().sort((a, b) => { + const aMeta = workspaceMetadata.get(a.path); + const bMeta = workspaceMetadata.get(b.path); + if (!aMeta || !bMeta) return 0; + + // Get timestamp of most recent user message (0 if never used) + const aTimestamp = workspaceRecency[aMeta.id] ?? 0; + const bTimestamp = workspaceRecency[bMeta.id] ?? 0; + return bTimestamp - aTimestamp; + }) + ); + } + return result; + }, [projects, workspaceMetadata, workspaceRecency]); + const handleNavigateWorkspace = useCallback( (direction: "next" | "prev") => { if (!selectedWorkspace) return; - const projectConfig = projects.get(selectedWorkspace.projectPath); - if (!projectConfig || projectConfig.workspaces.length <= 1) return; + // Use sorted workspaces to match visual order in sidebar + const sortedWorkspaces = sortedWorkspacesByProject.get(selectedWorkspace.projectPath); + if (!sortedWorkspaces || sortedWorkspaces.length <= 1) return; - // Find current workspace index - const currentIndex = projectConfig.workspaces.findIndex( + // Find current workspace index in sorted list + const currentIndex = sortedWorkspaces.findIndex( (ws) => ws.path === selectedWorkspace.workspacePath ); if (currentIndex === -1) return; @@ -303,12 +326,12 @@ function AppInner() { // Calculate next/prev index with wrapping let targetIndex: number; if (direction === "next") { - targetIndex = (currentIndex + 1) % projectConfig.workspaces.length; + targetIndex = (currentIndex + 1) % sortedWorkspaces.length; } else { - targetIndex = currentIndex === 0 ? projectConfig.workspaces.length - 1 : currentIndex - 1; + targetIndex = currentIndex === 0 ? sortedWorkspaces.length - 1 : currentIndex - 1; } - const targetWorkspace = projectConfig.workspaces[targetIndex]; + const targetWorkspace = sortedWorkspaces[targetIndex]; if (!targetWorkspace) return; const metadata = workspaceMetadata.get(targetWorkspace.path); @@ -321,7 +344,7 @@ function AppInner() { workspaceId: metadata.id, }); }, - [selectedWorkspace, projects, workspaceMetadata, setSelectedWorkspace] + [selectedWorkspace, sortedWorkspacesByProject, workspaceMetadata, setSelectedWorkspace] ); // Register command sources with registry @@ -554,7 +577,7 @@ function AppInner() { onToggleCollapsed={() => setSidebarCollapsed((prev) => !prev)} onGetSecrets={handleGetSecrets} onUpdateSecrets={handleUpdateSecrets} - workspaceRecency={workspaceRecency} + sortedWorkspacesByProject={sortedWorkspacesByProject} /> diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index 71f6d6dbb..e24eee1ff 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -43,7 +43,7 @@ interface LeftSidebarProps { onToggleCollapsed: () => void; onGetSecrets: (projectPath: string) => Promise; onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise; - workspaceRecency: Record; + sortedWorkspacesByProject: Map; } export function LeftSidebar(props: LeftSidebarProps) { diff --git a/src/components/ProjectSidebar.tsx b/src/components/ProjectSidebar.tsx index 58041968f..446dd87ef 100644 --- a/src/components/ProjectSidebar.tsx +++ b/src/components/ProjectSidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; import styled from "@emotion/styled"; import { css } from "@emotion/react"; @@ -585,7 +585,7 @@ interface ProjectSidebarProps { onToggleCollapsed: () => void; onGetSecrets: (projectPath: string) => Promise; onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise; - workspaceRecency: Record; + sortedWorkspacesByProject: Map; } const ProjectSidebar: React.FC = ({ @@ -605,33 +605,11 @@ const ProjectSidebar: React.FC = ({ onToggleCollapsed, onGetSecrets, onUpdateSecrets, - workspaceRecency, + sortedWorkspacesByProject, }) => { // Subscribe to git status updates (causes this component to re-render every 10s) const gitStatus = useGitStatus(); - // Sort workspaces by last user message (most recent first) - // workspaceRecency only updates when timestamps actually change (stable reference optimization) - const sortedWorkspacesByProject = useMemo(() => { - const result = new Map(); - for (const [projectPath, config] of projects) { - result.set( - projectPath, - config.workspaces.slice().sort((a, b) => { - const aMeta = workspaceMetadata.get(a.path); - const bMeta = workspaceMetadata.get(b.path); - if (!aMeta || !bMeta) return 0; - - // Get timestamp of most recent user message (0 if never used) - const aTimestamp = workspaceRecency[aMeta.id] ?? 0; - const bTimestamp = workspaceRecency[bMeta.id] ?? 0; - return bTimestamp - aTimestamp; - }) - ); - } - return result; - }, [projects, workspaceMetadata, workspaceRecency]); - // Store as array in localStorage, convert to Set for usage const [expandedProjectsArray, setExpandedProjectsArray] = usePersistedState( "expandedProjects",