agent-ecosystem/src/renderer/components/team/ProcessesSection.tsx
iliya 1498350cb1 feat: enhance React component data handling and improve task management features
- Updated `ProcessesSection` to directly access data from Zustand store, eliminating prop drilling for cleaner component structure.
- Added new guidelines in `react.md` for accessing data from the store over props to streamline component communication.
- Enhanced `TeamProvisioningService` to include task snapshots for team members, improving task management and visibility.
- Implemented notifications for killed processes, providing better feedback to team leads about process management actions.
- Refactored `TaskDetailDialog` and `GlobalTaskDetailDialog` to improve header content handling, enhancing user experience.
- Introduced folder collapsing functionality in `ReviewFileTree`, allowing for better navigation and organization of file structures.
2026-02-25 18:07:03 +02:00

131 lines
5 KiB
TypeScript

import { useStore } from '@renderer/store';
import { formatDistanceToNowStrict } from 'date-fns';
import { ExternalLink, Square, Terminal } from 'lucide-react';
import { MemberBadge } from './MemberBadge';
function formatShortTime(date: Date): string {
const distance = formatDistanceToNowStrict(date, { addSuffix: false });
return distance
.replace(' seconds', 's')
.replace(' second', 's')
.replace(' minutes', 'm')
.replace(' minute', 'm')
.replace(' hours', 'h')
.replace(' hour', 'h')
.replace(' days', 'd')
.replace(' day', 'd')
.replace(' weeks', 'w')
.replace(' week', 'w')
.replace(' months', 'mo')
.replace(' month', 'mo')
.replace(' years', 'y')
.replace(' year', 'y');
}
export const ProcessesSection = (): React.JSX.Element | null => {
const teamName = useStore((s) => s.selectedTeamName);
const data = useStore((s) => s.selectedTeamData);
if (!teamName || !data?.processes?.length) return null;
const memberColorMap = new Map(data.members.map((m) => [m.name, m.color]));
const sorted = [...data.processes].sort((a, b) => {
const aAlive = !a.stoppedAt;
const bAlive = !b.stoppedAt;
if (aAlive !== bAlive) return aAlive ? -1 : 1;
return Date.parse(b.registeredAt) - Date.parse(a.registeredAt);
});
return (
<div className="space-y-0.5">
{sorted.map((proc) => {
const alive = !proc.stoppedAt;
const timeStr = alive
? `${formatShortTime(new Date(proc.registeredAt))} ago`
: `stopped ${formatShortTime(new Date(proc.stoppedAt!))} ago`;
return (
<div
key={proc.id}
className={`flex items-center gap-2 rounded px-2 py-1.5 text-xs transition-colors hover:bg-[var(--color-surface-raised)] ${!alive ? 'opacity-50' : ''}`}
>
{/* Status indicator */}
<span
className="relative inline-flex size-2 shrink-0"
title={alive ? 'Running' : 'Stopped'}
>
{alive && (
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-50" />
)}
<span
className={`relative inline-flex size-2 rounded-full ${alive ? 'bg-emerald-400' : 'bg-zinc-500'}`}
/>
</span>
{/* Icon + label — takes available space */}
<Terminal size={12} className="shrink-0 text-[var(--color-text-muted)]" />
<span
className="min-w-0 truncate font-medium text-[var(--color-text)]"
title={proc.label}
>
{proc.label}
</span>
{/* Port + URL inline — only when present */}
{(proc.port != null || proc.url) && (
<span className="min-w-0 truncate text-[var(--color-text-secondary)]">
{proc.port != null && !proc.url && `:${proc.port}`}
{proc.url && (
<button
type="button"
className="text-[var(--color-text-secondary)] underline decoration-dotted underline-offset-2 transition-colors hover:text-blue-400"
onClick={() => void window.electronAPI.openExternal(proc.url!)}
title={proc.url}
>
{proc.url}
</button>
)}
</span>
)}
{/* Right-aligned group: Kill button, Open button, member badge, PID, time */}
<span className="ml-auto flex shrink-0 items-center gap-2">
{alive && (
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-red-400 transition-colors hover:bg-red-500/10"
onClick={() => void window.electronAPI.teams.killProcess(teamName, proc.pid)}
title="Stop process (SIGTERM)"
>
<Square size={8} className="fill-current" />
Kill
</button>
)}
{alive && proc.url && (
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-blue-400 transition-colors hover:bg-blue-500/10"
onClick={() => void window.electronAPI.openExternal(proc.url!)}
title="Open in browser"
>
<ExternalLink size={10} />
Open
</button>
)}
<span className="font-mono text-[var(--color-text-muted)]">PID{proc.pid}</span>
{proc.registeredBy && (
<MemberBadge
name={proc.registeredBy}
color={memberColorMap.get(proc.registeredBy)}
/>
)}
<span className="text-[var(--color-text-muted)]">{timeStr}</span>
</span>
</div>
);
})}
</div>
);
};