SPB Git

Repository Public

header.tsx 5.6 KB 108 lines tsx

1'use client';2import Link from 'next/link';3import { usePathname } from 'next/navigation';4import { Columns2, Command, FolderGit2, GitBranch, LayoutPanelLeft, ListChecks, PanelRight, Plus } from 'lucide-react';5import { useSessions, useTasks } from '@/lib/queries';6import { PRESETS, useUi } from '@/stores/ui';7import { useTerminals } from '@/stores/terminals';8import { useActiveSession, useCurrentProject } from '@/hooks/use-current';9import { Button, IconButton } from '@/components/ui/button';10import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from '@/components/ui/dropdown';11import { SessionStateBadge } from '@/components/sessions/state-badge';12import { Kbd } from '@/components/ui/misc';13import { Tooltip } from '@/components/ui/tooltip';14import { TASK_ACTIVE } from '@/lib/states';15import { isMac } from '@/lib/format';16import { ThemeToggle } from './theme-toggle';1718export function Header() {19  const pathname = usePathname();20  const onTerminal = pathname.startsWith('/terminal');21  const project = useCurrentProject();22  const session = useActiveSession();23  const tasks = useTasks().data ?? [];24  const running = tasks.filter((t) => TASK_ACTIVE.includes(t.state)).length;25  const ui = useUi();26  const { openIds, activeId, splitId, setSplit } = useTerminals();27  const sessions = useSessions().data ?? [];28  const others = openIds.filter((id) => id !== activeId);29  const cwd = project ? `${project.rootLabel}/${project.path}${session?.cwd ? `/${session.cwd}` : ''}` : null;3031  return (32    <header className="flex h-14 shrink-0 items-center gap-3 border-b border-line bg-bg px-4">33      {onTerminal ? (34        <div className="flex min-w-0 flex-1 items-center gap-3 text-xs" data-testid="workspace-header">35          <span className="flex min-w-0 items-center gap-1.5 text-[13px] font-medium text-text">36            <FolderGit2 className="size-3.5 shrink-0 text-muted" aria-hidden />37            <span className="truncate">{project?.name ?? 'No project'}</span>38          </span>39          {project?.gitBranch && (40            <Tooltip content={project.gitDirty ? 'Working tree has uncommitted changes' : 'Working tree clean'}>41              <span className="flex shrink-0 items-center gap-1 text-text-2">42                <GitBranch className="size-3.5 text-muted" aria-hidden />43                <span className="font-mono">{project.gitBranch}</span>44                {project.gitDirty && <span className="text-warning" aria-label="uncommitted changes">●</span>}45              </span>46            </Tooltip>47          )}48          {cwd && <span className="hidden min-w-0 truncate font-mono text-[11px] text-muted xl:block">{cwd}</span>}49          {session && <SessionStateBadge state={session.state} />}50          {session?.state === 'WAITING_FOR_INPUT' && <span className="hidden truncate text-coral-ink lg:block">{session.waitingReason ?? 'Waiting for input'}</span>}51        </div>52      ) : (53        <span className="flex-1" />54      )}55      {running > 0 && (56        <Link href="/tasks/" className="tnum flex h-7 items-center gap-1.5 rounded-sm px-2 text-xs text-accent hover:bg-surface-2" aria-label={`${running} running tasks`}>57          <ListChecks className="size-3.5" /> {running} running58        </Link>59      )}60      <button onClick={() => ui.set({ paletteOpen: true })} className="hidden h-8 items-center gap-2 rounded-full bg-surface px-3 text-xs text-muted shadow-card hover:text-text-2 lg:flex" aria-label="Open command palette">61        <Command className="size-3" /> Search <Kbd>{isMac() ? '⌘K' : 'Ctrl K'}</Kbd>62      </button>63      {onTerminal && (64        <>65          <DropdownMenu>66            <DropdownMenuTrigger asChild>67              <IconButton label="Split view" active={!!splitId} disabled={others.length === 0 && !splitId}>68                <Columns2 className="size-4" />69              </IconButton>70            </DropdownMenuTrigger>71            <DropdownMenuContent>72              <DropdownMenuLabel>Show beside the active terminal</DropdownMenuLabel>73              {others.map((id) => (74                <DropdownMenuItem key={id} onSelect={() => setSplit(id)}>75                  {sessions.find((s) => s.id === id)?.title ?? id.slice(0, 8)}76                </DropdownMenuItem>77              ))}78              {splitId && <DropdownMenuItem onSelect={() => setSplit(null)}>Close split</DropdownMenuItem>}79            </DropdownMenuContent>80          </DropdownMenu>81          <DropdownMenu>82            <DropdownMenuTrigger asChild>83              <IconButton label="Layout presets">84                <LayoutPanelLeft className="size-4" />85              </IconButton>86            </DropdownMenuTrigger>87            <DropdownMenuContent>88              <DropdownMenuLabel>Layout</DropdownMenuLabel>89              {PRESETS.map((p) => (90                <DropdownMenuItem key={p.id} onSelect={() => ui.applyPreset(p.id)} data-testid={`preset-${p.id}`}>91                  <span className={ui.preset === p.id ? 'text-accent' : undefined}>{p.label}</span>92                </DropdownMenuItem>93              ))}94            </DropdownMenuContent>95          </DropdownMenu>96          <IconButton label={ui.rightOpen ? 'Hide side panel' : 'Show side panel'} active={ui.rightOpen} onClick={() => ui.set({ rightOpen: !ui.rightOpen })}>97            <PanelRight className="size-4" />98          </IconButton>99        </>100      )}101      <ThemeToggle />102      <Button size="sm" variant="brand" className="h-8 rounded-full px-3.5" onClick={() => ui.openNewSession(project?.id ?? null)} data-testid="new-session">103        <Plus className="size-3.5" /> New session104      </Button>105    </header>106  );107}108
Files main
Menu

Theme