feat(FN-xxx): show created-by agent link in task detail
This commit is contained in:
5
.changeset/task-detail-created-by-agent-link.md
Normal file
5
.changeset/task-detail-created-by-agent-link.md
Normal 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.
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
@@ -227,6 +227,11 @@ interface ProvenanceDisplay {
|
||||
label: string;
|
||||
parentTaskId?: string;
|
||||
contextInfo?: string;
|
||||
sourceAgentId?: string;
|
||||
}
|
||||
|
||||
interface ProvenanceLabelOptions {
|
||||
sourceAgentName?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
case "dashboard_ui":
|
||||
return { label: "Dashboard" };
|
||||
@@ -242,10 +249,13 @@ function getProvenanceLabel(task: Task | TaskDetail): ProvenanceDisplay | null {
|
||||
return { label: "Quick Chat" };
|
||||
case "chat_session":
|
||||
return { label: "Chat Session" };
|
||||
case "agent_heartbeat":
|
||||
case "agent_heartbeat": {
|
||||
const sourceLabel = options.sourceAgentName ?? task.sourceAgentId;
|
||||
return {
|
||||
label: task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent",
|
||||
label: sourceLabel ?? "agent",
|
||||
sourceAgentId: task.sourceAgentId,
|
||||
};
|
||||
}
|
||||
case "automation":
|
||||
return { label: "Automation" };
|
||||
case "cron":
|
||||
@@ -361,7 +371,11 @@ export function TaskDetailModal({
|
||||
(task.stuckKillCount ?? 0) > 0 ||
|
||||
(task.recoveryRetryCount ?? 0) > 0 ||
|
||||
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)
|
||||
useEffect(() => {
|
||||
@@ -554,6 +568,32 @@ export function TaskDetailModal({
|
||||
};
|
||||
}, [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(() => {
|
||||
setShowAgentPicker(false);
|
||||
}, [task.id]);
|
||||
@@ -1566,7 +1606,24 @@ export function TaskDetailModal({
|
||||
<div className="detail-provenance">
|
||||
<GitBranch aria-hidden="true" />
|
||||
<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 && (
|
||||
<>
|
||||
{" "}of{" "}
|
||||
@@ -2480,6 +2537,16 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedSourceAgentId && (
|
||||
<Suspense fallback={null}>
|
||||
<AgentDetailView
|
||||
agentId={selectedSourceAgentId}
|
||||
projectId={projectId}
|
||||
onClose={() => setSelectedSourceAgentId(null)}
|
||||
addToast={addToast}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -142,7 +142,7 @@ describe("TaskDetailModal", () => {
|
||||
describe("provenance display", () => {
|
||||
it.each([
|
||||
["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) => {
|
||||
render(
|
||||
<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 () => {
|
||||
|
||||
Reference in New Issue
Block a user