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