mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-29 22:17:13 +02:00
Merge branch 'copilot/precious-seahorse'
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Bot, CheckCircle2, Loader2, XCircle } from 'lucide-react';
|
||||
|
||||
import type { ActiveSubagent } from '@renderer/lib/subagentTracker';
|
||||
|
||||
const COMPLETION_GRACE_MS = 3000;
|
||||
|
||||
function formatElapsed(startedAt: string): string {
|
||||
const seconds = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
@@ -37,9 +39,10 @@ function ElapsedTimer({ startedAt }: { startedAt: string }) {
|
||||
|
||||
interface SubagentActivityCardProps {
|
||||
subagent: ActiveSubagent;
|
||||
fading?: boolean;
|
||||
}
|
||||
|
||||
function SubagentActivityCard({ subagent }: SubagentActivityCardProps) {
|
||||
function SubagentActivityCard({ subagent, fading }: SubagentActivityCardProps) {
|
||||
const borderClass =
|
||||
subagent.status === 'running'
|
||||
? 'border-[var(--color-accent-sky)]/20'
|
||||
@@ -49,7 +52,7 @@ function SubagentActivityCard({ subagent }: SubagentActivityCardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-lg border bg-[var(--color-glass)] px-3 py-1.5 transition-all duration-200 ${borderClass}`}
|
||||
className={`flex items-center gap-2 rounded-lg border bg-[var(--color-glass)] px-3 py-1.5 transition-all duration-300 ${borderClass} ${fading ? 'opacity-0' : 'opacity-100'}`}
|
||||
role="status"
|
||||
aria-label={`Sub-agent ${subagent.name}: ${subagent.activityLabel}`}
|
||||
>
|
||||
@@ -73,16 +76,68 @@ interface SubagentActivityListProps {
|
||||
}
|
||||
|
||||
export function SubagentActivityList({ subagents }: SubagentActivityListProps) {
|
||||
// Track recently-completed subagent IDs so we can show them briefly
|
||||
const [recentlyDone, setRecentlyDone] = useState<Set<string>>(new Set());
|
||||
const [fading, setFading] = useState<Set<string>>(new Set());
|
||||
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
for (const sub of subagents) {
|
||||
if (sub.status !== 'running' && !recentlyDone.has(sub.toolCallId) && !timersRef.current.has(sub.toolCallId)) {
|
||||
// Newly completed — track it
|
||||
setRecentlyDone((prev) => new Set(prev).add(sub.toolCallId));
|
||||
|
||||
const fadeTimer = setTimeout(() => {
|
||||
setFading((prev) => new Set(prev).add(sub.toolCallId));
|
||||
}, COMPLETION_GRACE_MS - 300);
|
||||
|
||||
const removeTimer = setTimeout(() => {
|
||||
setRecentlyDone((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(sub.toolCallId);
|
||||
return next;
|
||||
});
|
||||
setFading((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(sub.toolCallId);
|
||||
return next;
|
||||
});
|
||||
timersRef.current.delete(sub.toolCallId);
|
||||
}, COMPLETION_GRACE_MS);
|
||||
|
||||
timersRef.current.set(sub.toolCallId, removeTimer);
|
||||
// Store fade timer for cleanup
|
||||
timersRef.current.set(`fade-${sub.toolCallId}`, fadeTimer);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subagents]);
|
||||
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current;
|
||||
return () => {
|
||||
for (const timer of timers.values()) clearTimeout(timer);
|
||||
timers.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (subagents.length === 0) return null;
|
||||
|
||||
// Only show running subagents in the chat stream
|
||||
const visible = subagents.filter((s) => s.status === 'running');
|
||||
// Show running subagents + recently-completed ones within the grace period
|
||||
const visible = subagents.filter(
|
||||
(s) => s.status === 'running' || recentlyDone.has(s.toolCallId),
|
||||
);
|
||||
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} />
|
||||
<SubagentActivityCard
|
||||
key={subagent.toolCallId}
|
||||
subagent={subagent}
|
||||
fading={fading.has(subagent.toolCallId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Brain, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
import { useElapsedTimer } from '@renderer/hooks/useElapsedTimer';
|
||||
import type { ChatMessageRecord } from '@shared/domain/session';
|
||||
|
||||
interface ThinkingProcessProps {
|
||||
@@ -26,24 +27,22 @@ export function ThinkingProcess({ messages, isActive, turnStartedAt }: ThinkingP
|
||||
|
||||
const toggle = useCallback(() => setExpanded((prev) => !prev), []);
|
||||
|
||||
const elapsed = useMemo(() => {
|
||||
if (!turnStartedAt || messages.length === 0) return undefined;
|
||||
const start = new Date(turnStartedAt).getTime();
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const end = isActive ? Date.now() : new Date(lastMessage.createdAt).getTime();
|
||||
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
||||
if (seconds < 2) return undefined;
|
||||
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
||||
}, [turnStartedAt, messages, isActive]);
|
||||
const elapsed = useElapsedTimer(
|
||||
messages.length > 0 ? turnStartedAt : undefined,
|
||||
isActive,
|
||||
);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepCount = messages.length;
|
||||
// Don't count empty pending messages (optimistically folded in) toward step count
|
||||
const stepCount = messages.filter((m) => m.content).length;
|
||||
const summaryParts: string[] = [];
|
||||
if (elapsed) summaryParts.push(`${elapsed}`);
|
||||
summaryParts.push(`${stepCount} ${stepCount === 1 ? 'step' : 'steps'}`);
|
||||
if (stepCount > 0) {
|
||||
summaryParts.push(`${stepCount} ${stepCount === 1 ? 'step' : 'steps'}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="thinking-process-enter mb-2 overflow-hidden rounded-lg border border-[var(--color-border)]/50 bg-[var(--color-surface-1)]/60">
|
||||
@@ -87,6 +86,11 @@ export function ThinkingProcess({ messages, isActive, turnStartedAt }: ThinkingP
|
||||
function ThinkingStep({ message }: { message: ChatMessageRecord }) {
|
||||
const preview = useMemo(() => truncatePreview(message.content, 180), [message.content]);
|
||||
|
||||
// Pending message folded into thinking group with no content yet
|
||||
if (message.pending && !message.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 text-[12px] leading-relaxed">
|
||||
<span className="mt-0.5 shrink-0 text-[var(--color-text-muted)]">▸</span>
|
||||
|
||||
Reference in New Issue
Block a user