From 1725a5e9484c4d186738b5b6f5612ee3630d2cc4 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 12 Oct 2025 19:44:54 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20Fix=20Ctrl+J/K=20workspace?= =?UTF-8?q?=20navigation=20to=20use=20sorted=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigation now follows visual order displayed in sidebar. **Problem:** - PR #205 added recency-based sorting to workspace display - Ctrl+J/K navigation still used unsorted config order - Caused confusion: pressing next didn't select next visible workspace **Solution:** - Move sorting logic from ProjectSidebar to App.tsx - Share sorted list between navigation and display - Both now use same recency-sorted order **Changes:** - Added sortedWorkspacesByProject memo in App.tsx - Updated handleNavigateWorkspace to use sorted list - Pass sorted list through LeftSidebar to ProjectSidebar - Remove duplicate sorting logic from ProjectSidebar --- src/App.tsx | 42 ++++++++++++++++++++++++------- src/components/LeftSidebar.tsx | 1 + src/components/ProjectSidebar.tsx | 24 ++---------------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 39205ce1b..1932de77c 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 @@ -555,6 +578,7 @@ function AppInner() { onGetSecrets={handleGetSecrets} onUpdateSecrets={handleUpdateSecrets} workspaceRecency={workspaceRecency} + sortedWorkspacesByProject={sortedWorkspacesByProject} /> diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index 71f6d6dbb..687c8b4e0 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -44,6 +44,7 @@ interface LeftSidebarProps { 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..4e2ff3e3a 100644 --- a/src/components/ProjectSidebar.tsx +++ b/src/components/ProjectSidebar.tsx @@ -586,6 +586,7 @@ interface ProjectSidebarProps { onGetSecrets: (projectPath: string) => Promise; onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise; workspaceRecency: Record; + sortedWorkspacesByProject: Map; } const ProjectSidebar: React.FC = ({ @@ -606,32 +607,11 @@ const ProjectSidebar: React.FC = ({ 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", From 2aac4373c61c3eeabbaa47bfc9f18ca47119b4d2 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 12 Oct 2025 19:48:08 -0500 Subject: [PATCH 2/2] Remove unused workspaceRecency prop Now that sorting is done in App.tsx, ProjectSidebar no longer needs the workspaceRecency prop - it just receives the pre-sorted list. --- src/App.tsx | 1 - src/components/LeftSidebar.tsx | 1 - src/components/ProjectSidebar.tsx | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1932de77c..fe5c37677 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -577,7 +577,6 @@ 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 687c8b4e0..e24eee1ff 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -43,7 +43,6 @@ interface LeftSidebarProps { onToggleCollapsed: () => void; onGetSecrets: (projectPath: string) => Promise; onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise; - workspaceRecency: Record; sortedWorkspacesByProject: Map; } diff --git a/src/components/ProjectSidebar.tsx b/src/components/ProjectSidebar.tsx index 4e2ff3e3a..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,6 @@ interface ProjectSidebarProps { onToggleCollapsed: () => void; onGetSecrets: (projectPath: string) => Promise; onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise; - workspaceRecency: Record; sortedWorkspacesByProject: Map; } @@ -606,7 +605,6 @@ const ProjectSidebar: React.FC = ({ onToggleCollapsed, onGetSecrets, onUpdateSecrets, - workspaceRecency, sortedWorkspacesByProject, }) => { // Subscribe to git status updates (causes this component to re-render every 10s)