| ... | ... | @@ -1,8 +1,11 @@ |
| 1 | 1 | import { |
| 2 | ArrowLeftIcon, |
| 2 | 3 | ChevronRightIcon, |
| 3 | 4 | FolderIcon, |
| 4 | 5 | GitPullRequestIcon, |
| 6 | PlusIcon, |
| 5 | 7 | RocketIcon, |
| 8 | SettingsIcon, |
| 6 | 9 | SquarePenIcon, |
| 7 | 10 | TerminalIcon, |
| 8 | 11 | } from "lucide-react"; |
| ... | ... | @@ -17,7 +20,7 @@ import { |
| 17 | 20 | type ResolvedKeybindingsConfig, |
| 18 | 21 | } from "@t3tools/contracts"; |
| 19 | 22 | import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 20 | | import { useNavigate, useParams } from "@tanstack/react-router"; |
| 23 | import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; |
| 21 | 24 | import { useAppSettings } from "../appSettings"; |
| 22 | 25 | import { isElectron } from "../env"; |
| 23 | 26 | import { APP_STAGE_LABEL } from "../branding"; |
| ... | ... | @@ -278,6 +281,7 @@ export default function Sidebar() { |
| 278 | 281 | (store) => store.clearProjectDraftThreadById, |
| 279 | 282 | ); |
| 280 | 283 | const navigate = useNavigate(); |
| 284 | const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" }); |
| 281 | 285 | const { settings: appSettings } = useAppSettings(); |
| 282 | 286 | const routeThreadId = useParams({ |
| 283 | 287 | strict: false, |
| ... | ... | @@ -293,6 +297,8 @@ export default function Sidebar() { |
| 293 | 297 | const [newCwd, setNewCwd] = useState(""); |
| 294 | 298 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 295 | 299 | const [isAddingProject, setIsAddingProject] = useState(false); |
| 300 | const [addProjectError, setAddProjectError] = useState<string | null>(null); |
| 301 | const addProjectInputRef = useRef<HTMLInputElement | null>(null); |
| 296 | 302 | const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null); |
| 297 | 303 | const [renamingTitle, setRenamingTitle] = useState(""); |
| 298 | 304 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< |
| ... | ... | @@ -486,6 +492,7 @@ export default function Sidebar() { |
| 486 | 492 | const finishAddingProject = () => { |
| 487 | 493 | setIsAddingProject(false); |
| 488 | 494 | setNewCwd(""); |
| 495 | setAddProjectError(null); |
| 489 | 496 | setAddingProject(false); |
| 490 | 497 | }; |
| 491 | 498 | |
| ... | ... | @@ -512,12 +519,9 @@ export default function Sidebar() { |
| 512 | 519 | await handleNewThread(projectId).catch(() => undefined); |
| 513 | 520 | } catch (error) { |
| 514 | 521 | setIsAddingProject(false); |
| 515 | | toastManager.add({ |
| 516 | | type: "error", |
| 517 | | title: "Unable to add project", |
| 518 | | description: |
| 519 | | error instanceof Error ? error.message : "An error occurred while adding the project.", |
| 520 | | }); |
| 522 | setAddProjectError( |
| 523 | error instanceof Error ? error.message : "An error occurred while adding the project.", |
| 524 | ); |
| 521 | 525 | return; |
| 522 | 526 | } |
| 523 | 527 | finishAddingProject(); |
| ... | ... | @@ -541,6 +545,8 @@ export default function Sidebar() { |
| 541 | 545 | } |
| 542 | 546 | if (pickedPath) { |
| 543 | 547 | await addProjectFromPath(pickedPath); |
| 548 | } else { |
| 549 | addProjectInputRef.current?.focus(); |
| 544 | 550 | } |
| 545 | 551 | setIsPickingFolder(false); |
| 546 | 552 | }; |
| ... | ... | @@ -1025,6 +1031,95 @@ export default function Sidebar() { |
| 1025 | 1031 | |
| 1026 | 1032 | <SidebarContent className="gap-0"> |
| 1027 | 1033 | <SidebarGroup className="px-2 py-2"> |
| 1034 | <div className="mb-1 flex items-center justify-between px-2"> |
| 1035 | <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> |
| 1036 | Projects |
| 1037 | </span> |
| 1038 | <Tooltip> |
| 1039 | <TooltipTrigger |
| 1040 | render={ |
| 1041 | <button |
| 1042 | type="button" |
| 1043 | aria-label="Add project" |
| 1044 | className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground" |
| 1045 | onClick={() => { |
| 1046 | setAddingProject((prev) => !prev); |
| 1047 | setAddProjectError(null); |
| 1048 | }} |
| 1049 | /> |
| 1050 | } |
| 1051 | > |
| 1052 | <PlusIcon className="size-3.5" /> |
| 1053 | </TooltipTrigger> |
| 1054 | <TooltipPopup side="right">Add project</TooltipPopup> |
| 1055 | </Tooltip> |
| 1056 | </div> |
| 1057 | |
| 1058 | {addingProject && ( |
| 1059 | <div className="mb-2 px-1"> |
| 1060 | {isElectron && ( |
| 1061 | <button |
| 1062 | type="button" |
| 1063 | 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" |
| 1064 | onClick={() => void handlePickFolder()} |
| 1065 | disabled={isPickingFolder || isAddingProject} |
| 1066 | > |
| 1067 | <FolderIcon className="size-3.5" /> |
| 1068 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 1069 | </button> |
| 1070 | )} |
| 1071 | <div className="flex gap-1.5"> |
| 1072 | <input |
| 1073 | ref={addProjectInputRef} |
| 1074 | 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 ${ |
| 1075 | addProjectError |
| 1076 | ? "border-red-500/70 focus:border-red-500" |
| 1077 | : "border-border focus:border-ring" |
| 1078 | }`} |
| 1079 | placeholder="/path/to/project" |
| 1080 | value={newCwd} |
| 1081 | onChange={(event) => { |
| 1082 | setNewCwd(event.target.value); |
| 1083 | setAddProjectError(null); |
| 1084 | }} |
| 1085 | onKeyDown={(event) => { |
| 1086 | if (event.key === "Enter") handleAddProject(); |
| 1087 | if (event.key === "Escape") { |
| 1088 | setAddingProject(false); |
| 1089 | setAddProjectError(null); |
| 1090 | } |
| 1091 | }} |
| 1092 | autoFocus |
| 1093 | /> |
| 1094 | <button |
| 1095 | type="button" |
| 1096 | 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" |
| 1097 | onClick={handleAddProject} |
| 1098 | disabled={isAddingProject} |
| 1099 | > |
| 1100 | {isAddingProject ? "Adding..." : "Add"} |
| 1101 | </button> |
| 1102 | </div> |
| 1103 | {addProjectError && ( |
| 1104 | <p className="mt-1 px-0.5 text-[11px] leading-tight text-red-400"> |
| 1105 | {addProjectError} |
| 1106 | </p> |
| 1107 | )} |
| 1108 | <div className="mt-1.5 px-0.5"> |
| 1109 | <button |
| 1110 | type="button" |
| 1111 | className="text-[11px] text-muted-foreground/50 transition-colors hover:text-muted-foreground" |
| 1112 | onClick={() => { |
| 1113 | setAddingProject(false); |
| 1114 | setAddProjectError(null); |
| 1115 | }} |
| 1116 | > |
| 1117 | Cancel |
| 1118 | </button> |
| 1119 | </div> |
| 1120 | </div> |
| 1121 | )} |
| 1122 | |
| 1028 | 1123 | <SidebarMenu> |
| 1029 | 1124 | {projects.map((project) => { |
| 1030 | 1125 | const projectThreads = threads |
| ... | ... | @@ -1288,68 +1383,37 @@ export default function Sidebar() { |
| 1288 | 1383 | |
| 1289 | 1384 | {projects.length === 0 && !addingProject && ( |
| 1290 | 1385 | <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60"> |
| 1291 | | No projects yet. |
| 1292 | | <br /> |
| 1293 | | Add one to get started. |
| 1386 | No projects yet |
| 1294 | 1387 | </div> |
| 1295 | 1388 | )} |
| 1296 | 1389 | </SidebarGroup> |
| 1297 | 1390 | </SidebarContent> |
| 1298 | 1391 | |
| 1299 | 1392 | <SidebarSeparator /> |
| 1300 | | <SidebarFooter className="gap-0 p-3"> |
| 1301 | | {addingProject ? ( |
| 1302 | | <> |
| 1303 | | <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> |
| 1304 | | Add project |
| 1305 | | </p> |
| 1306 | | <input |
| 1307 | | 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" |
| 1308 | | placeholder="/path/to/project" |
| 1309 | | value={newCwd} |
| 1310 | | onChange={(event) => setNewCwd(event.target.value)} |
| 1311 | | onKeyDown={(event) => { |
| 1312 | | if (event.key === "Enter") handleAddProject(); |
| 1313 | | if (event.key === "Escape") setAddingProject(false); |
| 1314 | | }} |
| 1315 | | /> |
| 1316 | | {isElectron && ( |
| 1317 | | <button |
| 1318 | | type="button" |
| 1319 | | 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" |
| 1320 | | onClick={() => void handlePickFolder()} |
| 1321 | | disabled={isPickingFolder || isAddingProject} |
| 1393 | <SidebarFooter className="p-2"> |
| 1394 | <SidebarMenu> |
| 1395 | <SidebarMenuItem> |
| 1396 | {isOnSettings ? ( |
| 1397 | <SidebarMenuButton |
| 1398 | size="sm" |
| 1399 | className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground" |
| 1400 | onClick={() => window.history.back()} |
| 1322 | 1401 | > |
| 1323 | | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 1324 | | </button> |
| 1325 | | )} |
| 1326 | | <div className="flex gap-2"> |
| 1327 | | <button |
| 1328 | | type="button" |
| 1329 | | 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" |
| 1330 | | onClick={handleAddProject} |
| 1331 | | disabled={isAddingProject} |
| 1332 | | > |
| 1333 | | {isAddingProject ? "Adding..." : "Add"} |
| 1334 | | </button> |
| 1335 | | <button |
| 1336 | | type="button" |
| 1337 | | 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" |
| 1338 | | onClick={() => setAddingProject(false)} |
| 1402 | <ArrowLeftIcon className="size-3.5" /> |
| 1403 | <span className="text-xs">Back</span> |
| 1404 | </SidebarMenuButton> |
| 1405 | ) : ( |
| 1406 | <SidebarMenuButton |
| 1407 | size="sm" |
| 1408 | className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground" |
| 1409 | onClick={() => void navigate({ to: "/settings" })} |
| 1339 | 1410 | > |
| 1340 | | Cancel |
| 1341 | | </button> |
| 1342 | | </div> |
| 1343 | | </> |
| 1344 | | ) : ( |
| 1345 | | <button |
| 1346 | | type="button" |
| 1347 | | 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" |
| 1348 | | onClick={() => setAddingProject(true)} |
| 1349 | | > |
| 1350 | | + Add project |
| 1351 | | </button> |
| 1352 | | )} |
| 1411 | <SettingsIcon className="size-3.5" /> |
| 1412 | <span className="text-xs">Settings</span> |
| 1413 | </SidebarMenuButton> |
| 1414 | )} |
| 1415 | </SidebarMenuItem> |
| 1416 | </SidebarMenu> |
| 1353 | 1417 | </SidebarFooter> |
| 1354 | 1418 | </> |
| 1355 | 1419 | ); |