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 "./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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user