feat(FN-2924): merge fusion/fn-2924
- Add task provenance display to TaskDetailModal, surfacing AI-generated merge commit summaries so users understand when and why tasks landed - New `ai-summarize` module in core with structured commit summarization logic and tests - Extend CLI `fn task show` with provenance output - Refactor merger to produce cleaner merge commit messages with task context - Update SettingsModal to expose AI summarization toggle (hidden behind feature flag) - Adjust QuickChatFAB styling for theme compatibility - Update scheduler and node-routing policy tests to match new behavior - Add changeset for `@runfusion/fusion` minor release Commits merged: - feat(FN-2924): display task provenance in dashboard and cli - feat(FN-2971): merge fusion/fn-2971 - feat(FN-2947): merge fusion/fn-2947 - feat(FN-2970): merge fusion/fn-2970 - feat(FN-2956): merge fusion/fn-2956 Files changed: .changeset/add-ai-merge-commit-summary.md | 5 + docs/cli-reference.md | 3 +- docs/settings-reference.md | 3 + packages/cli/src/commands/__tests__/task.test.ts | 44 ++++- packages/cli/src/commands/task.ts | 55 +++++++ packages/core/src/__tests__/ai-summarize.test.ts | 64 ++++++++ packages/core/src/ai-summarize.ts | 114 +++++++++++++ packages/core/src/index.ts | 3 + packages/core/src/settings-schema.ts | 1 + packages/core/src/types.ts | 4 + packages/dashboard/app/components/QuickChatFAB.css | 17 +- packages/dashboard/app/components/QuickChatFAB.tsx | 2 +- .../dashboard/app/components/SettingsModal.css | 11 +- .../dashboard/app/components/SettingsModal.tsx | 22 ++- .../dashboard/app/components/TaskDetailModal.css | 40 +++++ .../dashboard/app/components/TaskDetailModal.tsx | 82 +++++++++- .../__tests__/SettingsModalNodeRouting.test.tsx | 12 +- .../components/__tests__/TaskDetailModal.test.tsx | 81 ++++++++- packages/engine/src/__tests__/merger.test.ts | 75 +++++++++ .../src/__tests__/node-routing-policy.test.ts | 25 ++- .../src/__tests__/scheduler-node-routing.test.ts | 18 +- packages/engine/src/merger.ts | 181 +++++++++------------ packages/engine/src/scheduler.ts | 8 +- 23 files changed, 723 insertions(+), 147 deletions(-) Fusion-Task-Id: FN-2924
This commit is contained in:
@@ -115,6 +115,46 @@
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.detail-provenance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.detail-provenance svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.detail-provenance-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--todo);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.detail-provenance-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.detail-provenance-link:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-provenance {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-priority-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./TaskDetailModal.css";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
@@ -222,6 +222,64 @@ function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "sta
|
||||
return executionMode === "fast" ? "fast" : "standard";
|
||||
}
|
||||
|
||||
interface ProvenanceDisplay {
|
||||
label: string;
|
||||
parentTaskId?: string;
|
||||
contextInfo?: string;
|
||||
}
|
||||
|
||||
function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined {
|
||||
const issueUrl = metadata?.issueUrl;
|
||||
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||
}
|
||||
|
||||
function getProvenanceLabel(task: Task | TaskDetail): ProvenanceDisplay | null {
|
||||
switch (task.sourceType) {
|
||||
case "dashboard_ui":
|
||||
return { label: "Dashboard" };
|
||||
case "quick_chat":
|
||||
return { label: "Quick Chat" };
|
||||
case "chat_session":
|
||||
return { label: "Chat Session" };
|
||||
case "agent_heartbeat":
|
||||
return {
|
||||
label: task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent",
|
||||
};
|
||||
case "automation":
|
||||
return { label: "Automation" };
|
||||
case "cron":
|
||||
return { label: "Scheduled Task" };
|
||||
case "workflow_step":
|
||||
return { label: "Workflow Step" };
|
||||
case "github_import": {
|
||||
const issueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||
return {
|
||||
label: "GitHub Import",
|
||||
contextInfo: issueUrl,
|
||||
};
|
||||
}
|
||||
case "task_refine":
|
||||
return {
|
||||
label: "Refinement",
|
||||
parentTaskId: task.sourceParentTaskId,
|
||||
};
|
||||
case "task_duplicate":
|
||||
return {
|
||||
label: "Duplicate",
|
||||
parentTaskId: task.sourceParentTaskId,
|
||||
};
|
||||
case "cli":
|
||||
return { label: "CLI" };
|
||||
case "api":
|
||||
return { label: "API" };
|
||||
case "recovery":
|
||||
return { label: "Recovery" };
|
||||
case "unknown":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DESCRIPTION_TRUNCATE_LENGTH = 200;
|
||||
|
||||
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
|
||||
@@ -301,6 +359,7 @@ export function TaskDetailModal({
|
||||
(task.stuckKillCount ?? 0) > 0 ||
|
||||
(task.recoveryRetryCount ?? 0) > 0 ||
|
||||
Boolean(task.nextRecoveryAt);
|
||||
const provenanceDisplay = getProvenanceLabel(workingTask);
|
||||
|
||||
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
||||
useEffect(() => {
|
||||
@@ -1488,6 +1547,27 @@ export function TaskDetailModal({
|
||||
<span className={`detail-priority-chip detail-priority-chip--${normalizeTaskPriorityValue(task.priority)}`}>
|
||||
Priority: {normalizeTaskPriorityValue(task.priority)}
|
||||
</span>
|
||||
{provenanceDisplay && (
|
||||
<div className="detail-provenance">
|
||||
<GitBranch aria-hidden="true" />
|
||||
<span>
|
||||
Created via {provenanceDisplay.label}
|
||||
{provenanceDisplay.parentTaskId && (
|
||||
<>
|
||||
{" "}of{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="detail-provenance-link"
|
||||
onClick={() => handleDepClick(provenanceDisplay.parentTaskId!)}
|
||||
>
|
||||
{provenanceDisplay.parentTaskId}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{provenanceDisplay.contextInfo ? ` (${provenanceDisplay.contextInfo})` : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ vi.mock("../../api", () => ({
|
||||
uploadAttachment: vi.fn(),
|
||||
deleteAttachment: vi.fn(),
|
||||
updateTask: vi.fn().mockResolvedValue({}),
|
||||
fetchTaskDetail: vi.fn(),
|
||||
fetchTaskDetail: vi.fn().mockResolvedValue(makeTask()),
|
||||
fetchAgentLogs: vi.fn().mockResolvedValue([]),
|
||||
requestSpecRevision: vi.fn().mockResolvedValue({}),
|
||||
approvePlan: vi.fn().mockResolvedValue({}),
|
||||
@@ -57,6 +57,7 @@ vi.mock("lucide-react", () => ({
|
||||
CircleDot: () => null,
|
||||
XCircle: () => null,
|
||||
GitMerge: () => null,
|
||||
GitBranch: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useAgentLogs", () => ({
|
||||
@@ -136,6 +137,84 @@ describe("TaskDetailModal", () => {
|
||||
localStorage.removeItem("fn.authToken");
|
||||
});
|
||||
|
||||
describe("provenance display", () => {
|
||||
it.each([
|
||||
["dashboard_ui", undefined, "Created via Dashboard"],
|
||||
["agent_heartbeat", "agent-123", "Created via Agent (agent-123)"],
|
||||
] as const)("renders provenance text for %s", (sourceType, sourceAgentId, expectedText) => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ sourceType, sourceAgentId })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders parent task link for refinement provenance", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ sourceType: "task_refine", sourceParentTaskId: "FN-001" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Created via Refinement/)).toBeInTheDocument();
|
||||
const link = screen.getByRole("button", { name: "FN-001" });
|
||||
expect(link).toBeInTheDocument();
|
||||
await userEvent.click(link);
|
||||
await waitFor(() => {
|
||||
expect(noopOpenDetail).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders issue URL for github import provenance", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
sourceType: "github_import",
|
||||
sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" },
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Created via GitHub Import (https://github.com/owner/repo/issues/42)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(["unknown", undefined] as const)("omits provenance for %s source", (sourceType) => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ sourceType })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Created via/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("styles detail-body scrollbar rules", () => {
|
||||
const css = loadAllAppCss();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user