mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-25 20:33:58 +02:00
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:
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AppShell } from '@renderer/components/AppShell';
|
import { AppShell } from '@renderer/components/AppShell';
|
||||||
|
import { ActivityPanel } from '@renderer/components/ActivityPanel';
|
||||||
import { ChatPane } from '@renderer/components/ChatPane';
|
import { ChatPane } from '@renderer/components/ChatPane';
|
||||||
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
import { NewSessionModal } from '@renderer/components/NewSessionModal';
|
||||||
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
import { SettingsPanel } from '@renderer/components/SettingsPanel';
|
||||||
@@ -117,6 +118,7 @@ export default function App() {
|
|||||||
|
|
||||||
// Determine main content
|
// Determine main content
|
||||||
let content: React.ReactNode;
|
let content: React.ReactNode;
|
||||||
|
let detailPanel: React.ReactNode | undefined;
|
||||||
if (error) {
|
if (error) {
|
||||||
content = (
|
content = (
|
||||||
<div className="flex h-full items-center justify-center px-8">
|
<div className="flex h-full items-center justify-center px-8">
|
||||||
@@ -129,13 +131,19 @@ export default function App() {
|
|||||||
} else if (selectedSession && patternForSession && projectForSession) {
|
} else if (selectedSession && patternForSession && projectForSession) {
|
||||||
content = (
|
content = (
|
||||||
<ChatPane
|
<ChatPane
|
||||||
activity={activityForSession}
|
|
||||||
onSend={(c) => api.sendSessionMessage({ sessionId: selectedSession.id, content: c })}
|
onSend={(c) => api.sendSessionMessage({ sessionId: selectedSession.id, content: c })}
|
||||||
pattern={patternForSession}
|
pattern={patternForSession}
|
||||||
project={projectForSession}
|
project={projectForSession}
|
||||||
session={selectedSession}
|
session={selectedSession}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
detailPanel = (
|
||||||
|
<ActivityPanel
|
||||||
|
activity={activityForSession}
|
||||||
|
pattern={patternForSession}
|
||||||
|
session={selectedSession}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<WelcomePane
|
<WelcomePane
|
||||||
@@ -166,6 +174,7 @@ export default function App() {
|
|||||||
<>
|
<>
|
||||||
<AppShell
|
<AppShell
|
||||||
content={content}
|
content={content}
|
||||||
|
detailPanel={detailPanel}
|
||||||
overlay={overlay}
|
overlay={overlay}
|
||||||
sidebar={
|
sidebar={
|
||||||
<Sidebar
|
<Sidebar
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ import type { ReactNode } from 'react';
|
|||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
sidebar: ReactNode;
|
sidebar: ReactNode;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
|
detailPanel?: ReactNode;
|
||||||
overlay?: ReactNode;
|
overlay?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppShell({ sidebar, content, overlay }: AppShellProps) {
|
export function AppShell({ sidebar, content, detailPanel, overlay }: AppShellProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen bg-[var(--color-surface-0)] text-zinc-100">
|
<div className="relative flex h-screen bg-[var(--color-surface-0)] text-zinc-100">
|
||||||
{/* Full-width drag region matching the title bar overlay height */}
|
{/* Full-width drag region matching the title bar overlay height */}
|
||||||
@@ -16,6 +17,11 @@ export function AppShell({ sidebar, content, overlay }: AppShellProps) {
|
|||||||
{sidebar}
|
{sidebar}
|
||||||
</aside>
|
</aside>
|
||||||
<main className="relative min-w-0 flex-1">{content}</main>
|
<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}
|
{overlay}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { 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 { PatternDefinition } from '@shared/domain/pattern';
|
||||||
import type { ProjectRecord } from '@shared/domain/project';
|
import type { ProjectRecord } from '@shared/domain/project';
|
||||||
import type { SessionRecord } from '@shared/domain/session';
|
import type { SessionRecord } from '@shared/domain/session';
|
||||||
@@ -23,25 +16,18 @@ function ThinkingDots() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ChatPaneProps {
|
interface ChatPaneProps {
|
||||||
activity?: SessionActivityState;
|
|
||||||
project: ProjectRecord;
|
project: ProjectRecord;
|
||||||
pattern: PatternDefinition;
|
pattern: PatternDefinition;
|
||||||
session: SessionRecord;
|
session: SessionRecord;
|
||||||
onSend: (content: string) => Promise<void>;
|
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 [input, setInput] = useState('');
|
||||||
const transcriptRef = useRef<HTMLDivElement>(null);
|
const transcriptRef = useRef<HTMLDivElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const isBusy = session.status === 'running';
|
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(() => {
|
useEffect(() => {
|
||||||
transcriptRef.current?.scrollTo({
|
transcriptRef.current?.scrollTo({
|
||||||
@@ -139,35 +125,6 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user