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} /> ))}