SPB Git

Repository Public

right-panel.tsx 7.4 KB 149 lines tsx

1'use client';2import Link from 'next/link';3import { useRouter } from 'next/navigation';4import { useNotifications, useSystem, useTasks } from '@/lib/queries';5import { useUi, type RightTab } from '@/stores/ui';6import { useActiveSession, useCurrentProject } from '@/hooks/use-current';7import { useNow } from '@/hooks/use-viewport';8import { DASH, fmtDateTime, fmtRelative, fmtTime } from '@/lib/format';9import { TASK_ACTIVE, sessionStateHint } from '@/lib/states';10import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';11import { EmptyState, ErrorState, Spinner } from '@/components/ui/misc';12import { SessionStateBadge, TaskStateBadge } from '@/components/sessions/state-badge';13import { FilesBrowser } from '@/components/files/files-browser';14import { GitPanel } from '@/components/files/git-panel';15import { useEditor } from '@/components/editor/editor-store';16import { HostMetrics } from '@/components/system/host-metrics';17import { useBrand } from '@/stores/branding';1819export function RightPanel() {20  const tab = useUi((s) => s.rightTab);21  const editorOpen = useUi((s) => s.editorOpen);22  const setUi = useUi((s) => s.set);23  const project = useCurrentProject();24  const router = useRouter();25  const activePath = useEditor((s) => s.tabs.find((t) => t.key === s.activeKey)?.path ?? null);26  const noProject = <EmptyState title="No project in context" description="Open a session that belongs to a project, or register a project." />;27  return (28    <Tabs value={tab} onValueChange={(v) => setUi({ rightTab: v as RightTab })} className="flex h-full min-w-0 flex-col bg-surface" data-testid="right-panel">29      <TabsList>30        <TabsTrigger value="files">Files</TabsTrigger>31        <TabsTrigger value="git">Git</TabsTrigger>32        <TabsTrigger value="activity">Activity</TabsTrigger>33        <TabsTrigger value="metrics">Metrics</TabsTrigger>34        <TabsTrigger value="task">Task</TabsTrigger>35      </TabsList>36      <TabsContent value="files" className="overflow-hidden">37        {project ? (38          <FilesBrowser39            projectId={project.id}40            mode="tree"41            activePath={activePath}42            onOpenFile={(path) => (editorOpen ? void useEditor.getState().open(project.id, path) : router.push(`/files/?p=${project.id}&path=${encodeURIComponent(path)}`))}43          />44        ) : noProject}45      </TabsContent>46      <TabsContent value="git" className="overflow-hidden">47        {project ? (48          <GitPanel49            projectId={project.id}50            onOpenDiff={(path) => {51              setUi({ editorOpen: true });52              void useEditor.getState().openGitDiff(project.id, path);53            }}54          />55        ) : noProject}56      </TabsContent>57      <TabsContent value="activity" className="overflow-y-auto"><ActivityTab /></TabsContent>58      <TabsContent value="metrics" className="overflow-y-auto"><MetricsTab /></TabsContent>59      <TabsContent value="task" className="overflow-y-auto"><TaskTab /></TabsContent>60    </Tabs>61  );62}6364function ActivityTab() {65  const brand = useBrand();66  const s = useActiveSession();67  const notifications = useNotifications().data ?? [];68  const now = useNow(10_000);69  return (70    <div className="flex flex-col gap-4 p-3">71      {s ? (72        <dl className="tnum grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs">73          <dt className="text-muted">Session</dt><dd className="truncate text-text">{s.title}</dd>74          <dt className="text-muted">State</dt><dd><SessionStateBadge state={s.state} /></dd>75          <dt className="text-muted" /><dd className="text-[11px] text-text-2">{s.state === 'WAITING_FOR_INPUT' ? (s.waitingReason ?? sessionStateHint(s.state, brand)) : sessionStateHint(s.state, brand)}</dd>76          <dt className="text-muted">Kind</dt><dd>{s.kind === 'claude' ? brand.agentName : 'Shell'}</dd>77          <dt className="text-muted">Attached clients</dt><dd>{s.attachedClients}</dd>78          <dt className="text-muted">Last activity</dt><dd>{fmtRelative(s.lastActivityAt, now)}</dd>79          <dt className="text-muted">Started</dt><dd>{fmtDateTime(s.createdAt)}</dd>80          <dt className="text-muted">PID</dt><dd>{s.pid ?? DASH}</dd>81          <dt className="text-muted">Conversation</dt><dd className="truncate font-mono text-[11px]">{s.claudeConversationId ?? DASH}</dd>82        </dl>83      ) : (84        <p className="text-xs text-muted">No active session.</p>85      )}86      <div>87        <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted">Recent events</h3>88        {notifications.length === 0 ? <p className="text-xs text-muted">No events yet</p> : (89          <ul className="flex flex-col">90            {notifications.slice(0, 30).map((n) => (91              <li key={n.id} className="flex items-baseline gap-2 border-b border-line py-1.5 text-xs last:border-0">92                <span className="tnum shrink-0 text-[11px] text-muted">{fmtTime(n.createdAt)}</span>93                {n.link ? <Link href={n.link} className="truncate text-text-2 hover:text-text">{n.title}</Link> : <span className="truncate text-text-2">{n.title}</span>}94              </li>95            ))}96          </ul>97        )}98      </div>99    </div>100  );101}102103function MetricsTab() {104  const q = useSystem();105  if (q.isPending) return <div className="flex justify-center p-6"><Spinner /></div>;106  if (q.isError) return <ErrorState error={q.error} onRetry={() => q.refetch()} />;107  const s = q.data;108  return (109    <div className="flex flex-col gap-4 p-3">110      <div className="flex items-baseline justify-between text-xs">111        <span className="font-medium text-text">{s.host.name}</span>112        <span className="tnum text-[11px] text-muted">as of {fmtTime(s.collectedAt)}</span>113      </div>114      <HostMetrics host={s.host} />115      <dl className="tnum grid grid-cols-2 gap-x-4 gap-y-1 border-t border-line pt-3 text-xs">116        <dt className="text-text-2">Live sessions</dt><dd className="text-right">{s.counts.liveSessions}</dd>117        <dt className="text-text-2">WebSocket clients</dt><dd className="text-right">{s.counts.wsClients}</dd>118        <dt className="text-text-2">Running tasks</dt><dd className="text-right">{s.counts.runningTasks}</dd>119        <dt className="text-text-2">Queued tasks</dt><dd className="text-right">{s.counts.queuedTasks}</dd>120      </dl>121      <Link href="/system/" className="text-xs text-accent hover:underline">Open System</Link>122    </div>123  );124}125126function TaskTab() {127  const q = useTasks();128  const now = useNow(1000);129  if (q.isPending) return <div className="flex justify-center p-6"><Spinner /></div>;130  if (q.isError) return <ErrorState error={q.error} onRetry={() => q.refetch()} />;131  const list = [...q.data].sort((a, b) => Number(TASK_ACTIVE.includes(b.state)) - Number(TASK_ACTIVE.includes(a.state))).slice(0, 25);132  if (list.length === 0) return <EmptyState title="No tasks yet" description="Background tasks started from the Tasks page appear here." />;133  return (134    <ul className="p-1.5">135      {list.map((t) => (136        <li key={t.id}>137          <Link href={`/tasks/?t=${t.id}`} className="flex min-h-10 items-center gap-2 rounded-sm px-2 py-1.5 hover:bg-surface-2">138            <span className="min-w-0 flex-1">139              <span className="block truncate text-xs text-text">{t.title}</span>140              <span className="tnum block text-[11px] text-muted">{t.host} · {fmtRelative(t.startedAt ?? t.createdAt, now)}</span>141            </span>142            <TaskStateBadge state={t.state} />143          </Link>144        </li>145      ))}146    </ul>147  );148}149
Files main
Menu

Theme