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