Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -287,28 +287,51 @@ 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<string, ProjectConfig["workspaces"]>();
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;

// 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);
Expand All @@ -321,7 +344,7 @@ function AppInner() {
workspaceId: metadata.id,
});
},
[selectedWorkspace, projects, workspaceMetadata, setSelectedWorkspace]
[selectedWorkspace, sortedWorkspacesByProject, workspaceMetadata, setSelectedWorkspace]
);

// Register command sources with registry
Expand Down Expand Up @@ -554,7 +577,7 @@ function AppInner() {
onToggleCollapsed={() => setSidebarCollapsed((prev) => !prev)}
onGetSecrets={handleGetSecrets}
onUpdateSecrets={handleUpdateSecrets}
workspaceRecency={workspaceRecency}
sortedWorkspacesByProject={sortedWorkspacesByProject}
/>
<MainContent>
<ContentArea>
Expand Down
2 changes: 1 addition & 1 deletion src/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface LeftSidebarProps {
onToggleCollapsed: () => void;
onGetSecrets: (projectPath: string) => Promise<Secret[]>;
onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise<void>;
workspaceRecency: Record<string, number>;
sortedWorkspacesByProject: Map<string, ProjectConfig["workspaces"]>;
}

export function LeftSidebar(props: LeftSidebarProps) {
Expand Down
28 changes: 3 additions & 25 deletions src/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -585,7 +585,7 @@ interface ProjectSidebarProps {
onToggleCollapsed: () => void;
onGetSecrets: (projectPath: string) => Promise<Secret[]>;
onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise<void>;
workspaceRecency: Record<string, number>;
sortedWorkspacesByProject: Map<string, Workspace[]>;
}

const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
Expand All @@ -605,33 +605,11 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
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<string, Workspace[]>();
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<string[]>(
"expandedProjects",
Expand Down