Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function App() {

<AgentLane
agents={displayAgents}
status={status}
selectedId={selectedId}
searchQuery={searchQuery}
onSelect={select}
Expand Down
19 changes: 17 additions & 2 deletions public/src/components/AgentLane.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { AgentSnapshot } from '../types';
import type { AgentSnapshot, WsStatus } from '../types';
import { isServerKind } from '../lib/palette';
import { agentIdentity } from '../lib/format';
import { sortAgentsForLane } from '../lib/agents';
import { AgentListItem } from './AgentListItem';

const SKELETON_COUNT = 3;

interface AgentLaneProps {
agents: AgentSnapshot[];
status: WsStatus;
selectedId: string | null;
searchQuery: string;
onSelect: (agent: AgentSnapshot) => void;
Expand All @@ -14,6 +17,7 @@ interface AgentLaneProps {

export function AgentLane({
agents,
status,
selectedId,
searchQuery,
onSelect,
Expand All @@ -27,6 +31,7 @@ export function AgentLane({
);
const visibleAgents = agentNodes;
const visibleServers = serverNodes;
const isLoading = (status === 'connecting') && agentNodes.length === 0;

const agentTitle = searchQuery ? 'search results' : 'agents';
const serverTitle = searchQuery ? 'server results' : 'servers';
Expand All @@ -44,7 +49,17 @@ export function AgentLane({
/>

<div id="active-list">
{visibleAgents.length === 0 ? (
{isLoading ? (
Array.from({ length: SKELETON_COUNT }, (_, i) => (
<div key={i} className="lane-item is-skeleton" aria-hidden="true">
<div className="lane-pill idle" />
<div className="lane-copy">
<div className="lane-label lane-skel" />
<div className="lane-meta lane-skel lane-skel-short" />
</div>
</div>
))
) : visibleAgents.length === 0 ? (
<div className="lane-meta">No agents detected.</div>
) : (
visibleAgents.map((agent) => (
Expand Down
10 changes: 8 additions & 2 deletions public/src/components/AgentListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AgentSnapshot } from '../types';
import { agentIdentity, labelFor, truncate } from '../lib/format';
import { agentIdentity, labelFor, truncate, formatRelativeTime } from '../lib/format';
import { accentFor, accentSoftFor, cliForAgent } from '../lib/palette';

interface AgentListItemProps {
Expand All @@ -12,6 +12,12 @@ export function AgentListItem({ agent, isSelected, onClick }: AgentListItemProps
const doingRaw = agent.summary?.current || agent.doing || agent.cmdShort || '';
const doing = truncate(doingRaw, 80);
const label = labelFor(agent);
const eventCount = agent.events?.length ?? 0;
const relTime = formatRelativeTime(agent.lastActivityAt ?? agent.lastEventAt);
const meta = doing || [
eventCount > 0 ? `${eventCount} events` : null,
relTime,
].filter((x): x is string => x !== null).join(' · ');
const accent = accentFor(agent);
const accentGlow = accentSoftFor(agent);
const cli = cliForAgent(agent);
Expand All @@ -36,7 +42,7 @@ export function AgentListItem({ agent, isSelected, onClick }: AgentListItemProps
<div className={`lane-pill ${agent.state}`} />
<div className="lane-copy">
<div className="lane-label">{label}</div>
<div className="lane-meta">{doing}</div>
<div className="lane-meta">{meta}</div>
</div>
</button>
);
Expand Down
9 changes: 9 additions & 0 deletions public/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ export function formatDateFull(timestamp: number | undefined): string | null {
if (!timestamp) return null;
return new Date(timestamp * 1000).toLocaleString();
}

export function formatRelativeTime(ts: number | undefined): string | null {
if (!ts) return null;
const ageMs = Date.now() - ts;
if (ageMs < 60_000) return 'just now';
if (ageMs < 3_600_000) return `${Math.floor(ageMs / 60_000)}m ago`;
if (ageMs < 86_400_000) return `${Math.floor(ageMs / 3_600_000)}h ago`;
return `${Math.floor(ageMs / 86_400_000)}d ago`;
}
29 changes: 29 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,35 @@ body {
}
}

.lane-item.is-skeleton {
pointer-events: none;
opacity: 0.5;
}

.lane-skel {
border-radius: 4px;
background: rgba(62, 78, 89, 0.5);
animation: laneSkelPulse 1.5s ease-in-out infinite;
height: 13px;
width: 75%;
}

.lane-skel-short {
width: 50%;
height: 11px;
}

@keyframes laneSkelPulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.7; }
}

@media (prefers-reduced-motion: reduce) {
.lane-skel {
animation: none;
}
}

.lane-copy {
display: flex;
flex-direction: column;
Expand Down
8 changes: 8 additions & 0 deletions src/codexLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ export function getTailState(sessionPath: string): TailState | undefined {
return tailStates.get(sessionPath);
}

export function evictStaleTails(activePaths: Set<string>): void {
for (const key of tailStates.keys()) {
if (!activePaths.has(key)) {
tailStates.delete(key);
}
}
}

export function consumeRecentEvents(sessionPath: string): CodexEventLite[] {
const state = tailStates.get(sessionPath);
if (!state?.recentEvents || state.recentEvents.length === 0) return [];
Expand Down
2 changes: 2 additions & 0 deletions src/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
updateTail,
getTailState,
getSessionStartMsFromPath,
evictStaleTails,
} from "./codexLogs.js";
import {
deriveCodexSessionIdentity,
Expand Down Expand Up @@ -2478,6 +2479,7 @@ export async function scanCodexProcesses(options: ScanOptions = {}): Promise<Sna
activityCache.delete(id);
}
}
evictStaleTails(tailTargets);
}
for (const pid of pidSessionCache.keys()) {
if (!codexPidSet.has(pid)) {
Expand Down
Loading