Add live badge to issues with active runs on /issues/active

Server: the /companies/:companyId/live-runs endpoint now returns
issueId extracted from contextSnapshot, so the UI can match runs
to issues without N+1 queries.

UI (Issues.tsx): fetches company live runs (with 5s polling), builds
a set of issue IDs with active runs, and shows a pulsing "Live" badge
in each matching issue row — matching the existing blue live indicator
style from AgentDetail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 12:24:38 -06:00
parent 260d525686
commit 8d5525d0da
3 changed files with 28 additions and 1 deletions

View File

@@ -906,6 +906,7 @@ export function agentRoutes(db: Db) {
agentId: heartbeatRuns.agentId,
agentName: agentsTable.name,
adapterType: agentsTable.adapterType,
issueId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"),
})
.from(heartbeatRuns)
.innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))

View File

@@ -18,6 +18,7 @@ export interface LiveRunForIssue {
agentId: string;
agentName: string;
adapterType: string;
issueId?: string | null;
}
export const heartbeatsApi = {

View File

@@ -1,8 +1,9 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues";
import { agentsApi } from "../api/agents";
import { heartbeatsApi } from "../api/heartbeats";
import { useCompany } from "../context/CompanyContext";
import { useDialog } from "../context/DialogContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
@@ -69,6 +70,21 @@ export function Issues() {
enabled: !!selectedCompanyId,
});
const { data: liveRuns } = useQuery({
queryKey: queryKeys.liveRuns(selectedCompanyId!),
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
enabled: !!selectedCompanyId,
refetchInterval: 5000,
});
const liveIssueIds = useMemo(() => {
const ids = new Set<string>();
for (const run of liveRuns ?? []) {
if (run.issueId) ids.add(run.issueId);
}
return ids;
}, [liveRuns]);
useEffect(() => {
setBreadcrumbs([{ label: "Issues" }]);
}, [setBreadcrumbs]);
@@ -169,6 +185,15 @@ export function Issues() {
}
trailing={
<div className="flex items-center gap-3">
{liveIssueIds.has(issue.id) && (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
</span>
<span className="text-[11px] font-medium text-blue-400">Live</span>
</span>
)}
{issue.assigneeAgentId && (() => {
const name = agentName(issue.assigneeAgentId);
return name