Repository Public
1'use client';2import { useEffect, useState } from 'react';3import { Command } from 'cmdk';4import { useSessions } from '@/lib/queries';5import { useUi } from '@/stores/ui';6import { useTerminals } from '@/stores/terminals';7import { useOpenSession } from '@/hooks/use-nav';8import { SessionDot } from '@/components/sessions/state-badge';9import { SESSION_STATE } from '@/lib/states';10import { Kbd } from '@/components/ui/misc';1112/** Quick switcher (Ctrl+` or Cmd/Ctrl+J): most recent first, the previous session preselected. */13export function SessionSwitcher() {14 const open = useUi((s) => s.switcherOpen);15 const setUi = useUi((s) => s.set);16 const sessions = useSessions().data ?? [];17 const activeId = useTerminals((s) => s.activeId);18 const openSession = useOpenSession();19 const ordered = [...sessions].sort((a, b) => (b.lastActivityAt ?? b.createdAt).localeCompare(a.lastActivityAt ?? a.createdAt));20 const [value, setValue] = useState('');21 useEffect(() => {22 if (open) setValue((ordered.find((s) => s.id !== activeId) ?? ordered[0])?.id ?? '');23 // eslint-disable-next-line react-hooks/exhaustive-deps -- preselect only when opening24 }, [open]);25 return (26 <Command.Dialog27 open={open}28 onOpenChange={(o) => setUi({ switcherOpen: o })}29 label="Switch session"30 value={value}31 onValueChange={setValue}32 overlayClassName="anim-fade fixed inset-0 z-[60] bg-scrim"33 contentClassName="anim-pop fixed left-1/2 top-[18dvh] z-[60] w-[min(460px,calc(100vw-24px))] -translate-x-1/2 overflow-hidden rounded-lg border border-line-2 bg-surface shadow-pop"34 >35 <Command.Input placeholder="Switch session…" data-testid="switcher-input" className="h-11 w-full border-b border-line bg-transparent px-3.5 text-sm placeholder:text-muted focus:outline-none" />36 <Command.List className="max-h-[50dvh] overflow-y-auto p-1.5">37 <Command.Empty className="px-3 py-6 text-center text-xs text-muted">No sessions yet</Command.Empty>38 {ordered.map((s) => (39 <Command.Item40 key={s.id}41 value={s.id}42 keywords={[s.title, s.projectName ?? '']}43 onSelect={() => {44 setUi({ switcherOpen: false });45 openSession(s.id);46 }}47 className="flex min-h-10 cursor-default items-center gap-2.5 rounded-sm px-2.5 text-[13px] text-text-2 data-[selected=true]:bg-hover data-[selected=true]:text-text pointer-coarse:min-h-12"48 >49 <SessionDot state={s.state} />50 <span className="truncate">{s.title}</span>51 {s.id === activeId && <span className="text-[11px] text-muted">current</span>}52 <span className="ml-auto shrink-0 text-[11px] text-muted">{SESSION_STATE[s.state].label}</span>53 </Command.Item>54 ))}55 </Command.List>56 <div className="flex items-center gap-3 border-t border-line px-3 py-1.5 text-[11px] text-muted">57 <span><Kbd>↑</Kbd> <Kbd>↓</Kbd> select</span>58 <span><Kbd>⏎</Kbd> open</span>59 </div>60 </Command.Dialog>61 );62}63