SPB Git

Repository Public

session-switcher.tsx 3.0 KB 63 lines tsx

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
Files main
Menu

Theme