feat: add right-side activity panel for agent status

- Create ActivityPanel component showing per-agent status with
  activity dots, agent names, and contextual status labels
- Add detailPanel slot to AppShell layout (right aside, w-64)
- Move agent activity display from inline ChatPane card to the
  dedicated side panel
- Remove activity prop and inline panel from ChatPane
- Panel appears when a session is selected, hidden on WelcomePane

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-21 18:34:05 +01:00
co-authored by Copilot
parent 867f2932df
commit 9138aa8819
4 changed files with 94 additions and 47 deletions
+10 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { AppShell } from '@renderer/components/AppShell';
import { ActivityPanel } from '@renderer/components/ActivityPanel';
import { ChatPane } from '@renderer/components/ChatPane';
import { NewSessionModal } from '@renderer/components/NewSessionModal';
import { SettingsPanel } from '@renderer/components/SettingsPanel';
@@ -117,6 +118,7 @@ export default function App() {
// Determine main content
let content: React.ReactNode;
let detailPanel: React.ReactNode | undefined;
if (error) {
content = (
<div className="flex h-full items-center justify-center px-8">
@@ -129,13 +131,19 @@ export default function App() {
} else if (selectedSession && patternForSession && projectForSession) {
content = (
<ChatPane
activity={activityForSession}
onSend={(c) => api.sendSessionMessage({ sessionId: selectedSession.id, content: c })}
pattern={patternForSession}
project={projectForSession}
session={selectedSession}
/>
);
detailPanel = (
<ActivityPanel
activity={activityForSession}
pattern={patternForSession}
session={selectedSession}
/>
);
} else {
content = (
<WelcomePane
@@ -166,6 +174,7 @@ export default function App() {
<>
<AppShell
content={content}
detailPanel={detailPanel}
overlay={overlay}
sidebar={
<Sidebar
+75
View File
@@ -0,0 +1,75 @@
import { useMemo } from 'react';
import { Activity } from 'lucide-react';
import {
buildAgentActivityRows,
formatAgentActivityLabel,
isAgentActivityActive,
isAgentActivityCompleted,
type SessionActivityState,
} from '@renderer/lib/sessionActivity';
import type { PatternDefinition } from '@shared/domain/pattern';
import type { SessionRecord } from '@shared/domain/session';
interface ActivityPanelProps {
activity?: SessionActivityState;
pattern: PatternDefinition;
session: SessionRecord;
}
export function ActivityPanel({ activity, pattern, session }: ActivityPanelProps) {
const activityRows = useMemo(
() => buildAgentActivityRows(activity, pattern.agents),
[activity, pattern.agents],
);
const isBusy = session.status === 'running';
return (
<div className="flex h-full flex-col">
{/* Header — top padding clears the title bar overlay zone */}
<div className="border-b border-[var(--color-border)] px-4 pb-3 pt-12">
<div className="flex items-center gap-2">
<Activity className="size-4 text-zinc-500" />
<span className="text-[12px] font-semibold uppercase tracking-[0.12em] text-zinc-400">
Activity
</span>
{isBusy && <span className="size-1.5 animate-pulse rounded-full bg-blue-400" />}
</div>
</div>
{/* Agent list */}
<div className="flex-1 overflow-y-auto px-4 py-3">
<div className="space-y-3">
{activityRows.map((row) => (
<div className="flex items-start gap-2.5" key={row.key}>
<span
className={`mt-1.5 size-2 shrink-0 rounded-full ${
isAgentActivityActive(row.activity)
? 'animate-pulse bg-blue-400'
: isAgentActivityCompleted(row.activity)
? 'bg-emerald-400'
: 'bg-zinc-700'
}`}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-[12px] font-medium text-zinc-300">
{row.agentName}
</div>
<div className="truncate text-[12px] text-zinc-500">
{formatAgentActivityLabel(row.activity)}
</div>
</div>
</div>
))}
</div>
{activityRows.length === 0 && (
<p className="py-6 text-center text-[12px] text-zinc-600">
No agents configured
</p>
)}
</div>
</div>
);
}
+7 -1
View File
@@ -3,10 +3,11 @@ import type { ReactNode } from 'react';
interface AppShellProps {
sidebar: ReactNode;
content: ReactNode;
detailPanel?: ReactNode;
overlay?: ReactNode;
}
export function AppShell({ sidebar, content, overlay }: AppShellProps) {
export function AppShell({ sidebar, content, detailPanel, overlay }: AppShellProps) {
return (
<div className="relative flex h-screen bg-[var(--color-surface-0)] text-zinc-100">
{/* Full-width drag region matching the title bar overlay height */}
@@ -16,6 +17,11 @@ export function AppShell({ sidebar, content, overlay }: AppShellProps) {
{sidebar}
</aside>
<main className="relative min-w-0 flex-1">{content}</main>
{detailPanel && (
<aside className="flex w-64 shrink-0 flex-col border-l border-[var(--color-border)] bg-[var(--color-surface-1)]">
{detailPanel}
</aside>
)}
{overlay}
</div>
);
+2 -45
View File
@@ -1,13 +1,6 @@
import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
import { type KeyboardEvent, useEffect, useRef, useState } from 'react';
import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react';
import {
buildAgentActivityRows,
formatAgentActivityLabel,
isAgentActivityActive,
isAgentActivityCompleted,
type SessionActivityState,
} from '@renderer/lib/sessionActivity';
import type { PatternDefinition } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
@@ -23,25 +16,18 @@ function ThinkingDots() {
}
interface ChatPaneProps {
activity?: SessionActivityState;
project: ProjectRecord;
pattern: PatternDefinition;
session: SessionRecord;
onSend: (content: string) => Promise<void>;
}
export function ChatPane({ activity, project, pattern, session, onSend }: ChatPaneProps) {
export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
const [input, setInput] = useState('');
const transcriptRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isBusy = session.status === 'running';
const activityRows = useMemo(
() => buildAgentActivityRows(activity, pattern.agents),
[activity, pattern.agents],
);
const hasObservedActivity = activityRows.some((row) => !!row.activity);
const showActivityPanel = (isBusy || hasObservedActivity) && activityRows.length > 0;
useEffect(() => {
transcriptRef.current?.scrollTo({
@@ -139,35 +125,6 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
);
})}
</div>
{showActivityPanel && (
<div className="mb-4 rounded-xl border border-zinc-800 bg-zinc-900/60 px-4 py-3">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-600">
Agent activity
</div>
<div className="space-y-2.5">
{activityRows.map((row) => (
<div className="flex items-start gap-3" key={row.key}>
<span
className={`mt-1.5 size-2 shrink-0 rounded-full ${
isAgentActivityActive(row.activity)
? 'animate-pulse bg-blue-400'
: isAgentActivityCompleted(row.activity)
? 'bg-emerald-400'
: 'bg-zinc-700'
}`}
/>
<div className="min-w-0 flex-1">
<div className="text-[12px] font-medium text-zinc-300">{row.agentName}</div>
<div className="text-[12px] text-zinc-500">
{formatAgentActivityLabel(row.activity)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>