mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: show per-agent activity statuses
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -310,6 +310,7 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
kind: 'agent-activity',
|
||||
occurredAt: nowIso(),
|
||||
activityType: event.activityType,
|
||||
agentId: event.agentId,
|
||||
agentName: event.agentName,
|
||||
toolName: event.toolName,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { type KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react';
|
||||
|
||||
import {
|
||||
formatSessionActivityLabel,
|
||||
shouldAnimateSessionActivity,
|
||||
buildAgentActivityRows,
|
||||
formatAgentActivityLabel,
|
||||
isAgentActivityActive,
|
||||
isAgentActivityCompleted,
|
||||
type SessionActivityState,
|
||||
} from '@renderer/lib/sessionActivity';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
@@ -34,10 +36,10 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const isBusy = session.status === 'running';
|
||||
const hasPendingMessage = session.messages.some((m) => m.pending);
|
||||
const isThinking = isBusy && !hasPendingMessage;
|
||||
const activityLabel = formatSessionActivityLabel(activity, pattern.agents[0]?.name ?? 'Agent');
|
||||
const showActivityAnimation = shouldAnimateSessionActivity(activity);
|
||||
const activityRows = useMemo(
|
||||
() => buildAgentActivityRows(activity, pattern.agents, isBusy),
|
||||
[activity, isBusy, pattern.agents],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current?.scrollTo({
|
||||
@@ -136,17 +138,31 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Activity indicator — shown while the agent is thinking (before streaming starts) */}
|
||||
{isThinking && (
|
||||
<div className="py-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-zinc-800 text-zinc-400">
|
||||
<Bot className="size-3.5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1.5 text-[12px] font-medium text-zinc-500">{activityLabel}</div>
|
||||
{showActivityAnimation && <ThinkingDots />}
|
||||
</div>
|
||||
{isBusy && activityRows.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
|
||||
export interface SessionActivityState {
|
||||
sessionId: string;
|
||||
export interface AgentActivityState {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
activityType?: SessionEventRecord['activityType'];
|
||||
agentName?: string;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
export type SessionActivityState = Record<string, AgentActivityState>;
|
||||
export type SessionActivityMap = Record<string, SessionActivityState | undefined>;
|
||||
|
||||
export interface AgentActivityRow {
|
||||
key: string;
|
||||
agentName: string;
|
||||
activity?: AgentActivityState;
|
||||
}
|
||||
|
||||
export function applySessionEventActivity(
|
||||
current: SessionActivityMap,
|
||||
event: SessionEventRecord,
|
||||
): SessionActivityMap {
|
||||
if (event.kind === 'agent-activity') {
|
||||
const agentKey = resolveAgentKey(event);
|
||||
if (!agentKey) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
[event.sessionId]: {
|
||||
sessionId: event.sessionId,
|
||||
activityType: event.activityType,
|
||||
agentName: event.agentName,
|
||||
toolName: event.toolName,
|
||||
...(current[event.sessionId] ?? {}),
|
||||
[agentKey]: {
|
||||
agentId: event.agentId ?? agentKey,
|
||||
agentName: event.agentName?.trim() || event.agentId?.trim() || agentKey,
|
||||
activityType: event.activityType,
|
||||
toolName: event.toolName,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -58,27 +74,72 @@ export function pruneSessionActivities(
|
||||
return changed || Object.keys(next).length !== Object.keys(current).length ? next : current;
|
||||
}
|
||||
|
||||
export function formatSessionActivityLabel(
|
||||
activity: SessionActivityState | undefined,
|
||||
fallbackAgentName = 'Agent',
|
||||
): string {
|
||||
const agentName = activity?.agentName?.trim() || fallbackAgentName;
|
||||
export function buildAgentActivityRows(
|
||||
current: SessionActivityState | undefined,
|
||||
agents: PatternDefinition['agents'],
|
||||
isBusy: boolean,
|
||||
): AgentActivityRow[] {
|
||||
const hasReportedActivity = !!current && Object.keys(current).length > 0;
|
||||
|
||||
return agents.map((agent, index) => {
|
||||
const activity = current?.[agent.id] ?? current?.[agent.name];
|
||||
|
||||
if (activity) {
|
||||
return {
|
||||
key: agent.id,
|
||||
agentName: agent.name,
|
||||
activity,
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasReportedActivity && isBusy && index === 0) {
|
||||
return {
|
||||
key: agent.id,
|
||||
agentName: agent.name,
|
||||
activity: {
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
activityType: 'thinking',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key: agent.id,
|
||||
agentName: agent.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function formatAgentActivityLabel(activity: AgentActivityState | undefined): string {
|
||||
if (!activity) {
|
||||
return 'Waiting…';
|
||||
}
|
||||
|
||||
switch (activity?.activityType) {
|
||||
case 'tool-calling':
|
||||
return `${agentName} is using ${activity.toolName?.trim() || 'a tool'}…`;
|
||||
return `Using ${activity.toolName?.trim() || 'a tool'}…`;
|
||||
case 'handoff':
|
||||
return `Handing off to ${agentName}…`;
|
||||
return 'Handling handoff…';
|
||||
case 'completed':
|
||||
return `${agentName} completed their turn.`;
|
||||
return 'Completed';
|
||||
case 'thinking':
|
||||
return 'Thinking…';
|
||||
default:
|
||||
return `${agentName} is thinking…`;
|
||||
return 'Waiting…';
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAnimateSessionActivity(activity: SessionActivityState | undefined): boolean {
|
||||
return activity?.activityType !== 'completed';
|
||||
export function isAgentActivityActive(activity: AgentActivityState | undefined): boolean {
|
||||
return (
|
||||
activity?.activityType === 'thinking'
|
||||
|| activity?.activityType === 'tool-calling'
|
||||
|| activity?.activityType === 'handoff'
|
||||
);
|
||||
}
|
||||
|
||||
export function isAgentActivityCompleted(activity: AgentActivityState | undefined): boolean {
|
||||
return activity?.activityType === 'completed';
|
||||
}
|
||||
|
||||
function removeSessionActivity(
|
||||
@@ -93,3 +154,7 @@ function removeSessionActivity(
|
||||
delete next[sessionId];
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveAgentKey(event: SessionEventRecord): string | undefined {
|
||||
return event.agentId?.trim() || event.agentName?.trim();
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface AgentActivityEvent {
|
||||
requestId: string;
|
||||
sessionId: string;
|
||||
activityType: AgentActivityType;
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface SessionEventRecord {
|
||||
authorName?: string;
|
||||
contentDelta?: string;
|
||||
activityType?: SessionActivityType;
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
toolName?: string;
|
||||
error?: string;
|
||||
|
||||
Reference in New Issue
Block a user