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:
@@ -113,6 +113,13 @@ function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
describe("runTaskShow", () => {
|
||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
const mockTaskStoreGetTask = (task: Record<string, unknown>) => {
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn(),
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
@@ -125,10 +132,7 @@ describe("runTaskShow", () => {
|
||||
const longDesc = "A".repeat(120); // well over 60 chars
|
||||
const task = makeTask({ description: longDesc });
|
||||
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn(),
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
}));
|
||||
mockTaskStoreGetTask(task);
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
@@ -148,10 +152,7 @@ describe("runTaskShow", () => {
|
||||
description: "This is the full description that should not appear in the header",
|
||||
});
|
||||
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn(),
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
}));
|
||||
mockTaskStoreGetTask(task);
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
@@ -162,6 +163,33 @@ describe("runTaskShow", () => {
|
||||
expect(headerLine![0]).toContain("My Task Title");
|
||||
expect(headerLine![0]).not.toContain("This is the full description");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ sourceType: "dashboard_ui" }, "Source: Dashboard"],
|
||||
[{ sourceType: "agent_heartbeat", sourceAgentId: "agent-123" }, "Source: Agent (agent-123)"],
|
||||
[{ sourceType: "task_refine", sourceParentTaskId: "FN-2904" }, "Source: Refinement of FN-2904"],
|
||||
[{ sourceType: "task_duplicate", sourceParentTaskId: "FN-2905" }, "Source: Duplicate of FN-2905"],
|
||||
[
|
||||
{ sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" } },
|
||||
"Source: GitHub Import (https://github.com/owner/repo/issues/42)",
|
||||
],
|
||||
] as const)("prints provenance line for %o", async (overrides, expectedLine) => {
|
||||
mockTaskStoreGetTask(makeTask(overrides));
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain(expectedLine);
|
||||
});
|
||||
|
||||
it.each([{ sourceType: "unknown" }, {}])("omits provenance line for %o", async (overrides) => {
|
||||
mockTaskStoreGetTask(makeTask(overrides));
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).not.toContain("Source:");
|
||||
});
|
||||
});
|
||||
|
||||
describe("task node overrides", () => {
|
||||
|
||||
@@ -18,6 +18,57 @@ import { findNodeByNameOrId } from "./node.js";
|
||||
|
||||
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
|
||||
|
||||
function getGitHubIssueUrl(sourceMetadata: unknown): string | undefined {
|
||||
if (!sourceMetadata || typeof sourceMetadata !== "object") return undefined;
|
||||
const issueUrl = (sourceMetadata as { issueUrl?: unknown }).issueUrl;
|
||||
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||
}
|
||||
|
||||
function formatTaskSource(task: {
|
||||
sourceType?: string;
|
||||
sourceAgentId?: string;
|
||||
sourceParentTaskId?: string;
|
||||
sourceMetadata?: unknown;
|
||||
}): string | null {
|
||||
switch (task.sourceType) {
|
||||
case "dashboard_ui":
|
||||
return "Dashboard";
|
||||
case "quick_chat":
|
||||
return "Quick Chat";
|
||||
case "chat_session":
|
||||
return "Chat Session";
|
||||
case "agent_heartbeat":
|
||||
return task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent";
|
||||
case "automation":
|
||||
return "Automation";
|
||||
case "cron":
|
||||
return "Scheduled Task";
|
||||
case "workflow_step":
|
||||
return "Workflow Step";
|
||||
case "github_import": {
|
||||
const issueUrl = getGitHubIssueUrl(task.sourceMetadata);
|
||||
return issueUrl ? `GitHub Import (${issueUrl})` : "GitHub Import";
|
||||
}
|
||||
case "task_refine":
|
||||
return task.sourceParentTaskId
|
||||
? `Refinement of ${task.sourceParentTaskId}`
|
||||
: "Refinement";
|
||||
case "task_duplicate":
|
||||
return task.sourceParentTaskId
|
||||
? `Duplicate of ${task.sourceParentTaskId}`
|
||||
: "Duplicate";
|
||||
case "cli":
|
||||
return "CLI";
|
||||
case "api":
|
||||
return "API";
|
||||
case "recovery":
|
||||
return "Recovery";
|
||||
case "unknown":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandContext {
|
||||
store: TaskStore;
|
||||
projectPath: string;
|
||||
@@ -519,6 +570,10 @@ export async function runTaskShow(id: string, projectName?: string) {
|
||||
if (settings.unavailableNodePolicy) {
|
||||
console.log(` Unavailable Node Policy: ${settings.unavailableNodePolicy}`);
|
||||
}
|
||||
const sourceSummary = formatTaskSource(task);
|
||||
if (sourceSummary) {
|
||||
console.log(` Source: ${sourceSummary}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Steps
|
||||
|
||||
Reference in New Issue
Block a user