Skip to content
Merged
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
186 changes: 125 additions & 61 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {
ArrowLeftIcon,
ChevronRightIcon,
FolderIcon,
GitPullRequestIcon,
PlusIcon,
RocketIcon,
SettingsIcon,
SquarePenIcon,
TerminalIcon,
} from "lucide-react";
Expand All @@ -17,7 +20,7 @@ import {
type ResolvedKeybindingsConfig,
} from "@t3tools/contracts";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
import { useAppSettings } from "../appSettings";
import { isElectron } from "../env";
import { APP_STAGE_LABEL } from "../branding";
Expand Down Expand Up @@ -278,6 +281,7 @@ export default function Sidebar() {
(store) => store.clearProjectDraftThreadById,
);
const navigate = useNavigate();
const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" });
const { settings: appSettings } = useAppSettings();
const routeThreadId = useParams({
strict: false,
Expand All @@ -293,6 +297,8 @@ export default function Sidebar() {
const [newCwd, setNewCwd] = useState("");
const [isPickingFolder, setIsPickingFolder] = useState(false);
const [isAddingProject, setIsAddingProject] = useState(false);
const [addProjectError, setAddProjectError] = useState<string | null>(null);
const addProjectInputRef = useRef<HTMLInputElement | null>(null);
const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null);
const [renamingTitle, setRenamingTitle] = useState("");
const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState<
Expand Down Expand Up @@ -486,6 +492,7 @@ export default function Sidebar() {
const finishAddingProject = () => {
setIsAddingProject(false);
setNewCwd("");
setAddProjectError(null);
setAddingProject(false);
};

Expand All @@ -512,12 +519,9 @@ export default function Sidebar() {
await handleNewThread(projectId).catch(() => undefined);
} catch (error) {
setIsAddingProject(false);
toastManager.add({
type: "error",
title: "Unable to add project",
description:
error instanceof Error ? error.message : "An error occurred while adding the project.",
});
setAddProjectError(
error instanceof Error ? error.message : "An error occurred while adding the project.",
);
return;
}
finishAddingProject();
Expand All @@ -541,6 +545,8 @@ export default function Sidebar() {
}
if (pickedPath) {
await addProjectFromPath(pickedPath);
} else {
addProjectInputRef.current?.focus();
}
setIsPickingFolder(false);
};
Expand Down Expand Up @@ -1025,6 +1031,95 @@ export default function Sidebar() {

<SidebarContent className="gap-0">
<SidebarGroup className="px-2 py-2">
<div className="mb-1 flex items-center justify-between px-2">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
Projects
</span>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label="Add project"
className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
onClick={() => {
setAddingProject((prev) => !prev);
setAddProjectError(null);
}}
/>
}
>
<PlusIcon className="size-3.5" />
</TooltipTrigger>
<TooltipPopup side="right">Add project</TooltipPopup>
</Tooltip>
</div>

{addingProject && (
<div className="mb-2 px-1">
{isElectron && (
<button
type="button"
className="mb-1.5 flex w-full items-center justify-center gap-2 rounded-md border border-border bg-secondary py-1.5 text-xs text-foreground/80 transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => void handlePickFolder()}
disabled={isPickingFolder || isAddingProject}
>
<FolderIcon className="size-3.5" />
{isPickingFolder ? "Picking folder..." : "Browse for folder"}
</button>
)}
<div className="flex gap-1.5">
<input
ref={addProjectInputRef}
className={`min-w-0 flex-1 rounded-md border bg-secondary px-2 py-1 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:outline-none ${
addProjectError
? "border-red-500/70 focus:border-red-500"
: "border-border focus:border-ring"
}`}
placeholder="/path/to/project"
value={newCwd}
onChange={(event) => {
setNewCwd(event.target.value);
setAddProjectError(null);
}}
onKeyDown={(event) => {
if (event.key === "Enter") handleAddProject();
if (event.key === "Escape") {
setAddingProject(false);
setAddProjectError(null);
}
}}
autoFocus
/>
<button
type="button"
className="shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90 disabled:opacity-60"
onClick={handleAddProject}
disabled={isAddingProject}
>
{isAddingProject ? "Adding..." : "Add"}
</button>
</div>
{addProjectError && (
<p className="mt-1 px-0.5 text-[11px] leading-tight text-red-400">
{addProjectError}
</p>
)}
<div className="mt-1.5 px-0.5">
<button
type="button"
className="text-[11px] text-muted-foreground/50 transition-colors hover:text-muted-foreground"
onClick={() => {
setAddingProject(false);
setAddProjectError(null);
}}
>
Cancel
</button>
</div>
</div>
)}

<SidebarMenu>
{projects.map((project) => {
const projectThreads = threads
Expand Down Expand Up @@ -1288,68 +1383,37 @@ export default function Sidebar() {

{projects.length === 0 && !addingProject && (
<div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">
No projects yet.
<br />
Add one to get started.
No projects yet
</div>
)}
</SidebarGroup>
</SidebarContent>

<SidebarSeparator />
<SidebarFooter className="gap-0 p-3">
{addingProject ? (
<>
<p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
Add project
</p>
<input
className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none"
placeholder="/path/to/project"
value={newCwd}
onChange={(event) => setNewCwd(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") handleAddProject();
if (event.key === "Escape") setAddingProject(false);
}}
/>
{isElectron && (
<button
type="button"
className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => void handlePickFolder()}
disabled={isPickingFolder || isAddingProject}
<SidebarFooter className="p-2">
<SidebarMenu>
<SidebarMenuItem>
{isOnSettings ? (
<SidebarMenuButton
size="sm"
className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
onClick={() => window.history.back()}
>
{isPickingFolder ? "Picking folder..." : "Browse for folder"}
</button>
)}
<div className="flex gap-2">
<button
type="button"
className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
onClick={handleAddProject}
disabled={isAddingProject}
>
{isAddingProject ? "Adding..." : "Add"}
</button>
<button
type="button"
className="flex-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground/80 transition-colors duration-150 hover:bg-secondary"
onClick={() => setAddingProject(false)}
<ArrowLeftIcon className="size-3.5" />
<span className="text-xs">Back</span>
</SidebarMenuButton>
) : (
<SidebarMenuButton
size="sm"
className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
onClick={() => void navigate({ to: "/settings" })}
>
Cancel
</button>
</div>
</>
) : (
<button
type="button"
className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground/70 transition-colors duration-150 hover:border-ring hover:text-muted-foreground"
onClick={() => setAddingProject(true)}
>
+ Add project
</button>
)}
<SettingsIcon className="size-3.5" />
<span className="text-xs">Settings</span>
</SidebarMenuButton>
)}
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</>
);
Expand Down