From 20176d9d60036c2657f687b1e1849124005325ce Mon Sep 17 00:00:00 2001 From: Forgotten Date: Thu, 26 Feb 2026 10:32:51 -0600 Subject: [PATCH] fix(ui): resume lost runs, activity feed fixes, and selector focus Add resume button for process_lost runs on agent detail page. Fix activity row text overflow with truncation. Pass entityTitleMap to Dashboard activity feed. Fix InlineEntitySelector stealing focus on close when advancing to next field. Co-Authored-By: Claude Opus 4.6 --- ui/src/components/ActivityRow.tsx | 2 +- ui/src/components/InlineEntitySelector.tsx | 7 +++ ui/src/pages/AgentDetail.tsx | 59 ++++++++++++++++++++++ ui/src/pages/Dashboard.tsx | 7 +++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/ui/src/components/ActivityRow.tsx b/ui/src/components/ActivityRow.tsx index 8516ffc7..bf8258e6 100644 --- a/ui/src/components/ActivityRow.tsx +++ b/ui/src/components/ActivityRow.tsx @@ -108,7 +108,7 @@ export function ActivityRow({ event, agentMap, entityNameMap, entityTitleMap, cl const inner = (
-

+

(null); + const shouldPreventCloseAutoFocusRef = useRef(false); const allOptions = useMemo( () => [{ id: "", label: noneLabel, searchText: noneLabel }, ...options], @@ -70,6 +71,7 @@ export const InlineEntitySelector = forwardRef { const option = filteredOptions[index] ?? filteredOptions[0]; if (option) onChange(option.id); + shouldPreventCloseAutoFocusRef.current = moveNext; setOpen(false); setQuery(""); if (moveNext && onConfirm) { @@ -109,6 +111,11 @@ export const InlineEntitySelector = forwardRef { + if (!shouldPreventCloseAutoFocusRef.current) return; + event.preventDefault(); + shouldPreventCloseAutoFocusRef.current = false; + }} > | null { return value as Record; } +function asNonEmptyString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + export function AgentDetail() { const { agentId, tab: urlTab, runId: urlRunId } = useParams<{ agentId: string; tab?: string; runId?: string }>(); const { selectedCompanyId } = useCompany(); @@ -1509,6 +1515,7 @@ function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { run function RunDetail({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) { const queryClient = useQueryClient(); + const navigate = useNavigate(); const metrics = runMetrics(run); const [sessionOpen, setSessionOpen] = useState(false); const [claudeLoginResult, setClaudeLoginResult] = useState(null); @@ -1523,6 +1530,41 @@ function RunDetail({ run, adapterType }: { run: HeartbeatRun; adapterType: strin queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) }); }, }); + const canResumeLostRun = run.errorCode === "process_lost" && run.status === "failed"; + const resumePayload = useMemo(() => { + const payload: Record = { + resumeFromRunId: run.id, + }; + const context = asRecord(run.contextSnapshot); + if (!context) return payload; + const issueId = asNonEmptyString(context.issueId); + const taskId = asNonEmptyString(context.taskId); + const taskKey = asNonEmptyString(context.taskKey); + const commentId = asNonEmptyString(context.wakeCommentId) ?? asNonEmptyString(context.commentId); + if (issueId) payload.issueId = issueId; + if (taskId) payload.taskId = taskId; + if (taskKey) payload.taskKey = taskKey; + if (commentId) payload.commentId = commentId; + return payload; + }, [run.contextSnapshot, run.id]); + const resumeRun = useMutation({ + mutationFn: async () => { + const result = await agentsApi.wakeup(run.agentId, { + source: "on_demand", + triggerDetail: "manual", + reason: "resume_process_lost_run", + payload: resumePayload, + }); + if (!("id" in result)) { + throw new Error("Resume request was skipped because the agent is not currently invokable."); + } + return result; + }, + onSuccess: (resumedRun) => { + queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) }); + navigate(`/agents/${run.agentId}/runs/${resumedRun.id}`); + }, + }); const { data: touchedIssues } = useQuery({ queryKey: queryKeys.runIssues(run.id), @@ -1602,7 +1644,24 @@ function RunDetail({ run, adapterType }: { run: HeartbeatRun; adapterType: strin {cancelRun.isPending ? "Cancelling..." : "Cancel"} )} + {canResumeLostRun && ( + + )}

+ {resumeRun.isError && ( +
+ {resumeRun.error instanceof Error ? resumeRun.error.message : "Failed to resume run"} +
+ )} {startTime && (
diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index e89185c7..5d15de87 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -142,6 +142,12 @@ export function Dashboard() { return map; }, [issues, agents, projects]); + const entityTitleMap = useMemo(() => { + const map = new Map(); + for (const i of issues ?? []) map.set(`issue:${i.id}`, i.title); + return map; + }, [issues]); + const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; @@ -240,6 +246,7 @@ export function Dashboard() { event={event} agentMap={agentMap} entityNameMap={entityNameMap} + entityTitleMap={entityTitleMap} className={animatedActivityIds.has(event.id) ? "activity-row-enter" : undefined} /> ))}