()
| 231 | // --------------------------------------------------------------------------- |
| 232 | |
| 233 | function ThreadsRoute() { |
| 234 | const { threads, loaded, createThread, deleteThread, touchThread } = |
| 235 | useThreadIndex() |
| 236 | const [activeId, setActiveId] = useState<string | null>(null) |
| 237 | |
| 238 | // Ensure there's always an active thread: keep the current one if it still |
| 239 | // exists, otherwise fall back to the most recent, otherwise create one. |
| 240 | useEffect(() => { |
| 241 | if (!loaded) return |
| 242 | if (activeId && threads.some((t) => t.id === activeId)) return |
| 243 | if (threads.length > 0) { |
| 244 | setActiveId(threads[0].id) |
| 245 | return |
| 246 | } |
| 247 | setActiveId(createThread().id) |
| 248 | // `threads`/`createThread` are derived from the same state; depending on |
| 249 | // `activeId` + `loaded` is enough to re-run when the active thread vanishes. |
| 250 | }, [loaded, activeId, threads.length]) |
| 251 | |
| 252 | const handleNewChat = () => { |
| 253 | // Don't pile up empty chats — if an untouched "New chat" exists, focus it. |
| 254 | const empty = threads.find((t) => t.title === NEW_CHAT_TITLE) |
| 255 | setActiveId(empty ? empty.id : createThread().id) |
| 256 | } |
| 257 | |
| 258 | const handleDelete = (id: string) => { |
| 259 | deleteThread(id) |
| 260 | if (id === activeId) { |
| 261 | const next = threads.find((t) => t.id !== id) |
| 262 | // If nothing remains the bootstrap effect creates a fresh thread. |
| 263 | setActiveId(next ? next.id : null) |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | return ( |
| 268 | <div className="flex h-[calc(100vh-72px)] bg-gray-900 text-white"> |
| 269 | <aside className="flex w-72 flex-col border-r border-gray-800 bg-gray-950"> |
| 270 | <div className="p-3"> |
| 271 | <button |
| 272 | type="button" |
| 273 | onClick={handleNewChat} |
| 274 | className="flex w-full items-center justify-center gap-2 rounded-lg border border-orange-500/30 bg-orange-500/10 px-3 py-2 text-sm font-medium text-orange-300 transition-colors hover:bg-orange-500/20" |
| 275 | > |
| 276 | <Plus size={16} /> |
| 277 | New chat |
| 278 | </button> |
| 279 | </div> |
| 280 | <nav className="flex-1 overflow-y-auto px-2 pb-3"> |
| 281 | {threads.length === 0 && ( |
| 282 | <p className="px-3 py-4 text-sm text-gray-500">No chats yet.</p> |
| 283 | )} |
| 284 | {threads.map((thread) => { |
| 285 | const isActive = thread.id === activeId |
| 286 | return ( |
| 287 | <div |
| 288 | key={thread.id} |
| 289 | className={`group mb-1 flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors ${ |
| 290 | isActive |
nothing calls this directly
no test coverage detected