mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: show sub-agent activity cards in chat stream
Display compact status cards in the chat transcript showing each sub-agent's name, current activity (Thinking, Using grep, etc.), and elapsed time. Cards appear while sub-agents are running and clear when the session goes idle. - Extend SessionEventRecord with description, error, toolCallId, and model fields for subagent events - Forward additional subagent fields in handleTurnScopedEvent - Add subagentTracker reducer to derive active subagent state from session events, including agent-activity correlation - Create SubagentActivityCard component with spinner, status icon, agent name, activity label, and live elapsed timer - Wire activeSubagents state in App.tsx and pass to ChatPane - Add 16 unit tests for the subagent tracker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -10,12 +10,14 @@ import { UserInputBanner } from '@renderer/components/chat/UserInputBanner';
|
||||
import { InlineApprovalPill, InlineModelPill, InlineTerminalPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills';
|
||||
import { InlinePromptPill } from '@renderer/components/chat/InlinePromptPill';
|
||||
import { ThinkingDots } from '@renderer/components/chat/ThinkingDots';
|
||||
import { SubagentActivityList } from '@renderer/components/chat/SubagentActivityCard';
|
||||
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
||||
import type { ApprovalDecision } from '@shared/domain/approval';
|
||||
import type { InteractionMode, MessageMode } from '@shared/contracts/sidecar';
|
||||
import type { ChatMessageAttachment } from '@shared/domain/attachment';
|
||||
import { getAttachmentDisplayName, isImageAttachment } from '@shared/domain/attachment';
|
||||
import type { SessionUsageState } from '@renderer/lib/sessionActivity';
|
||||
import type { ActiveSubagent } from '@renderer/lib/subagentTracker';
|
||||
import {
|
||||
findModel,
|
||||
getSupportedReasoningEfforts,
|
||||
@@ -44,6 +46,7 @@ interface ChatPaneProps {
|
||||
mcpProbingServerIds?: string[];
|
||||
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
|
||||
sessionUsage?: SessionUsageState;
|
||||
activeSubagents?: ReadonlyArray<ActiveSubagent>;
|
||||
terminalOpen?: boolean;
|
||||
terminalRunning?: boolean;
|
||||
onSend: (content: string, attachments?: ChatMessageAttachment[], messageMode?: MessageMode) => Promise<void>;
|
||||
@@ -72,6 +75,7 @@ export function ChatPane({
|
||||
mcpProbingServerIds,
|
||||
runtimeTools,
|
||||
sessionUsage,
|
||||
activeSubagents,
|
||||
terminalOpen,
|
||||
terminalRunning,
|
||||
onSend,
|
||||
@@ -409,6 +413,11 @@ export function ChatPane({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{activeSubagents && activeSubagents.length > 0 && (
|
||||
<div className="px-6 py-1">
|
||||
<SubagentActivityList subagents={activeSubagents} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Bot, CheckCircle2, Loader2, XCircle } from 'lucide-react';
|
||||
|
||||
import type { ActiveSubagent } from '@renderer/lib/subagentTracker';
|
||||
|
||||
function formatElapsed(startedAt: string): string {
|
||||
const seconds = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainder = seconds % 60;
|
||||
return `${minutes}m ${remainder}s`;
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status: ActiveSubagent['status'] }) {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Loader2 className="size-3.5 animate-spin text-sky-400" aria-label="Running" />;
|
||||
case 'completed':
|
||||
return <CheckCircle2 className="size-3.5 text-emerald-400" aria-label="Completed" />;
|
||||
case 'failed':
|
||||
return <XCircle className="size-3.5 text-red-400" aria-label="Failed" />;
|
||||
}
|
||||
}
|
||||
|
||||
function ElapsedTimer({ startedAt }: { startedAt: string }) {
|
||||
const [elapsed, setElapsed] = useState(() => formatElapsed(startedAt));
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setElapsed(formatElapsed(startedAt)), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [startedAt]);
|
||||
|
||||
return (
|
||||
<span className="ml-auto shrink-0 text-[10px] tabular-nums text-zinc-600">{elapsed}</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface SubagentActivityCardProps {
|
||||
subagent: ActiveSubagent;
|
||||
}
|
||||
|
||||
function SubagentActivityCard({ subagent }: SubagentActivityCardProps) {
|
||||
const borderClass =
|
||||
subagent.status === 'running'
|
||||
? 'border-sky-500/20'
|
||||
: subagent.status === 'failed'
|
||||
? 'border-red-500/20'
|
||||
: 'border-emerald-500/20';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-lg border bg-zinc-900/60 px-3 py-1.5 ${borderClass}`}
|
||||
role="status"
|
||||
aria-label={`Sub-agent ${subagent.name}: ${subagent.activityLabel}`}
|
||||
>
|
||||
<StatusIcon status={subagent.status} />
|
||||
<Bot className="size-3 text-zinc-500" />
|
||||
<span className="text-[11px] font-medium text-zinc-300">{subagent.name}</span>
|
||||
<span className="text-[10px] text-zinc-600">—</span>
|
||||
<span className="text-[10px] text-zinc-500">{subagent.activityLabel}</span>
|
||||
{subagent.status === 'running' && <ElapsedTimer startedAt={subagent.startedAt} />}
|
||||
{subagent.error && (
|
||||
<span className="truncate text-[10px] text-red-400" title={subagent.error}>
|
||||
{subagent.error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SubagentActivityListProps {
|
||||
subagents: ReadonlyArray<ActiveSubagent>;
|
||||
}
|
||||
|
||||
export function SubagentActivityList({ subagents }: SubagentActivityListProps) {
|
||||
if (subagents.length === 0) return null;
|
||||
|
||||
// Only show running subagents in the chat stream
|
||||
const visible = subagents.filter((s) => s.status === 'running');
|
||||
if (visible.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 py-1" aria-label="Active sub-agents">
|
||||
{visible.map((subagent) => (
|
||||
<SubagentActivityCard key={subagent.toolCallId} subagent={subagent} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user