feat(FN-xxx): show created-by agent link in task detail

This commit is contained in:
gsxdsm
2026-05-01 06:45:44 -07:00
parent d8db7da946
commit ecabab86ed
3 changed files with 83 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Show task provenance as "Created by <agent name>" for agent-created tasks and make the agent name clickable to open the agent detail modal.

View File

@@ -1,5 +1,5 @@
import "./TaskDetailModal.css"; import "./TaskDetailModal.css";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch } from "lucide-react"; import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
@@ -227,6 +227,11 @@ interface ProvenanceDisplay {
label: string; label: string;
parentTaskId?: string; parentTaskId?: string;
contextInfo?: string; contextInfo?: string;
sourceAgentId?: string;
}
interface ProvenanceLabelOptions {
sourceAgentName?: string;
} }
function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined { function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined {
@@ -234,7 +239,9 @@ function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | und
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined; return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
} }
function getProvenanceLabel(task: Task | TaskDetail): ProvenanceDisplay | null { const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
function getProvenanceLabel(task: Task | TaskDetail, options: ProvenanceLabelOptions = {}): ProvenanceDisplay | null {
switch (task.sourceType) { switch (task.sourceType) {
case "dashboard_ui": case "dashboard_ui":
return { label: "Dashboard" }; return { label: "Dashboard" };
@@ -242,10 +249,13 @@ function getProvenanceLabel(task: Task | TaskDetail): ProvenanceDisplay | null {
return { label: "Quick Chat" }; return { label: "Quick Chat" };
case "chat_session": case "chat_session":
return { label: "Chat Session" }; return { label: "Chat Session" };
case "agent_heartbeat": case "agent_heartbeat": {
const sourceLabel = options.sourceAgentName ?? task.sourceAgentId;
return { return {
label: task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent", label: sourceLabel ?? "agent",
sourceAgentId: task.sourceAgentId,
}; };
}
case "automation": case "automation":
return { label: "Automation" }; return { label: "Automation" };
case "cron": case "cron":
@@ -361,7 +371,11 @@ export function TaskDetailModal({
(task.stuckKillCount ?? 0) > 0 || (task.stuckKillCount ?? 0) > 0 ||
(task.recoveryRetryCount ?? 0) > 0 || (task.recoveryRetryCount ?? 0) > 0 ||
Boolean(task.nextRecoveryAt); Boolean(task.nextRecoveryAt);
const provenanceDisplay = getProvenanceLabel(workingTask); const [sourceAgent, setSourceAgent] = useState<Agent | null>(null);
const [selectedSourceAgentId, setSelectedSourceAgentId] = useState<string | null>(null);
const provenanceDisplay = getProvenanceLabel(workingTask, {
sourceAgentName: sourceAgent?.name,
});
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab) // Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
useEffect(() => { useEffect(() => {
@@ -554,6 +568,32 @@ export function TaskDetailModal({
}; };
}, [task.assignedAgentId, projectId, agents]); }, [task.assignedAgentId, projectId, agents]);
useEffect(() => {
if (!task.sourceAgentId) {
setSourceAgent(null);
return;
}
const knownAgent = agents.find((agent) => agent.id === task.sourceAgentId);
if (knownAgent) {
setSourceAgent(knownAgent);
return;
}
let cancelled = false;
void Promise.resolve(fetchAgent(task.sourceAgentId, projectId))
.then((agent) => {
if (!cancelled) setSourceAgent(agent ?? null);
})
.catch(() => {
if (!cancelled) setSourceAgent(null);
});
return () => {
cancelled = true;
};
}, [task.sourceAgentId, projectId, agents]);
useEffect(() => { useEffect(() => {
setShowAgentPicker(false); setShowAgentPicker(false);
}, [task.id]); }, [task.id]);
@@ -1566,7 +1606,24 @@ export function TaskDetailModal({
<div className="detail-provenance"> <div className="detail-provenance">
<GitBranch aria-hidden="true" /> <GitBranch aria-hidden="true" />
<span> <span>
Created via {provenanceDisplay.label} {workingTask.sourceType === "agent_heartbeat" ? (
<>
Created by{" "}
{provenanceDisplay.sourceAgentId ? (
<button
type="button"
className="detail-provenance-link"
onClick={() => setSelectedSourceAgentId(provenanceDisplay.sourceAgentId!)}
>
{provenanceDisplay.label}
</button>
) : (
provenanceDisplay.label
)}
</>
) : (
<>Created via {provenanceDisplay.label}</>
)}
{provenanceDisplay.parentTaskId && ( {provenanceDisplay.parentTaskId && (
<> <>
{" "}of{" "} {" "}of{" "}
@@ -2480,6 +2537,16 @@ export function TaskDetailModal({
</div> </div>
</div> </div>
)} )}
{selectedSourceAgentId && (
<Suspense fallback={null}>
<AgentDetailView
agentId={selectedSourceAgentId}
projectId={projectId}
onClose={() => setSelectedSourceAgentId(null)}
addToast={addToast}
/>
</Suspense>
)}
</div> </div>
</div> </div>
); );

View File

@@ -142,7 +142,7 @@ describe("TaskDetailModal", () => {
describe("provenance display", () => { describe("provenance display", () => {
it.each([ it.each([
["dashboard_ui", undefined, "Created via Dashboard"], ["dashboard_ui", undefined, "Created via Dashboard"],
["agent_heartbeat", "agent-123", "Created via Agent (agent-123)"], ["agent_heartbeat", "agent-123", "Created by"],
] as const)("renders provenance text for %s", (sourceType, sourceAgentId, expectedText) => { ] as const)("renders provenance text for %s", (sourceType, sourceAgentId, expectedText) => {
render( render(
<TaskDetailModal <TaskDetailModal
@@ -156,7 +156,10 @@ describe("TaskDetailModal", () => {
/>, />,
); );
expect(screen.getByText(expectedText)).toBeInTheDocument(); expect(screen.getByText(new RegExp(expectedText))).toBeInTheDocument();
if (sourceType === "agent_heartbeat" && sourceAgentId) {
expect(screen.getByRole("button", { name: sourceAgentId })).toBeInTheDocument();
}
}); });
it("renders parent task link for refinement provenance", async () => { it("renders parent task link for refinement provenance", async () => {