| @@ -2,11 +2,12 @@ import { · 1 unmodified line |
| 2 | 2 | ChevronRightIcon, |
| 3 | 3 | FolderIcon, |
| 4 | 4 | GitPullRequestIcon, |
| 5 | + GripVerticalIcon, |
| 5 | 6 | RocketIcon, |
| 6 | 7 | SquarePenIcon, |
| 7 | 8 | TerminalIcon, |
| 8 | 9 | } from "lucide-react"; |
| 9 | | −import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 10 | +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 10 | 11 | import { |
| 11 | 12 | DEFAULT_MODEL, |
| 12 | 13 | type DesktopUpdateState, |
| @@ -261,6 +262,7 @@ export default function Sidebar() { · 248 unmodified lines |
| 261 | 262 | const threads = useStore((store) => store.threads); |
| 262 | 263 | const markThreadUnread = useStore((store) => store.markThreadUnread); |
| 263 | 264 | const toggleProject = useStore((store) => store.toggleProject); |
| 265 | + const reorderProjects = useStore((store) => store.reorderProjects); |
| 264 | 266 | const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearThreadDraft); |
| 265 | 267 | const getDraftThreadByProjectId = useComposerDraftStore( |
| 266 | 268 | (store) => store.getDraftThreadByProjectId, |
| @@ -297,6 +299,8 @@ export default function Sidebar() { · 30 unmodified lines |
| 297 | 299 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< |
| 298 | 300 | ReadonlySet<ProjectId> |
| 299 | 301 | >(() => new Set()); |
| 302 | + const [draggedProjectId, setDraggedProjectId] = useState<ProjectId | null>(null); |
| 303 | + const [projectDropIndex, setProjectDropIndex] = useState<number | null>(null); |
| 300 | 304 | const renamingCommittedRef = useRef(false); |
| 301 | 305 | const renamingInputRef = useRef<HTMLInputElement | null>(null); |
| 302 | 306 | const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null); |
| @@ -982,6 +986,11 @@ export default function Sidebar() { · 679 unmodified lines |
| 982 | 986 | </div> |
| 983 | 987 | ); |
| 984 | 988 | |
| 989 | + const resetProjectDrag = () => { |
| 990 | + setDraggedProjectId(null); |
| 991 | + setProjectDropIndex(null); |
| 992 | + }; |
| 993 | + |
| 985 | 994 | return ( |
| 986 | 995 | <> |
| 987 | 996 | {isElectron ? ( |
| @@ -1018,7 +1027,7 @@ export default function Sidebar() { · 30 unmodified lines |
| 1018 | 1027 | <SidebarContent className="gap-0"> |
| 1019 | 1028 | <SidebarGroup className="px-2 py-2"> |
| 1020 | 1029 | <SidebarMenu> |
| 1021 | | − {projects.map((project) => { |
| 1030 | + {projects.map((project, projectIndex) => { |
| 1022 | 1031 | const projectThreads = threads |
| 1023 | 1032 | .filter((thread) => thread.projectId === project.id) |
| 1024 | 1033 | .toSorted((a, b) => { |
| @@ -1034,246 +1043,298 @@ export default function Sidebar() { · 9 unmodified lines |
| 1034 | 1043 | : projectThreads; |
| 1035 | 1044 | |
| 1036 | 1045 | return ( |
| 1037 | | − <Collapsible |
| 1038 | | − key={project.id} |
| 1039 | | − className="group/collapsible" |
| 1040 | | − open={project.expanded} |
| 1041 | | − onOpenChange={(open) => { |
| 1042 | | − if (open === project.expanded) return; |
| 1043 | | − toggleProject(project.id); |
| 1044 | | − }} |
| 1045 | | − > |
| 1046 | | − <SidebarMenuItem> |
| 1047 | | − <div className="group/project-header relative"> |
| 1048 | | − <CollapsibleTrigger |
| 1049 | | − render={ |
| 1050 | | − <SidebarMenuButton |
| 1051 | | − size="sm" |
| 1052 | | − className="gap-2 px-2 py-1.5 text-left hover:bg-accent group-hover/project-header:bg-accent group-hover/project-header:text-sidebar-accent-foreground" |
| 1053 | | − /> |
| 1054 | | − } |
| 1055 | | − onContextMenu={(event) => { |
| 1056 | | − event.preventDefault(); |
| 1057 | | − void handleProjectContextMenu(project.id, { |
| 1058 | | − x: event.clientX, |
| 1059 | | − y: event.clientY, |
| 1060 | | − }); |
| 1061 | | − }} |
| 1062 | | − > |
| 1063 | | − <ChevronRightIcon |
| 1064 | | − className={`-ml-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-150 ${ |
| 1065 | | − project.expanded ? "rotate-90" : "" |
| 1066 | | − }`} |
| 1067 | | − /> |
| 1068 | | − <ProjectFavicon cwd={project.cwd} /> |
| 1069 | | − <span className="flex-1 truncate text-xs font-medium text-foreground/90"> |
| 1070 | | − {project.name} |
| 1071 | | − </span> |
| 1072 | | − </CollapsibleTrigger> |
| 1073 | | − <Tooltip> |
| 1074 | | − <TooltipTrigger |
| 1046 | + <Fragment key={project.id}> |
| 1047 | + {draggedProjectId !== null && projectDropIndex === projectIndex && ( |
| 1048 | + <SidebarMenuItem aria-hidden className="pointer-events-none px-2 py-0.5"> |
| 1049 | + <div className="h-0.5 w-full rounded-full bg-primary/80" /> |
| 1050 | + </SidebarMenuItem> |
| 1051 | + )} |
| 1052 | + <Collapsible |
| 1053 | + className="group/collapsible" |
| 1054 | + open={project.expanded} |
| 1055 | + onOpenChange={(open) => { |
| 1056 | + if (open === project.expanded) return; |
| 1057 | + toggleProject(project.id); |
| 1058 | + }} |
| 1059 | + > |
| 1060 | + <SidebarMenuItem className={draggedProjectId === project.id ? "opacity-60" : ""}> |
| 1061 | + <div className="group/project-header relative"> |
| 1062 | + <CollapsibleTrigger |
| 1075 | 1063 | render={ |
| 1076 | | − <SidebarMenuAction |
| 1077 | | − render={ |
| 1078 | | − <button |
| 1079 | | − type="button" |
| 1080 | | − aria-label={`Create new thread in ${project.name}`} |
| 1081 | | − /> |
| 1082 | | − } |
| 1083 | | − showOnHover |
| 1084 | | − className="top-1 right-1 size-5 rounded-md p-0 text-muted-foreground/70 hover:bg-secondary hover:text-foreground" |
| 1085 | | − onClick={(event) => { |
| 1064 | + <SidebarMenuButton |
| 1065 | + size="sm" |
| 1066 | + draggable |
| 1067 | + aria-grabbed={draggedProjectId === project.id} |
| 1068 | + className="gap-2 px-2 py-1.5 text-left hover:bg-accent group-hover/project-header:bg-accent group-hover/project-header:text-sidebar-accent-foreground cursor-grab active:cursor-grabbing" |
| 1069 | + onDragStart={(event) => { |
| 1070 | + setDraggedProjectId(project.id); |
| 1071 | + setProjectDropIndex(null); |
| 1072 | + event.dataTransfer.effectAllowed = "move"; |
| 1073 | + event.dataTransfer.setData("text/plain", project.cwd); |
| 1074 | + }} |
| 1075 | + onDragEnd={() => { |
| 1076 | + resetProjectDrag(); |
| 1077 | + }} |
| 1078 | + onDragOver={(event) => { |
| 1079 | + if (draggedProjectId === null) return; |
| 1086 | 1080 | event.preventDefault(); |
| 1087 | | − event.stopPropagation(); |
| 1088 | | − void handleNewThread(project.id); |
| 1081 | + event.dataTransfer.dropEffect = "move"; |
| 1082 | + const bounds = event.currentTarget.getBoundingClientRect(); |
| 1083 | + const nextDropIndex = |
| 1084 | + event.clientY < bounds.top + bounds.height / 2 |
| 1085 | + ? projectIndex |
| 1086 | + : projectIndex + 1; |
| 1087 | + if (projectDropIndex !== nextDropIndex) { |
| 1088 | + setProjectDropIndex(nextDropIndex); |
| 1089 | + } |
| 1089 | 1090 | }} |
| 1090 | | − > |
| 1091 | | − <SquarePenIcon className="size-3.5" /> |
| 1092 | | − </SidebarMenuAction> |
| 1091 | + onDrop={(event) => { |
| 1092 | + if (draggedProjectId === null) return; |
| 1093 | + event.preventDefault(); |
| 1094 | + const bounds = event.currentTarget.getBoundingClientRect(); |
| 1095 | + const destinationIndex = |
| 1096 | + event.clientY < bounds.top + bounds.height / 2 |
| 1097 | + ? projectIndex |
| 1098 | + : projectIndex + 1; |
| 1099 | + reorderProjects(draggedProjectId, destinationIndex); |
| 1100 | + resetProjectDrag(); |
| 1101 | + }} |
| 1102 | + /> |
| 1093 | 1103 | } |
| 1094 | | − /> |
| 1095 | | − <TooltipPopup side="top"> |
| 1096 | | − {newThreadShortcutLabel |
| 1097 | | − ? `New thread (${newThreadShortcutLabel})` |
| 1098 | | − : "New thread"} |
| 1099 | | − </TooltipPopup> |
| 1100 | | − </Tooltip> |
| 1101 | | − </div> |
| 1102 | | − |
| 1103 | | − <CollapsibleContent> |
| 1104 | | − <SidebarMenuSub className="mx-1 my-0 w-full translate-x-0 gap-0 px-1.5 py-0"> |
| 1105 | | − {visibleThreads.map((thread) => { |
| 1106 | | − const isActive = routeThreadId === thread.id; |
| 1107 | | − const threadStatus = threadStatusPill( |
| 1108 | | − thread, |
| 1109 | | − pendingApprovalByThreadId.get(thread.id) === true, |
| 1110 | | − ); |
| 1111 | | − const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null); |
| 1112 | | − const terminalStatus = terminalStatusFromRunningIds( |
| 1113 | | − selectThreadTerminalState(terminalStateByThreadId, thread.id) |
| 1114 | | − .runningTerminalIds, |
| 1115 | | − ); |
| 1116 | | − |
| 1117 | | − return ( |
| 1118 | | − <SidebarMenuSubItem key={thread.id} className="w-full"> |
| 1119 | | − <SidebarMenuSubButton |
| 1120 | | − render={<div role="button" tabIndex={0} />} |
| 1121 | | − size="sm" |
| 1122 | | − isActive={isActive} |
| 1123 | | − className={`h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left hover:bg-accent hover:text-foreground ${ |
| 1124 | | − isActive |
| 1125 | | − ? "bg-accent/85 text-foreground font-medium ring-1 ring-border/70 dark:bg-accent/55 dark:ring-border/50" |
| 1126 | | − : "text-muted-foreground" |
| 1127 | | − }`} |
| 1128 | | − onClick={() => { |
| 1129 | | − void navigate({ |
| 1130 | | − to: "/$threadId", |
| 1131 | | − params: { threadId: thread.id }, |
| 1132 | | − }); |
| 1133 | | − }} |
| 1134 | | − onKeyDown={(event) => { |
| 1135 | | − if (event.key !== "Enter" && event.key !== " ") return; |
| 1136 | | − event.preventDefault(); |
| 1137 | | − void navigate({ |
| 1138 | | − to: "/$threadId", |
| 1139 | | − params: { threadId: thread.id }, |
| 1140 | | − }); |
| 1141 | | − }} |
| 1142 | | − onContextMenu={(event) => { |
| 1104 | + onContextMenu={(event) => { |
| 1105 | + event.preventDefault(); |
| 1106 | + void handleProjectContextMenu(project.id, { |
| 1107 | + x: event.clientX, |
| 1108 | + y: event.clientY, |
| 1109 | + }); |
| 1110 | + }} |
| 1111 | + > |
| 1112 | + <GripVerticalIcon |
| 1113 | + aria-hidden |
| 1114 | + className="size-3 shrink-0 text-muted-foreground/35" |
| 1115 | + /> |
| 1116 | + <ChevronRightIcon |
| 1117 | + className={`-ml-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-150 ${ |
| 1118 | + project.expanded ? "rotate-90" : "" |
| 1119 | + }`} |
| 1120 | + /> |
| 1121 | + <ProjectFavicon cwd={project.cwd} /> |
| 1122 | + <span className="flex-1 truncate text-xs font-medium text-foreground/90"> |
| 1123 | + {project.name} |
| 1124 | + </span> |
| 1125 | + </CollapsibleTrigger> |
| 1126 | + <Tooltip> |
| 1127 | + <TooltipTrigger |
| 1128 | + render={ |
| 1129 | + <SidebarMenuAction |
| 1130 | + render={ |
| 1131 | + <button |
| 1132 | + type="button" |
| 1133 | + aria-label={`Create new thread in ${project.name}`} |
| 1134 | + /> |
| 1135 | + } |
| 1136 | + showOnHover |
| 1137 | + className="top-1 right-1 size-5 rounded-md p-0 text-muted-foreground/70 hover:bg-secondary hover:text-foreground" |
| 1138 | + onClick={(event) => { |
| 1143 | 1139 | event.preventDefault(); |
| 1144 | | − void handleThreadContextMenu(thread.id, { |
| 1145 | | − x: event.clientX, |
| 1146 | | − y: event.clientY, |
| 1147 | | − }); |
| 1140 | + event.stopPropagation(); |
| 1141 | + void handleNewThread(project.id); |
| 1148 | 1142 | }} |
| 1149 | 1143 | > |
| 1150 | | − <div className="flex min-w-0 flex-1 items-center gap-1.5 text-left"> |
| 1151 | | − {prStatus && ( |
| 1152 | | − <Tooltip> |
| 1153 | | − <TooltipTrigger |
| 1154 | | − render={ |
| 1155 | | − <button |
| 1156 | | − type="button" |
| 1157 | | − aria-label={prStatus.tooltip} |
| 1158 | | − className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-ring`} |
| 1159 | | − onClick={(event) => { |
| 1160 | | − openPrLink(event, prStatus.url); |
| 1161 | | − }} |
| 1162 | | − > |
| 1163 | | − <GitPullRequestIcon className="size-3" /> |
| 1164 | | − </button> |
| 1165 | | − } |
| 1166 | | − /> |
| 1167 | | − <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup> |
| 1168 | | − </Tooltip> |
| 1169 | | − )} |
| 1170 | | − {threadStatus && ( |
| 1171 | | − <span |
| 1172 | | − className={`inline-flex items-center gap-1 text-[10px] ${threadStatus.colorClass}`} |
| 1173 | | − > |
| 1144 | + <SquarePenIcon className="size-3.5" /> |
| 1145 | + </SidebarMenuAction> |
| 1146 | + } |
| 1147 | + /> |
| 1148 | + <TooltipPopup side="top"> |
| 1149 | + {newThreadShortcutLabel |
| 1150 | + ? `New thread (${newThreadShortcutLabel})` |
| 1151 | + : "New thread"} |
| 1152 | + </TooltipPopup> |
| 1153 | + </Tooltip> |
| 1154 | + </div> |
| 1155 | + |
| 1156 | + <CollapsibleContent> |
| 1157 | + <SidebarMenuSub className="mx-1 my-0 w-full translate-x-0 gap-0 px-1.5 py-0"> |
| 1158 | + {visibleThreads.map((thread) => { |
| 1159 | + const isActive = routeThreadId === thread.id; |
| 1160 | + const threadStatus = threadStatusPill( |
| 1161 | + thread, |
| 1162 | + pendingApprovalByThreadId.get(thread.id) === true, |
| 1163 | + ); |
| 1164 | + const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null); |
| 1165 | + const terminalStatus = terminalStatusFromRunningIds( |
| 1166 | + selectThreadTerminalState(terminalStateByThreadId, thread.id) |
| 1167 | + .runningTerminalIds, |
| 1168 | + ); |
| 1169 | + |
| 1170 | + return ( |
| 1171 | + <SidebarMenuSubItem key={thread.id} className="w-full"> |
| 1172 | + <SidebarMenuSubButton |
| 1173 | + render={<div role="button" tabIndex={0} />} |
| 1174 | + size="sm" |
| 1175 | + isActive={isActive} |
| 1176 | + className={`h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left hover:bg-accent hover:text-foreground ${ |
| 1177 | + isActive |
| 1178 | + ? "bg-accent/85 text-foreground font-medium ring-1 ring-border/70 dark:bg-accent/55 dark:ring-border/50" |
| 1179 | + : "text-muted-foreground" |
| 1180 | + }`} |
| 1181 | + onClick={() => { |
| 1182 | + void navigate({ |
| 1183 | + to: "/$threadId", |
| 1184 | + params: { threadId: thread.id }, |
| 1185 | + }); |
| 1186 | + }} |
| 1187 | + onKeyDown={(event) => { |
| 1188 | + if (event.key !== "Enter" && event.key !== " ") return; |
| 1189 | + event.preventDefault(); |
| 1190 | + void navigate({ |
| 1191 | + to: "/$threadId", |
| 1192 | + params: { threadId: thread.id }, |
| 1193 | + }); |
| 1194 | + }} |
| 1195 | + onContextMenu={(event) => { |
| 1196 | + event.preventDefault(); |
| 1197 | + void handleThreadContextMenu(thread.id, { |
| 1198 | + x: event.clientX, |
| 1199 | + y: event.clientY, |
| 1200 | + }); |
| 1201 | + }} |
| 1202 | + > |
| 1203 | + <div className="flex min-w-0 flex-1 items-center gap-1.5 text-left"> |
| 1204 | + {prStatus && ( |
| 1205 | + <Tooltip> |
| 1206 | + <TooltipTrigger |
| 1207 | + render={ |
| 1208 | + <button |
| 1209 | + type="button" |
| 1210 | + aria-label={prStatus.tooltip} |
| 1211 | + className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-ring`} |
| 1212 | + onClick={(event) => { |
| 1213 | + openPrLink(event, prStatus.url); |
| 1214 | + }} |
| 1215 | + > |
| 1216 | + <GitPullRequestIcon className="size-3" /> |
| 1217 | + </button> |
| 1218 | + } |
| 1219 | + /> |
| 1220 | + <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup> |
| 1221 | + </Tooltip> |
| 1222 | + )} |
| 1223 | + {threadStatus && ( |
| 1174 | 1224 | <span |
| 1175 | | − className={`h-1.5 w-1.5 rounded-full ${threadStatus.dotClass} ${ |
| 1176 | | − threadStatus.pulse ? "animate-pulse" : "" |
| 1177 | | − }`} |
| 1225 | + className={`inline-flex items-center gap-1 text-[10px] ${threadStatus.colorClass}`} |
| 1226 | + > |
| 1227 | + <span |
| 1228 | + className={`h-1.5 w-1.5 rounded-full ${threadStatus.dotClass} ${ |
| 1229 | + threadStatus.pulse ? "animate-pulse" : "" |
| 1230 | + }`} |
| 1231 | + /> |
| 1232 | + <span className="hidden md:inline">{threadStatus.label}</span> |
| 1233 | + </span> |
| 1234 | + )} |
| 1235 | + {renamingThreadId === thread.id ? ( |
| 1236 | + <input |
| 1237 | + ref={(el) => { |
| 1238 | + if (el && renamingInputRef.current !== el) { |
| 1239 | + renamingInputRef.current = el; |
| 1240 | + el.focus(); |
| 1241 | + el.select(); |
| 1242 | + } |
| 1243 | + }} |
| 1244 | + className="min-w-0 flex-1 truncate text-xs bg-transparent outline-none border border-ring rounded px-0.5" |
| 1245 | + value={renamingTitle} |
| 1246 | + onChange={(e) => setRenamingTitle(e.target.value)} |
| 1247 | + onKeyDown={(e) => { |
| 1248 | + e.stopPropagation(); |
| 1249 | + if (e.key === "Enter") { |
| 1250 | + e.preventDefault(); |
| 1251 | + renamingCommittedRef.current = true; |
| 1252 | + void commitRename(thread.id, renamingTitle, thread.title); |
| 1253 | + } else if (e.key === "Escape") { |
| 1254 | + e.preventDefault(); |
| 1255 | + renamingCommittedRef.current = true; |
| 1256 | + cancelRename(); |
| 1257 | + } |
| 1258 | + }} |
| 1259 | + onBlur={() => { |
| 1260 | + if (!renamingCommittedRef.current) { |
| 1261 | + void commitRename(thread.id, renamingTitle, thread.title); |
| 1262 | + } |
| 1263 | + }} |
| 1264 | + onClick={(e) => e.stopPropagation()} |
| 1178 | 1265 | /> |
| 1179 | | − <span className="hidden md:inline">{threadStatus.label}</span> |
| 1180 | | − </span> |
| 1181 | | − )} |
| 1182 | | − {renamingThreadId === thread.id ? ( |
| 1183 | | − <input |
| 1184 | | − ref={(el) => { |
| 1185 | | − if (el && renamingInputRef.current !== el) { |
| 1186 | | − renamingInputRef.current = el; |
| 1187 | | − el.focus(); |
| 1188 | | − el.select(); |
| 1189 | | − } |
| 1190 | | − }} |
| 1191 | | − className="min-w-0 flex-1 truncate text-xs bg-transparent outline-none border border-ring rounded px-0.5" |
| 1192 | | − value={renamingTitle} |
| 1193 | | − onChange={(e) => setRenamingTitle(e.target.value)} |
| 1194 | | − onKeyDown={(e) => { |
| 1195 | | − e.stopPropagation(); |
| 1196 | | − if (e.key === "Enter") { |
| 1197 | | − e.preventDefault(); |
| 1198 | | − renamingCommittedRef.current = true; |
| 1199 | | − void commitRename(thread.id, renamingTitle, thread.title); |
| 1200 | | − } else if (e.key === "Escape") { |
| 1201 | | − e.preventDefault(); |
| 1202 | | − renamingCommittedRef.current = true; |
| 1203 | | − cancelRename(); |
| 1204 | | − } |
| 1205 | | − }} |
| 1206 | | − onBlur={() => { |
| 1207 | | − if (!renamingCommittedRef.current) { |
| 1208 | | − void commitRename(thread.id, renamingTitle, thread.title); |
| 1209 | | − } |
| 1210 | | − }} |
| 1211 | | − onClick={(e) => e.stopPropagation()} |
| 1212 | | − /> |
| 1213 | | − ) : ( |
| 1214 | | − <span className="min-w-0 flex-1 truncate text-xs"> |
| 1215 | | − {thread.title} |
| 1216 | | − </span> |
| 1217 | | − )} |
| 1218 | | − </div> |
| 1219 | | − <div className="ml-auto flex shrink-0 items-center gap-1.5"> |
| 1220 | | − {terminalStatus && ( |
| 1266 | + ) : ( |
| 1267 | + <span className="min-w-0 flex-1 truncate text-xs"> |
| 1268 | + {thread.title} |
| 1269 | + </span> |
| 1270 | + )} |
| 1271 | + </div> |
| 1272 | + <div className="ml-auto flex shrink-0 items-center gap-1.5"> |
| 1273 | + {terminalStatus && ( |
| 1274 | + <span |
| 1275 | + role="img" |
| 1276 | + aria-label={terminalStatus.label} |
| 1277 | + title={terminalStatus.label} |
| 1278 | + className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`} |
| 1279 | + > |
| 1280 | + <TerminalIcon |
| 1281 | + className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} |
| 1282 | + /> |
| 1283 | + </span> |
| 1284 | + )} |
| 1221 | 1285 | <span |
| 1222 | | − role="img" |
| 1223 | | − aria-label={terminalStatus.label} |
| 1224 | | − title={terminalStatus.label} |
| 1225 | | − className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`} |
| 1286 | + className={`text-[10px] ${ |
| 1287 | + isActive ? "text-foreground/65" : "text-muted-foreground/40" |
| 1288 | + }`} |
| 1226 | 1289 | > |
| 1227 | | − <TerminalIcon |
| 1228 | | − className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} |
| 1229 | | − /> |
| 1290 | + {formatRelativeTime(thread.createdAt)} |
| 1230 | 1291 | </span> |
| 1231 | | − )} |
| 1232 | | − <span |
| 1233 | | − className={`text-[10px] ${ |
| 1234 | | − isActive ? "text-foreground/65" : "text-muted-foreground/40" |
| 1235 | | − }`} |
| 1236 | | − > |
| 1237 | | − {formatRelativeTime(thread.createdAt)} |
| 1238 | | − </span> |
| 1239 | | − </div> |
| 1292 | + </div> |
| 1293 | + </SidebarMenuSubButton> |
| 1294 | + </SidebarMenuSubItem> |
| 1295 | + ); |
| 1296 | + })} |
| 1297 | + |
| 1298 | + {hasHiddenThreads && !isThreadListExpanded && ( |
| 1299 | + <SidebarMenuSubItem className="w-full"> |
| 1300 | + <SidebarMenuSubButton |
| 1301 | + render={<button type="button" />} |
| 1302 | + size="sm" |
| 1303 | + className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80" |
| 1304 | + onClick={() => { |
| 1305 | + expandThreadListForProject(project.id); |
| 1306 | + }} |
| 1307 | + > |
| 1308 | + <span>Show more</span> |
| 1240 | 1309 | </SidebarMenuSubButton> |
| 1241 | 1310 | </SidebarMenuSubItem> |
| 1242 | | − ); |
| 1243 | | − })} |
| 1244 | | − |
| 1245 | | − {hasHiddenThreads && !isThreadListExpanded && ( |
| 1246 | | − <SidebarMenuSubItem className="w-full"> |
| 1247 | | − <SidebarMenuSubButton |
| 1248 | | − render={<button type="button" />} |
| 1249 | | − size="sm" |
| 1250 | | − className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80" |
| 1251 | | − onClick={() => { |
| 1252 | | − expandThreadListForProject(project.id); |
| 1253 | | − }} |
| 1254 | | − > |
| 1255 | | − <span>Show more</span> |
| 1256 | | − </SidebarMenuSubButton> |
| 1257 | | − </SidebarMenuSubItem> |
| 1258 | | − )} |
| 1259 | | − {hasHiddenThreads && isThreadListExpanded && ( |
| 1260 | | − <SidebarMenuSubItem className="w-full"> |
| 1261 | | − <SidebarMenuSubButton |
| 1262 | | − render={<button type="button" />} |
| 1263 | | − size="sm" |
| 1264 | | − className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80" |
| 1265 | | − onClick={() => { |
| 1266 | | − collapseThreadListForProject(project.id); |
| 1267 | | − }} |
| 1268 | | − > |
| 1269 | | − <span>Show less</span> |
| 1270 | | − </SidebarMenuSubButton> |
| 1271 | | − </SidebarMenuSubItem> |
| 1272 | | − )} |
| 1273 | | − </SidebarMenuSub> |
| 1274 | | − </CollapsibleContent> |
| 1275 | | − </SidebarMenuItem> |
| 1276 | | − </Collapsible> |
| 1311 | + )} |
| 1312 | + {hasHiddenThreads && isThreadListExpanded && ( |
| 1313 | + <SidebarMenuSubItem className="w-full"> |
| 1314 | + <SidebarMenuSubButton |
| 1315 | + render={<button type="button" />} |
| 1316 | + size="sm" |
| 1317 | + className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80" |
| 1318 | + onClick={() => { |
| 1319 | + collapseThreadListForProject(project.id); |
| 1320 | + }} |
| 1321 | + > |
| 1322 | + <span>Show less</span> |
| 1323 | + </SidebarMenuSubButton> |
| 1324 | + </SidebarMenuSubItem> |
| 1325 | + )} |
| 1326 | + </SidebarMenuSub> |
| 1327 | + </CollapsibleContent> |
| 1328 | + </SidebarMenuItem> |
| 1329 | + </Collapsible> |
| 1330 | + {draggedProjectId !== null && |
| 1331 | + projectDropIndex === projectIndex + 1 && |
| 1332 | + projectIndex === projects.length - 1 && ( |
| 1333 | + <SidebarMenuItem aria-hidden className="pointer-events-none px-2 py-0.5"> |
| 1334 | + <div className="h-0.5 w-full rounded-full bg-primary/80" /> |
| 1335 | + </SidebarMenuItem> |
| 1336 | + )} |
| 1337 | + </Fragment> |
| 1277 | 1338 | ); |
| 1278 | 1339 | })} |
| 1279 | 1340 | </SidebarMenu> |