{repositoriesWithWorktrees.map((repo) => {
- const worktrees = repo.worktrees;
+ // In project-specific mode, filter to only show workspaces for the current project
+ let filteredWorktrees = repo.worktrees;
+ if (isProjectSpecific && project) {
+ filteredWorktrees = repo.worktrees.filter(w =>
+ w.name.split("/")[0] === project
+ );
+ if (filteredWorktrees.length === 0) return null;
+ }
+
+ const worktrees = filteredWorktrees;
const isExpanded = expandedRepos.has(repo.id);
const isAvailable = repo.available !== false; // Default to true if not specified
// Get project name from the first worktree
- const projectName =
- worktrees.length > 0
- ? worktrees[0].name.split("/")[0]
- : repo.name;
+ const projectName = isProjectSpecific && project ?
+ project :
+ (worktrees.length > 0 ? worktrees[0].name.split("/")[0] : repo.name);
return (
- {/* Global New Workspace Button */}
-
+ {/* New Workspace/Repository Button */}
+ setNewWorkspaceDialogOpen(true)}
className="flex items-center gap-2 text-muted-foreground hover:text-foreground text-xs"
>
- New workspace
+
+ {isProjectSpecific ? "New workspace" : "New repository"}
+
⌘N
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 54dad69a..d4b09362 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -32,7 +32,7 @@ import {
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
-const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH = "20rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
diff --git a/src/routes/workspace.$project.tsx b/src/routes/workspace.$project.tsx
new file mode 100644
index 00000000..d23031e7
--- /dev/null
+++ b/src/routes/workspace.$project.tsx
@@ -0,0 +1,96 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect, useRef } from "react";
+import { useAppStore } from "@/stores/appStore";
+import { LoadingSpinner } from "@/components/LoadingSpinner";
+import { BackendErrorScreen } from "@/components/BackendErrorScreen";
+import type { Worktree, Repository } from "@/types/workspace";
+
+
+function ProjectWorkspaceRedirect() {
+ const { project } = createFileRoute("/workspace/$project").useParams();
+ const navigate = useNavigate();
+ const hasRedirected = useRef(false);
+
+ // Use stable selectors to avoid infinite loops
+ const initialLoading = useAppStore((state) => state.initialLoading);
+ const loadError = useAppStore((state) => state.loadError);
+ const getRepositoryById = useAppStore((state) => state.getRepositoryById);
+ const getWorktreesList = useAppStore((state) => state.getWorktreesList);
+
+ useEffect(() => {
+ if (hasRedirected.current || initialLoading || loadError) {
+ return; // Prevent multiple redirects or wait for data to load
+ }
+
+ const worktrees = getWorktreesList();
+
+ // Find all worktrees for this project
+ const projectWorktrees = worktrees.filter((worktree: Worktree) => {
+ const nameParts = worktree.name.split("/");
+ if (nameParts[0] !== project) return false;
+
+ // Check if the repository is available
+ const repo = getRepositoryById(worktree.repo_id) as Repository | undefined;
+ return repo && repo.available !== false;
+ });
+
+ if (projectWorktrees.length === 0) {
+ // No workspaces found for this project, redirect to index
+ hasRedirected.current = true;
+ void navigate({ to: "/workspace" });
+ return;
+ }
+
+ // Find the most recent worktree for this project
+ const mostRecentWorktree = projectWorktrees.reduce((latest: Worktree, current: Worktree) => {
+ const latestTime = new Date(latest.last_accessed).getTime();
+ const currentTime = new Date(current.last_accessed).getTime();
+ return currentTime > latestTime ? current : latest;
+ });
+
+ // Redirect to the most recent workspace
+ const nameParts = mostRecentWorktree.name.split("/");
+ if (nameParts.length >= 2) {
+ hasRedirected.current = true;
+ void navigate({
+ to: "/workspace/$project/$workspace",
+ params: {
+ project: nameParts[0],
+ workspace: nameParts[1],
+ },
+ });
+ } else {
+ // Malformed workspace name, redirect to index
+ hasRedirected.current = true;
+ void navigate({ to: "/workspace" });
+ }
+ }, [project, initialLoading, loadError, navigate, getRepositoryById, getWorktreesList]);
+
+ // Show error screen if backend is unavailable
+ if (loadError) {
+ return ;
+ }
+
+ // Show loading while finding workspace
+ return (
+
+
+
+
+ Looking for recent workspaces in {project}
+
+
+
+ );
+}
+
+export const Route = createFileRoute("/workspace/$project")({
+ component: ProjectWorkspaceRedirect,
+});
\ No newline at end of file
diff --git a/src/routes/workspace.index.tsx b/src/routes/workspace.index.tsx
index fa1d3799..d753a14c 100644
--- a/src/routes/workspace.index.tsx
+++ b/src/routes/workspace.index.tsx
@@ -1,79 +1,88 @@
import { createFileRoute } from "@tanstack/react-router";
-import { useEffect, useRef } from "react";
-import { useNavigate } from "@tanstack/react-router";
import { useAppStore } from "@/stores/appStore";
import { WorkspaceWelcome } from "@/components/WorkspaceWelcome";
import { BackendErrorScreen } from "@/components/BackendErrorScreen";
import { LoadingSpinner } from "@/components/LoadingSpinner";
+import { RepositoryListSidebar } from "@/components/RepositoryListSidebar";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+import { ErrorBoundary } from "react-error-boundary";
-function WorkspaceRedirect() {
- const navigate = useNavigate();
- const hasRedirected = useRef(false);
+function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
+ return (
+
+
+
Something went wrong
+
+ {error.message}
+
+
+
+
+ );
+}
+function WorkspaceIndex() {
// Use stable selectors to avoid infinite loops
const initialLoading = useAppStore((state) => state.initialLoading);
const loadError = useAppStore((state) => state.loadError);
const worktreesCount = useAppStore(
(state) => state.getWorktreesList().length,
);
- const getRepositoryById = useAppStore((state) => state.getRepositoryById);
-
- useEffect(() => {
- if (hasRedirected.current || initialLoading || loadError) {
- return; // Prevent multiple redirects or wait for data to load
- }
-
- if (worktreesCount > 0) {
- // Find the first available worktree
- const worktrees = useAppStore.getState().getWorktreesList();
- let firstAvailableWorktree = null;
-
- for (const worktree of worktrees) {
- const repo = getRepositoryById(worktree.repo_id);
- if (repo && repo.available) {
- firstAvailableWorktree = worktree;
- break;
- }
- }
-
- if (firstAvailableWorktree) {
- // Extract project/workspace from the workspace name (e.g., "vibes/tiger")
- const nameParts = firstAvailableWorktree.name.split("/");
- if (nameParts.length >= 2) {
- hasRedirected.current = true;
- void navigate({
- to: "/workspace/$project/$workspace",
- params: {
- project: nameParts[0],
- workspace: nameParts[1],
- },
- });
- return;
- }
- }
- }
-
- // Don't redirect if no available workspaces - show welcome screen instead
- }, [initialLoading, loadError, worktreesCount, navigate, getRepositoryById]);
// Show error screen if backend is unavailable
if (loadError) {
return ;
}
+ // Show loading while data loads
+ if (initialLoading) {
+ return (
+
+
+
+
+ Discovering your workspaces
+
+
+
+ );
+ }
+
// Show welcome screen if no workspaces
- if (!initialLoading && worktreesCount === 0) {
+ if (worktreesCount === 0) {
return ;
}
- // Show loading while checking for workspaces
+ // Show repository list layout
return (
-
-
-
+
+
+
+
+
+
+
+
Select a repository
+
+ Choose a repository from the sidebar to view its workspaces and get started.
+
+
+ You can also create a new repository by clicking the "New repository" button.
+