- Avoid cloning thread arrays before sorting - Cache loaded project favicons across sidebar renders - Update sidebar sort labels for clearer wording
Unified · Split
| @@ -224,7 +224,7 @@ export function getThreadSortTimestamp( · 223 unmodified lines | |||
| 224 | export function sortThreadsForSidebar< | 224 | export function sortThreadsForSidebar< |
| 225 | T extends Pick<Thread, "id" | "createdAt" | "updatedAt" | "messages">, | 225 | T extends Pick<Thread, "id" | "createdAt" | "updatedAt" | "messages">, |
| 226 | >(threads: readonly T[], sortOrder: SidebarThreadSortOrder): T[] { | 226 | >(threads: readonly T[], sortOrder: SidebarThreadSortOrder): T[] { |
| 227 | return [...threads].toSorted((left, right) => { | 227 | return threads.toSorted((left, right) => { |
| 228 | const byTimestamp = | 228 | const byTimestamp = |
| 229 | getThreadSortTimestamp(right, sortOrder) - getThreadSortTimestamp(left, sortOrder); | 229 | getThreadSortTimestamp(right, sortOrder) - getThreadSortTimestamp(left, sortOrder); |
| 230 | if (byTimestamp !== 0) return byTimestamp; | 230 | if (byTimestamp !== 0) return byTimestamp; |
| @@ -105,17 +105,18 @@ const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; · 104 unmodified lines | |||
| 105 | const THREAD_PREVIEW_LIMIT = 6; | 105 | const THREAD_PREVIEW_LIMIT = 6; |
| 106 | const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = { | 106 | const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = { |
| 107 | updated_at: "Last user message", | 107 | updated_at: "Last user message", |
| 108 | created_at: "Created", | 108 | created_at: "Created at", |
| 109 | manual: "Manual", | 109 | manual: "Manual", |
| 110 | }; | 110 | }; |
| 111 | const SIDEBAR_THREAD_SORT_LABELS: Record<SidebarThreadSortOrder, string> = { | 111 | const SIDEBAR_THREAD_SORT_LABELS: Record<SidebarThreadSortOrder, string> = { |
| 112 | updated_at: "Last user message", | 112 | updated_at: "Last user message", |
| 113 | created_at: "Created", | 113 | created_at: "Created at", |
| 114 | }; | 114 | }; |
| 115 | const SIDEBAR_LIST_ANIMATION_OPTIONS = { | 115 | const SIDEBAR_LIST_ANIMATION_OPTIONS = { |
| 116 | duration: 180, | 116 | duration: 180, |
| 117 | easing: "ease-out", | 117 | easing: "ease-out", |
| 118 | } as const; | 118 | } as const; |
| 119 | const loadedProjectFaviconSrcs = new Set<string>(); | ||
| 119 | 120 | ||
| 120 | function formatRelativeTime(iso: string): string { | 121 | function formatRelativeTime(iso: string): string { |
| 121 | const diff = Date.now() - new Date(iso).getTime(); | 122 | const diff = Date.now() - new Date(iso).getTime(); |
| @@ -226,9 +227,10 @@ function getServerHttpOrigin(): string { · 104 unmodified lines | |||
| 226 | const serverHttpOrigin = getServerHttpOrigin(); | 227 | const serverHttpOrigin = getServerHttpOrigin(); |
| 227 | 228 | ||
| 228 | function ProjectFavicon({ cwd }: { cwd: string }) { | 229 | function ProjectFavicon({ cwd }: { cwd: string }) { |
| 229 | const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading"); | ||
| 230 | |||
| 231 | const src = `${serverHttpOrigin}/api/project-favicon?cwd=${encodeURIComponent(cwd)}`; | 230 | const src = `${serverHttpOrigin}/api/project-favicon?cwd=${encodeURIComponent(cwd)}`; |
| 231 | const [status, setStatus] = useState<"loading" | "loaded" | "error">(() => | ||
| 232 | loadedProjectFaviconSrcs.has(src) ? "loaded" : "loading", | ||
| 233 | ); | ||
| 232 | 234 | ||
| 233 | if (status === "error") { | 235 | if (status === "error") { |
| 234 | return <FolderIcon className="size-3.5 shrink-0 text-muted-foreground/50" />; | 236 | return <FolderIcon className="size-3.5 shrink-0 text-muted-foreground/50" />; |
| @@ -239,7 +241,10 @@ function ProjectFavicon({ cwd }: { cwd: string }) { · 4 unmodified lines | |||
| 239 | src={src} | 241 | src={src} |
| 240 | alt="" | 242 | alt="" |
| 241 | className={`size-3.5 shrink-0 rounded-sm object-contain ${status === "loading" ? "hidden" : ""}`} | 243 | className={`size-3.5 shrink-0 rounded-sm object-contain ${status === "loading" ? "hidden" : ""}`} |
| 242 | onLoad={() => setStatus("loaded")} | 244 | onLoad={() => { |
| 245 | loadedProjectFaviconSrcs.add(src); | ||
| 246 | setStatus("loaded"); | ||
| 247 | }} | ||
| 243 | onError={() => setStatus("error")} | 248 | onError={() => setStatus("error")} |
| 244 | /> | 249 | /> |
| 245 | ); | 250 | ); |
| @@ -275,7 +280,7 @@ function ProjectSortMenu({ · 29 unmodified lines | |||
| 275 | </Tooltip> | 280 | </Tooltip> |
| 276 | <MenuPopup align="end" side="bottom" className="min-w-44"> | 281 | <MenuPopup align="end" side="bottom" className="min-w-44"> |
| 277 | <MenuGroup> | 282 | <MenuGroup> |
| 278 | <div className="px-2 py-1 text-[11px] font-medium text-muted-foreground"> | 283 | <div className="px-2 py-1 sm:text-xs font-medium text-muted-foreground"> |
| 279 | Sort projects | 284 | Sort projects |
| 280 | </div> | 285 | </div> |
| 281 | <MenuRadioGroup | 286 | <MenuRadioGroup |
| @@ -286,7 +291,7 @@ function ProjectSortMenu({ · 4 unmodified lines | |||
| 286 | > | 291 | > |
| 287 | {(Object.entries(SIDEBAR_SORT_LABELS) as Array<[SidebarProjectSortOrder, string]>).map( | 292 | {(Object.entries(SIDEBAR_SORT_LABELS) as Array<[SidebarProjectSortOrder, string]>).map( |
| 288 | ([value, label]) => ( | 293 | ([value, label]) => ( |
| 289 | <MenuRadioItem key={value} value={value} className="min-h-7 py-1 text-xs"> | 294 | <MenuRadioItem key={value} value={value} className="min-h-7 py-1 sm:text-xs"> |
| 290 | {label} | 295 | {label} |
| 291 | </MenuRadioItem> | 296 | </MenuRadioItem> |
| 292 | ), | 297 | ), |
| @@ -294,7 +299,7 @@ function ProjectSortMenu({ · 1 unmodified line | |||
| 294 | </MenuRadioGroup> | 299 | </MenuRadioGroup> |
| 295 | </MenuGroup> | 300 | </MenuGroup> |
| 296 | <MenuGroup> | 301 | <MenuGroup> |
| 297 | <div className="px-2 pt-2 pb-1 text-[11px] font-medium text-muted-foreground"> | 302 | <div className="px-2 pt-2 pb-1 sm:text-xs font-medium text-muted-foreground"> |
| 298 | Sort threads | 303 | Sort threads |
| 299 | </div> | 304 | </div> |
| 300 | <MenuRadioGroup | 305 | <MenuRadioGroup |
| @@ -306,7 +311,7 @@ function ProjectSortMenu({ · 5 unmodified lines | |||
| 306 | {( | 311 | {( |
| 307 | Object.entries(SIDEBAR_THREAD_SORT_LABELS) as Array<[SidebarThreadSortOrder, string]> | 312 | Object.entries(SIDEBAR_THREAD_SORT_LABELS) as Array<[SidebarThreadSortOrder, string]> |
| 308 | ).map(([value, label]) => ( | 313 | ).map(([value, label]) => ( |
| 309 | <MenuRadioItem key={value} value={value} className="min-h-7 py-1 text-xs"> | 314 | <MenuRadioItem key={value} value={value} className="min-h-7 py-1 sm:text-xs"> |
| 310 | {label} | 315 | {label} |
| 311 | </MenuRadioItem> | 316 | </MenuRadioItem> |
| 312 | ))} | 317 | ))} |