Repository Public
1'use client';2import { useRouter } from 'next/navigation';3import { FolderGit2, FolderTree, GitBranch, ListChecks, Sparkles, SquareTerminal } from 'lucide-react';4import { useAuthStatus, useProjects, useSessions } from '@/lib/queries';5import { useUi } from '@/stores/ui';6import { useCurrentProject } from '@/hooks/use-current';7import { useNow } from '@/hooks/use-viewport';8import { fmtRelative } from '@/lib/format';9import { LIVE_SESSION_STATES } from '@cr/shared';10import { Button } from '@/components/ui/button';11import { IllusFolder, IllusTerminal } from '@/components/ui/illustrations';12import { greeting } from './mobile-home';13import { HostStrip, JumpBackIn, NeedsYou, SectionTitle, TaskActivity } from './sections';14import { useBrand } from '@/stores/branding';1516/** Desktop start view: shown in the workspace when no terminal is open, and at /home/. Real data only. */17export function StartView() {18 const brand = useBrand();19 const router = useRouter();20 const auth = useAuthStatus();21 const sessions = useSessions().data ?? [];22 const projects = useProjects();23 const project = useCurrentProject();24 const openNew = useUi((s) => s.openNewSession);25 const now = useNow(30_000);26 const live = sessions.filter((s) => LIVE_SESSION_STATES.includes(s.state)).length;27 const recent = [...(projects.data ?? [])].sort((a, b) => Number(b.pinned) - Number(a.pinned) || (b.lastActivityAt ?? '').localeCompare(a.lastActivityAt ?? '')).slice(0, 6);28 const username = auth.data?.user?.username;29 return (30 <div className="h-full overflow-y-auto" data-testid="start-view">31 <div className="mx-auto flex max-w-6xl flex-col gap-7 px-8 py-8">32 <header className="flex flex-wrap items-end justify-between gap-4">33 <div>34 <h1 className="text-[26px] font-bold leading-tight tracking-tight" data-testid="greeting">35 {greeting(new Date(now).getHours())}{username ? `, ${username}` : ''}36 </h1>37 <p className="tnum mt-1 text-sm text-text-2">{live === 0 ? 'Nothing is running right now.' : `${live} session${live === 1 ? '' : 's'} running on the server.`}</p>38 </div>39 <div className="flex flex-wrap gap-2">40 <Button variant="brand" className="h-10 px-4" onClick={() => openNew(project?.id ?? null)} data-testid="empty-new-session">41 <Sparkles className="size-4" /> New session42 </Button>43 <Button className="h-10" onClick={() => router.push(project ? `/files/?p=${project.id}` : '/files/')}><FolderTree className="size-4" /> Files</Button>44 <Button className="h-10" onClick={() => router.push('/tasks/')}><ListChecks className="size-4" /> Tasks</Button>45 </div>46 </header>47 <div className="grid gap-7 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]">48 <div className="flex min-w-0 flex-col gap-7">49 <NeedsYou />50 <JumpBackIn51 empty={52 <div className="card flex items-center gap-6 px-8 py-7" data-testid="no-session">53 <IllusTerminal />54 <div>55 <p className="text-base font-semibold tracking-tight">No sessions yet</p>56 <p className="mt-1 max-w-md text-[13px] leading-relaxed text-text-2">Start a {brand.agentName} or shell session. It runs on the server inside tmux and keeps going after you close this browser.</p>57 </div>58 </div>59 }60 />61 <section aria-label="Projects">62 <SectionTitle href="/projects/">Projects</SectionTitle>63 {projects.isPending ? null : recent.length === 0 ? (64 <div className="card flex items-center gap-6 px-8 py-6">65 <IllusFolder />66 <div>67 <p className="text-base font-semibold tracking-tight">No projects yet</p>68 <p className="mt-1 text-[13px] text-text-2">Register a directory or clone a repository to give sessions a home.</p>69 <Button className="mt-3" onClick={() => router.push('/projects/')}>Add a project</Button>70 </div>71 </div>72 ) : (73 <ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">74 {recent.map((p) => (75 <li key={p.id}>76 <button onClick={() => openNew(p.id)} className="card pressable flex w-full items-center gap-3 p-3.5 text-left">77 <span className="flex size-9 shrink-0 items-center justify-center rounded-[12px] bg-accent/10 text-accent"><FolderGit2 className="size-[18px]" aria-hidden /></span>78 <span className="min-w-0 flex-1">79 <span className="block truncate text-[13.5px] font-semibold">{p.name}</span>80 <span className="tnum flex items-center gap-1 truncate text-xs text-muted">81 {p.gitBranch && <><GitBranch className="size-3" aria-hidden /><span className="font-mono">{p.gitBranch}</span> · </>}82 {p.lastActivityAt ? fmtRelative(p.lastActivityAt, now) : 'no activity yet'}83 </span>84 </span>85 <SquareTerminal className="size-4 shrink-0 text-faint" aria-hidden />86 </button>87 </li>88 ))}89 </ul>90 )}91 </section>92 </div>93 <aside className="flex min-w-0 flex-col gap-7">94 <HostStrip />95 <TaskActivity />96 </aside>97 </div>98 </div>99 </div>100 );101}102