| ... | @@ -1,8 +1,11 @@ | ... | @@ -1,8 +1,11 @@ |
| 1 | import { | 1 | import { |
| | 2 | ArrowLeftIcon, |
| 2 | ChevronRightIcon, | 3 | ChevronRightIcon, |
| 3 | FolderIcon, | 4 | FolderIcon, |
| 4 | GitPullRequestIcon, | 5 | GitPullRequestIcon, |
| | 6 | PlusIcon, |
| 5 | RocketIcon, | 7 | RocketIcon, |
| | 8 | SettingsIcon, |
| 6 | SquarePenIcon, | 9 | SquarePenIcon, |
| 7 | TerminalIcon, | 10 | TerminalIcon, |
| 8 | } from "lucide-react"; | 11 | } from "lucide-react"; |
| ... | @@ -17,7 +20,7 @@ import { | ... | @@ -17,7 +20,7 @@ import { |
| 17 | type ResolvedKeybindingsConfig, | 20 | type ResolvedKeybindingsConfig, |
| 18 | } from "@t3tools/contracts"; | 21 | } from "@t3tools/contracts"; |
| 19 | import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; | 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 | import { useAppSettings } from "../appSettings"; | 24 | import { useAppSettings } from "../appSettings"; |
| 22 | import { isElectron } from "../env"; | 25 | import { isElectron } from "../env"; |
| 23 | import { APP_STAGE_LABEL } from "../branding"; | 26 | import { APP_STAGE_LABEL } from "../branding"; |
| ... | @@ -278,6 +281,7 @@ export default function Sidebar() { | ... | @@ -278,6 +281,7 @@ export default function Sidebar() { |
| 278 | (store) => store.clearProjectDraftThreadById, | 281 | (store) => store.clearProjectDraftThreadById, |
| 279 | ); | 282 | ); |
| 280 | const navigate = useNavigate(); | 283 | const navigate = useNavigate(); |
| | 284 | const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" }); |
| 281 | const { settings: appSettings } = useAppSettings(); | 285 | const { settings: appSettings } = useAppSettings(); |
| 282 | const routeThreadId = useParams({ | 286 | const routeThreadId = useParams({ |
| 283 | strict: false, | 287 | strict: false, |
| ... | @@ -293,6 +297,8 @@ export default function Sidebar() { | ... | @@ -293,6 +297,8 @@ export default function Sidebar() { |
| 293 | const [newCwd, setNewCwd] = useState(""); | 297 | const [newCwd, setNewCwd] = useState(""); |
| 294 | const [isPickingFolder, setIsPickingFolder] = useState(false); | 298 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 295 | const [isAddingProject, setIsAddingProject] = useState(false); | 299 | const [isAddingProject, setIsAddingProject] = useState(false); |
| | 300 | const [addProjectError, setAddProjectError] = useState<string | null>(null); |
| | 301 | const addProjectInputRef = useRef<HTMLInputElement | null>(null); |
| 296 | const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null); | 302 | const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null); |
| 297 | const [renamingTitle, setRenamingTitle] = useState(""); | 303 | const [renamingTitle, setRenamingTitle] = useState(""); |
| 298 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< | 304 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< |
| ... | @@ -486,6 +492,7 @@ export default function Sidebar() { | ... | @@ -486,6 +492,7 @@ export default function Sidebar() { |
| 486 | const finishAddingProject = () => { | 492 | const finishAddingProject = () => { |
| 487 | setIsAddingProject(false); | 493 | setIsAddingProject(false); |
| 488 | setNewCwd(""); | 494 | setNewCwd(""); |
| | 495 | setAddProjectError(null); |
| 489 | setAddingProject(false); | 496 | setAddingProject(false); |
| 490 | }; | 497 | }; |
| 491 | | 498 | |
| ... | @@ -512,12 +519,9 @@ export default function Sidebar() { | ... | @@ -512,12 +519,9 @@ export default function Sidebar() { |
| 512 | await handleNewThread(projectId).catch(() => undefined); | 519 | await handleNewThread(projectId).catch(() => undefined); |
| 513 | } catch (error) { | 520 | } catch (error) { |
| 514 | setIsAddingProject(false); | 521 | setIsAddingProject(false); |
| 515 | toastManager.add({ | 522 | setAddProjectError( |
| 516 | type: "error", | 523 | error instanceof Error ? error.message : "An error occurred while adding the project.", |
| 517 | title: "Unable to add project", | 524 | ); |
| 518 | description: | | |
| 519 | error instanceof Error ? error.message : "An error occurred while adding the project.", | | |
| 520 | }); | | |
| 521 | return; | 525 | return; |
| 522 | } | 526 | } |
| 523 | finishAddingProject(); | 527 | finishAddingProject(); |
| ... | @@ -541,6 +545,8 @@ export default function Sidebar() { | ... | @@ -541,6 +545,8 @@ export default function Sidebar() { |
| 541 | } | 545 | } |
| 542 | if (pickedPath) { | 546 | if (pickedPath) { |
| 543 | await addProjectFromPath(pickedPath); | 547 | await addProjectFromPath(pickedPath); |
| | 548 | } else { |
| | 549 | addProjectInputRef.current?.focus(); |
| 544 | } | 550 | } |
| 545 | setIsPickingFolder(false); | 551 | setIsPickingFolder(false); |
| 546 | }; | 552 | }; |
| ... | @@ -1025,6 +1031,95 @@ export default function Sidebar() { | ... | @@ -1025,6 +1031,95 @@ export default function Sidebar() { |
| 1025 | | 1031 | |
| 1026 | <SidebarContent className="gap-0"> | 1032 | <SidebarContent className="gap-0"> |
| 1027 | <SidebarGroup className="px-2 py-2"> | 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 | <SidebarMenu> | 1123 | <SidebarMenu> |
| 1029 | {projects.map((project) => { | 1124 | {projects.map((project) => { |
| 1030 | const projectThreads = threads | 1125 | const projectThreads = threads |
| ... | @@ -1288,68 +1383,37 @@ export default function Sidebar() { | ... | @@ -1288,68 +1383,37 @@ export default function Sidebar() { |
| 1288 | | 1383 | |
| 1289 | {projects.length === 0 && !addingProject && ( | 1384 | {projects.length === 0 && !addingProject && ( |
| 1290 | <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60"> | 1385 | <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60"> |
| 1291 | No projects yet. | 1386 | No projects yet |
| 1292 | <br /> | | |
| 1293 | Add one to get started. | | |
| 1294 | </div> | 1387 | </div> |
| 1295 | )} | 1388 | )} |
| 1296 | </SidebarGroup> | 1389 | </SidebarGroup> |
| 1297 | </SidebarContent> | 1390 | </SidebarContent> |
| 1298 | | 1391 | |
| 1299 | <SidebarSeparator /> | 1392 | <SidebarSeparator /> |
| 1300 | <SidebarFooter className="gap-0 p-3"> | 1393 | <SidebarFooter className="p-2"> |
| 1301 | {addingProject ? ( | 1394 | <SidebarMenu> |
| 1302 | <> | 1395 | <SidebarMenuItem> |
| 1303 | <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> | 1396 | {isOnSettings ? ( |
| 1304 | Add project | 1397 | <SidebarMenuButton |
| 1305 | </p> | 1398 | size="sm" |
| 1306 | <input | 1399 | className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground" |
| 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" | 1400 | onClick={() => window.history.back()} |
| 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} | | |
| 1322 | > | 1401 | > |
| 1323 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} | 1402 | <ArrowLeftIcon className="size-3.5" /> |
| 1324 | </button> | 1403 | <span className="text-xs">Back</span> |
| 1325 | )} | 1404 | </SidebarMenuButton> |
| 1326 | <div className="flex gap-2"> | 1405 | ) : ( |
| 1327 | <button | 1406 | <SidebarMenuButton |
| 1328 | type="button" | 1407 | size="sm" |
| 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" | 1408 | className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground" |
| 1330 | onClick={handleAddProject} | 1409 | onClick={() => void navigate({ to: "/settings" })} |
| 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)} | | |
| 1339 | > | 1410 | > |
| 1340 | Cancel | 1411 | <SettingsIcon className="size-3.5" /> |
| 1341 | </button> | 1412 | <span className="text-xs">Settings</span> |
| 1342 | </div> | 1413 | </SidebarMenuButton> |
| 1343 | </> | 1414 | )} |
| 1344 | ) : ( | 1415 | </SidebarMenuItem> |
| 1345 | <button | 1416 | </SidebarMenu> |
| 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 | )} | | |
| 1353 | </SidebarFooter> | 1417 | </SidebarFooter> |
| 1354 | </> | 1418 | </> |
| 1355 | ); | 1419 | ); |