story-kit: start 73_story_fade_out_completed_agents
This commit is contained in:
@@ -16,6 +16,7 @@ interface AgentState {
|
||||
sessionId: string | null;
|
||||
worktreePath: string | null;
|
||||
baseBranch: string | null;
|
||||
terminalAt: number | null;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<AgentStatusValue, string> = {
|
||||
@@ -283,6 +284,8 @@ export function EditorCommand({
|
||||
);
|
||||
}
|
||||
|
||||
const FADE_DURATION_MS = 60_000;
|
||||
|
||||
export function AgentPanel() {
|
||||
const [agents, setAgents] = useState<Record<string, AgentState>>({});
|
||||
const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
|
||||
@@ -294,6 +297,10 @@ export function AgentPanel() {
|
||||
const [editingEditor, setEditingEditor] = useState(false);
|
||||
const cleanupRefs = useRef<Record<string, () => void>>({});
|
||||
const logEndRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
// Refs for fade-out timers (pause/resume on expand/collapse)
|
||||
const fadeTimerRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
||||
const fadeElapsedRef = useRef<Record<string, number>>({});
|
||||
const fadeTimerStartRef = useRef<Record<string, number | null>>({});
|
||||
|
||||
// Load roster, existing agents, and editor preference on mount
|
||||
useEffect(() => {
|
||||
@@ -306,8 +313,11 @@ export function AgentPanel() {
|
||||
.listAgents()
|
||||
.then((agentList) => {
|
||||
const agentMap: Record<string, AgentState> = {};
|
||||
const now = Date.now();
|
||||
for (const a of agentList) {
|
||||
const key = agentKey(a.story_id, a.agent_name);
|
||||
const isTerminal =
|
||||
a.status === "completed" || a.status === "failed";
|
||||
agentMap[key] = {
|
||||
agentName: a.agent_name,
|
||||
status: a.status,
|
||||
@@ -315,6 +325,7 @@ export function AgentPanel() {
|
||||
sessionId: a.session_id,
|
||||
worktreePath: a.worktree_path,
|
||||
baseBranch: a.base_branch,
|
||||
terminalAt: isTerminal ? now : null,
|
||||
};
|
||||
if (a.status === "running" || a.status === "pending") {
|
||||
subscribeToAgent(a.story_id, a.agent_name);
|
||||
@@ -357,17 +368,26 @@ export function AgentPanel() {
|
||||
sessionId: null,
|
||||
worktreePath: null,
|
||||
baseBranch: null,
|
||||
terminalAt: null,
|
||||
};
|
||||
|
||||
switch (event.type) {
|
||||
case "status":
|
||||
case "status": {
|
||||
const newStatus =
|
||||
(event.status as AgentStatusValue) ?? current.status;
|
||||
const isTerminal =
|
||||
newStatus === "completed" || newStatus === "failed";
|
||||
return {
|
||||
...prev,
|
||||
[key]: {
|
||||
...current,
|
||||
status: (event.status as AgentStatusValue) ?? current.status,
|
||||
status: newStatus,
|
||||
terminalAt: isTerminal
|
||||
? (current.terminalAt ?? Date.now())
|
||||
: current.terminalAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "output":
|
||||
return {
|
||||
...prev,
|
||||
@@ -383,6 +403,7 @@ export function AgentPanel() {
|
||||
...current,
|
||||
status: "completed",
|
||||
sessionId: event.session_id ?? current.sessionId,
|
||||
terminalAt: current.terminalAt ?? Date.now(),
|
||||
},
|
||||
};
|
||||
case "error":
|
||||
@@ -395,6 +416,7 @@ export function AgentPanel() {
|
||||
...current.log,
|
||||
`[ERROR] ${event.message ?? "Unknown error"}`,
|
||||
],
|
||||
terminalAt: current.terminalAt ?? Date.now(),
|
||||
},
|
||||
};
|
||||
default:
|
||||
@@ -418,6 +440,53 @@ export function AgentPanel() {
|
||||
}
|
||||
}, [expandedKey, agents]);
|
||||
|
||||
// Manage fade-out timers for terminal agents.
|
||||
// Timers are paused when an entry is expanded and resumed on collapse.
|
||||
useEffect(() => {
|
||||
for (const [key, agent] of Object.entries(agents)) {
|
||||
if (!agent.terminalAt) continue;
|
||||
const isExpanded = expandedKey === key;
|
||||
const hasTimer = key in fadeTimerRef.current;
|
||||
|
||||
if (isExpanded && hasTimer) {
|
||||
// Pause: clear timer and accumulate elapsed time
|
||||
clearTimeout(fadeTimerRef.current[key]);
|
||||
const started = fadeTimerStartRef.current[key];
|
||||
if (started !== null && started !== undefined) {
|
||||
fadeElapsedRef.current[key] =
|
||||
(fadeElapsedRef.current[key] ?? 0) + (Date.now() - started);
|
||||
}
|
||||
delete fadeTimerRef.current[key];
|
||||
fadeTimerStartRef.current[key] = null;
|
||||
} else if (!isExpanded && !hasTimer) {
|
||||
// Start or resume timer with remaining time
|
||||
const elapsed = fadeElapsedRef.current[key] ?? 0;
|
||||
const remaining = Math.max(0, FADE_DURATION_MS - elapsed);
|
||||
fadeTimerStartRef.current[key] = Date.now();
|
||||
fadeTimerRef.current[key] = setTimeout(() => {
|
||||
setAgents((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
return next;
|
||||
});
|
||||
delete fadeTimerRef.current[key];
|
||||
delete fadeTimerStartRef.current[key];
|
||||
delete fadeElapsedRef.current[key];
|
||||
}, remaining);
|
||||
}
|
||||
}
|
||||
}, [agents, expandedKey]);
|
||||
|
||||
// Clean up fade timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
for (const timer of Object.values(fadeTimerRef.current)) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
fadeTimerRef.current = {};
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStop = async (storyId: string, agentName: string) => {
|
||||
setActionError(null);
|
||||
const key = agentKey(storyId, agentName);
|
||||
@@ -626,11 +695,22 @@ export function AgentPanel() {
|
||||
{Object.entries(agents).map(([key, a]) => (
|
||||
<div
|
||||
key={`agent-${key}`}
|
||||
data-testid={`agent-entry-${key}`}
|
||||
style={{
|
||||
border: "1px solid #2a2a2a",
|
||||
borderRadius: "8px",
|
||||
background: "#191919",
|
||||
overflow: "hidden",
|
||||
...(a.terminalAt
|
||||
? {
|
||||
animationName: "agentFadeOut",
|
||||
animationDuration: "60s",
|
||||
animationTimingFunction: "linear",
|
||||
animationFillMode: "forwards",
|
||||
animationPlayState:
|
||||
expandedKey === key ? "paused" : "running",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user