Repository Public
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