@runfusion/fusion 0.26.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/bin.js +12847 -2514
  2. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  3. package/dist/client/assets/AgentDetailView-shgiiUb4.js +18 -0
  4. package/dist/client/assets/{AgentsView-CV3vm7Qk.css → AgentsView-B3ADnF0D.css} +1 -1
  5. package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-CpwqOVDz.js} +12 -7
  6. package/dist/client/assets/ChatView-DyRBOIKL.js +1 -0
  7. package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-Cdelj9-m.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-C0kmRv0u.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-B94U9ijs.js} +1 -1
  10. package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-O_4YWy--.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-CkEiF85-.js} +1 -1
  12. package/dist/client/assets/InsightsView-D-Qe0tRr.js +11 -0
  13. package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-CoRUmRvb.js} +2 -2
  14. package/dist/client/assets/NodesView-DQzXjcLc.js +14 -0
  15. package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-Dn1LmFbq.js} +2 -2
  16. package/dist/client/assets/PluginManager-Y0fs-6No.js +1 -0
  17. package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-CjOxKhdS.js} +1 -1
  18. package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-Bg1-3JO_.js} +1 -1
  19. package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
  20. package/dist/client/assets/SettingsModal-DL7tjJQa.js +31 -0
  21. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  22. package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-DuzYPbuJ.js} +1 -1
  23. package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-BIFoVNUf.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-C52KsV7f.js} +1 -1
  25. package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-sS_mT0Y7.js} +2 -2
  26. package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
  27. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  28. package/dist/client/assets/dashboard-view-MB-86hAu.js +21 -0
  29. package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-B9cwJ-OX.js} +1 -1
  30. package/dist/client/assets/index-BOjPRqEk.js +692 -0
  31. package/dist/client/assets/index-BmSEq8Rb.css +1 -0
  32. package/dist/client/assets/{star-D75YKEq-.js → star-BDn04UYV.js} +1 -1
  33. package/dist/client/assets/{upload-BYYTgWFj.js → upload-zdPPycKQ.js} +1 -1
  34. package/dist/client/assets/{users-RS90Aii3.js → users-CPYZjK2g.js} +1 -1
  35. package/dist/client/index.html +2 -2
  36. package/dist/client/version.json +1 -1
  37. package/dist/droid-cli/package.json +1 -1
  38. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  39. package/dist/extension.js +7433 -1920
  40. package/dist/pi-claude-cli/package.json +1 -1
  41. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  42. package/dist/pi-claude-cli/src/provider.ts +7 -1
  43. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
  44. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
  45. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  46. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  47. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  88. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  89. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  90. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  91. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  92. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  93. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  96. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  97. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  98. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  99. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  100. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  101. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  102. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  103. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  104. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  105. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  106. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  107. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  108. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  132. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  133. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  134. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  135. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  136. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  137. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  138. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  142. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  143. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  144. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  145. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  146. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  147. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  148. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  149. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  150. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  151. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  152. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
  153. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  154. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  155. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  156. package/package.json +1 -1
  157. package/skill/fusion/SKILL.md +1 -1
  158. package/skill/fusion/references/engine-tools.md +2 -2
  159. package/skill/fusion/references/extension-tools.md +4 -3
  160. package/skill/fusion/references/fusion-capabilities.md +1 -1
  161. package/skill/fusion/workflows/task-management.md +3 -1
  162. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  163. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
  164. package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
  165. package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
  166. package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
  167. package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
  168. package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
  169. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  170. package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
  171. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  172. package/dist/client/assets/index-Bdw6llW6.js +0 -692
  173. package/dist/client/assets/index-CZGlyJuS.css +0 -1
  174. package/dist/plugins/fusion-plugin-roadmap/bundled.css +0 -1093
@@ -0,0 +1,8 @@
1
+ import type { ReportListItemVm } from "../types.js";
2
+
3
+ export function ReportListItem({ report, selected, onSelect }: { report: ReportListItemVm; selected: boolean; onSelect: (id: string) => void }) {
4
+ return <button className="card reports-list-item" data-selected={selected ? "true" : "false"} onClick={() => onSelect(report.id)}>
5
+ <div className="card-header"><span className="card-title">{report.title}</span></div>
6
+ <div className="card-meta"><span>{report.cadence}</span><span>{report.status}</span><span>{report.periodStart} → {report.periodEnd}</span></div>
7
+ </button>;
8
+ }
@@ -0,0 +1,29 @@
1
+ .share-blocks-panel {
2
+ border-top: var(--btn-border-width) solid var(--border);
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--space-md);
6
+ padding-top: var(--space-md);
7
+ }
8
+
9
+ .share-blocks-panel__tabs {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ gap: var(--space-xs);
13
+ }
14
+
15
+ .share-blocks-panel__content {
16
+ min-height: calc(var(--space-2xl) * 4);
17
+ resize: vertical;
18
+ }
19
+
20
+ .share-blocks-panel__locked {
21
+ color: var(--text-muted);
22
+ margin: 0;
23
+ }
24
+
25
+ @media (max-width: 768px) {
26
+ .share-blocks-panel__tabs {
27
+ flex-direction: column;
28
+ }
29
+ }
@@ -0,0 +1,43 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getShareBlocks } from "../api.js";
3
+ import type { ReportRecord } from "../types.js";
4
+ import type { ShareBlocks } from "../../share-blocks.js";
5
+ import "./ShareBlocksPanel.css";
6
+
7
+ const TABS: Array<{ key: keyof ShareBlocks; label: string }> = [
8
+ { key: "plainText", label: "Plain Text" },
9
+ { key: "markdown", label: "Markdown" },
10
+ { key: "slack", label: "Slack" },
11
+ { key: "emailHtml", label: "Email HTML" },
12
+ ];
13
+
14
+ export function ShareBlocksPanel({ report }: { report: ReportRecord }) {
15
+ const [active, setActive] = useState<keyof ShareBlocks>("plainText");
16
+ const [data, setData] = useState<ShareBlocks | null>(null);
17
+ const [locked, setLocked] = useState(false);
18
+ const [copied, setCopied] = useState(false);
19
+
20
+ useEffect(() => {
21
+ setLocked(false);
22
+ setData(null);
23
+ getShareBlocks(report.id).then(setData).catch((error: Error) => {
24
+ if (error.message.includes("409")) setLocked(true);
25
+ });
26
+ }, [report.id]);
27
+
28
+ if (locked) return <section className="share-blocks-panel"><p className="share-blocks-panel__locked">Share blocks unlock after the report is approved.</p></section>;
29
+ if (!data) return <section className="share-blocks-panel"><p>Loading share blocks…</p></section>;
30
+
31
+ const value = data[active];
32
+ return <section className="share-blocks-panel">
33
+ <div className="share-blocks-panel__tabs">
34
+ {TABS.map((tab) => <button key={tab.key} className={`btn btn-sm ${active === tab.key ? "btn-primary" : ""}`} onClick={() => setActive(tab.key)}>{tab.label}</button>)}
35
+ </div>
36
+ <textarea className="input share-blocks-panel__content" readOnly value={value} />
37
+ <button className="btn btn-sm" onClick={async () => {
38
+ await navigator.clipboard.writeText(value);
39
+ setCopied(true);
40
+ setTimeout(() => setCopied(false), 1000);
41
+ }}>{copied ? "Copied" : "Copy"}</button>
42
+ </section>;
43
+ }
@@ -0,0 +1,38 @@
1
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { ReportApprovalPanel } from "../ReportApprovalPanel.js";
4
+
5
+ vi.mock("../../api.js", () => ({
6
+ approveReport: vi.fn(async () => ({ ...baseReport, approvalState: "approved" })),
7
+ rejectReport: vi.fn(async () => ({ ...baseReport, approvalState: "rejected" })),
8
+ publishReport: vi.fn(async () => ({ ...baseReport, approvalState: "published" })),
9
+ }));
10
+
11
+ const baseReport: any = {
12
+ id: "rep_1",
13
+ approvalState: "awaiting_approval",
14
+ approvalHistory: [],
15
+ status: "review_complete",
16
+ };
17
+
18
+ afterEach(() => cleanup());
19
+
20
+ describe("ReportApprovalPanel", () => {
21
+ it("renders actions for awaiting approval and posts approve", async () => {
22
+ const onReportChange = vi.fn();
23
+ render(<ReportApprovalPanel report={baseReport} onReportChange={onReportChange} />);
24
+ fireEvent.click(screen.getByText("Approve"));
25
+ await waitFor(() => expect(onReportChange).toHaveBeenCalled());
26
+ });
27
+
28
+ it("renders publish action for approved", () => {
29
+ render(<ReportApprovalPanel report={{ ...baseReport, approvalState: "approved" }} onReportChange={vi.fn()} />);
30
+ expect(screen.getByText("Publish")).toBeInTheDocument();
31
+ });
32
+
33
+ it("read-only for rejected", () => {
34
+ render(<ReportApprovalPanel report={{ id: "rep_1", status: "review_complete", approvalState: "rejected", approvalHistory: [{ action: "reject", decidedAt: "now", decidedBy: "u" }] } as any} onReportChange={vi.fn()} />);
35
+ expect(screen.queryByText("Publish")).toBeNull();
36
+ expect(screen.getByText(/reject by/i)).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,24 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ShareBlocksPanel } from "../ShareBlocksPanel.js";
4
+
5
+ const getShareBlocks = vi.fn();
6
+ vi.mock("../../api.js", () => ({ getShareBlocks: (...args: unknown[]) => getShareBlocks(...args) }));
7
+
8
+ describe("ShareBlocksPanel", () => {
9
+ it("renders tabs and copies selected block", async () => {
10
+ getShareBlocks.mockResolvedValue({ plainText: "a", markdown: "b", slack: "c", emailHtml: "d" });
11
+ Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
12
+ render(<ShareBlocksPanel report={{ id: "rep_1" } as any} />);
13
+ await screen.findByText("Plain Text");
14
+ fireEvent.click(screen.getByText("Markdown"));
15
+ fireEvent.click(screen.getByText("Copy"));
16
+ await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith("b"));
17
+ });
18
+
19
+ it("shows locked message on 409", async () => {
20
+ getShareBlocks.mockRejectedValue(new Error("409 Conflict"));
21
+ render(<ShareBlocksPanel report={{ id: "rep_1" } as any} />);
22
+ await screen.findByText(/unlock after the report is approved/i);
23
+ });
24
+ });
@@ -0,0 +1,18 @@
1
+ import "@testing-library/jest-dom/vitest";
2
+ import { vi } from "vitest";
3
+
4
+ if (typeof window !== "undefined") {
5
+ Object.defineProperty(window, "matchMedia", {
6
+ writable: true,
7
+ value: vi.fn().mockImplementation((query: string) => ({
8
+ matches: false,
9
+ media: query,
10
+ onchange: null,
11
+ addEventListener: vi.fn(),
12
+ removeEventListener: vi.fn(),
13
+ addListener: vi.fn(),
14
+ removeListener: vi.fn(),
15
+ dispatchEvent: vi.fn(),
16
+ })),
17
+ });
18
+ }
@@ -0,0 +1,22 @@
1
+ import type { Report, ReportCadence, ReportStatus } from "../store/report-types.js";
2
+
3
+ export type ToastType = "success" | "error" | "info" | "warning";
4
+
5
+ export interface ReportFilters {
6
+ cadence: "all" | ReportCadence;
7
+ status: "all" | ReportStatus;
8
+ from: string;
9
+ to: string;
10
+ q: string;
11
+ agentId: string;
12
+ }
13
+
14
+ export interface SectionRef {
15
+ id: string;
16
+ label: string;
17
+ hash: string;
18
+ }
19
+
20
+ export type ReportRecord = Report;
21
+
22
+ export type ReportListItemVm = Pick<ReportRecord, "id" | "title" | "cadence" | "status" | "periodStart" | "periodEnd" | "createdAt" | "metadata">;
@@ -0,0 +1,44 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getReportPreviewHtml } from "./api.js";
3
+
4
+ const cache = new Map<string, string>();
5
+
6
+ export function useReportPreview(id?: string, projectId?: string) {
7
+ const [html, setHtml] = useState<string>("");
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState<string | null>(null);
10
+
11
+ useEffect(() => {
12
+ if (!id) {
13
+ setHtml("");
14
+ setError(null);
15
+ return;
16
+ }
17
+ const key = `${projectId ?? ""}:${id}`;
18
+ const cached = cache.get(key);
19
+ if (cached) {
20
+ setHtml(cached);
21
+ setError(null);
22
+ return;
23
+ }
24
+ const controller = new AbortController();
25
+ setLoading(true);
26
+ setError(null);
27
+ getReportPreviewHtml(id, projectId)
28
+ .then((nextHtml) => {
29
+ if (controller.signal.aborted) return;
30
+ cache.set(key, nextHtml);
31
+ setHtml(nextHtml);
32
+ })
33
+ .catch((err: unknown) => {
34
+ if (controller.signal.aborted) return;
35
+ setError(err instanceof Error ? err.message : "Failed to load preview");
36
+ })
37
+ .finally(() => {
38
+ if (!controller.signal.aborted) setLoading(false);
39
+ });
40
+ return () => controller.abort();
41
+ }, [id, projectId]);
42
+
43
+ return { html, loading, error };
44
+ }
@@ -0,0 +1,59 @@
1
+ import { useMemo } from "react";
2
+ import type { ReportRecord, SectionRef } from "./types.js";
3
+
4
+ export interface SectionDiff {
5
+ added: SectionRef[];
6
+ removed: SectionRef[];
7
+ changed: SectionRef[];
8
+ unchanged: SectionRef[];
9
+ }
10
+
11
+ const ALL_SECTIONS: Array<{ id: string; label: string }> = [
12
+ { id: "summary", label: "Summary" },
13
+ { id: "system-wins", label: "System wins" },
14
+ { id: "system-highlights", label: "System highlights" },
15
+ { id: "system-lowlights", label: "System lowlights" },
16
+ { id: "system-proposals", label: "System proposals" },
17
+ { id: "system-deep-dives", label: "System deep dives" },
18
+ { id: "agent-card", label: "Per-agent" },
19
+ { id: "data-coverage", label: "Data coverage" },
20
+ { id: "review-panel", label: "Review panel" },
21
+ ];
22
+
23
+ function extractValue(report: ReportRecord | undefined, id: string): unknown {
24
+ if (!report) return undefined;
25
+ switch (id) {
26
+ case "summary": return report.metadata?.summary;
27
+ case "system-wins": return report.metadata?.wins;
28
+ case "system-highlights": return report.metadata?.highlights;
29
+ case "system-lowlights": return report.metadata?.lowlights;
30
+ case "system-proposals": return report.metadata?.proposals;
31
+ case "system-deep-dives": return report.metadata?.deepDives;
32
+ case "agent-card": return report.metadata?.perAgent;
33
+ case "data-coverage": return report.metadata?.dataCoverage;
34
+ case "review-panel": return report.combinedReview;
35
+ default: return undefined;
36
+ }
37
+ }
38
+
39
+ export function diffReportSections(a?: ReportRecord, b?: ReportRecord): SectionDiff {
40
+ const added: SectionRef[] = [];
41
+ const removed: SectionRef[] = [];
42
+ const changed: SectionRef[] = [];
43
+ const unchanged: SectionRef[] = [];
44
+
45
+ for (const section of ALL_SECTIONS) {
46
+ const left = extractValue(a, section.id);
47
+ const right = extractValue(b, section.id);
48
+ const ref: SectionRef = { id: section.id, label: section.label, hash: section.id };
49
+ if (left == null && right != null) added.push(ref);
50
+ else if (left != null && right == null) removed.push(ref);
51
+ else if (JSON.stringify(left) !== JSON.stringify(right)) changed.push(ref);
52
+ else unchanged.push(ref);
53
+ }
54
+ return { added, removed, changed, unchanged };
55
+ }
56
+
57
+ export function useReportSectionDiff(a?: ReportRecord, b?: ReportRecord): SectionDiff {
58
+ return useMemo(() => diffReportSections(a, b), [a, b]);
59
+ }
@@ -0,0 +1,71 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { getReport, listReports } from "./api.js";
3
+ import type { ReportFilters, ReportRecord, ToastType } from "./types.js";
4
+
5
+ const DEFAULT_FILTERS: ReportFilters = { cadence: "all", status: "all", from: "", to: "", q: "", agentId: "" };
6
+
7
+ export function useReports({ projectId, addToast }: { projectId?: string; addToast: (message: string, type?: ToastType) => void }) {
8
+ const [filters, setFilters] = useState<ReportFilters>(DEFAULT_FILTERS);
9
+ const [reports, setReports] = useState<ReportRecord[]>([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [selectedId, setSelectedId] = useState<string | undefined>();
12
+ const [selectedReport, setSelectedReport] = useState<ReportRecord | undefined>();
13
+ const [compareMode, setCompareMode] = useState(false);
14
+ const [compareA, setCompareA] = useState<string | undefined>();
15
+ const [compareB, setCompareB] = useState<string | undefined>();
16
+
17
+ useEffect(() => {
18
+ const controller = new AbortController();
19
+ setLoading(true);
20
+ listReports({
21
+ cadence: filters.cadence === "all" ? undefined : filters.cadence,
22
+ status: filters.status === "all" ? undefined : filters.status,
23
+ from: filters.from || undefined,
24
+ to: filters.to || undefined,
25
+ q: filters.q || undefined,
26
+ agentId: filters.agentId || undefined,
27
+ projectId,
28
+ }).then((items) => {
29
+ if (controller.signal.aborted) return;
30
+ setReports(items);
31
+ if (!selectedId && items[0]) setSelectedId(items[0].id);
32
+ }).catch((err: unknown) => {
33
+ if (controller.signal.aborted) return;
34
+ addToast(err instanceof Error ? err.message : "Failed to load reports", "error");
35
+ }).finally(() => {
36
+ if (!controller.signal.aborted) setLoading(false);
37
+ });
38
+ return () => controller.abort();
39
+ }, [filters, projectId, addToast, selectedId]);
40
+
41
+ useEffect(() => {
42
+ if (!selectedId) return;
43
+ getReport(selectedId, projectId).then(setSelectedReport).catch((err: unknown) => {
44
+ addToast(err instanceof Error ? err.message : "Failed to load report", "error");
45
+ });
46
+ }, [selectedId, projectId, addToast]);
47
+
48
+ const selectId = useCallback((id: string) => setSelectedId(id), []);
49
+ const enterCompareMode = useCallback(() => setCompareMode(true), []);
50
+ const closeCompareMode = useCallback(() => setCompareMode(false), []);
51
+ const setCompareSlot = useCallback((slot: "a" | "b", id: string) => {
52
+ if (slot === "a") setCompareA(id);
53
+ else setCompareB(id);
54
+ }, []);
55
+
56
+ return useMemo(() => ({
57
+ filters,
58
+ setFilters,
59
+ reports,
60
+ loading,
61
+ selectedId,
62
+ selectedReport,
63
+ selectId,
64
+ compareMode,
65
+ compareA,
66
+ compareB,
67
+ enterCompareMode,
68
+ closeCompareMode,
69
+ setCompareSlot,
70
+ }), [filters, reports, loading, selectedId, selectedReport, selectId, compareMode, compareA, compareB, enterCompareMode, closeCompareMode, setCompareSlot]);
71
+ }
@@ -0,0 +1,13 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export function useViewportMode() {
4
+ const [mobile, setMobile] = useState(false);
5
+ useEffect(() => {
6
+ const mq = window.matchMedia("(max-width: 768px)");
7
+ const onChange = () => setMobile(mq.matches);
8
+ onChange();
9
+ mq.addEventListener("change", onChange);
10
+ return () => mq.removeEventListener("change", onChange);
11
+ }, []);
12
+ return { mobile };
13
+ }
@@ -0,0 +1,6 @@
1
+ import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
2
+ import { ReportsView } from "./dashboard/ReportsView.js";
3
+
4
+ export function ReportsDashboardView({ context }: { context?: PluginDashboardViewContext }) {
5
+ return <ReportsView projectId={context?.projectId} addToast={context?.addToast ?? (() => undefined)} />;
6
+ }
@@ -1,11 +1,22 @@
1
1
  import type { PluginContext } from "@fusion/core";
2
2
  import { definePlugin } from "@fusion/plugin-sdk";
3
+ import { initializeApprovalState } from "./approval.js";
3
4
  import { runReviewPanel } from "./review-panel.js";
4
5
  import { ensureReportSchema } from "./report-schema.js";
6
+ import { createReportApprovalRoutes } from "./routes/report-approval-routes.js";
7
+ import { createReportExportRoutes } from "./routes/report-export-routes.js";
8
+ import { createReportListRoutes } from "./routes/report-list-routes.js";
5
9
  import type { CombinedReview, ReviewPanelMember, RunReviewPanelInput } from "./review-types.js";
10
+ import {
11
+ getApprovalRequired,
12
+ getApproverAgentIds,
13
+ getAutoPublishOnApproval,
14
+ getPublishTargets,
15
+ settingsSchema,
16
+ } from "./settings.js";
6
17
  import type { ReportCadence, ReportCreateInput } from "./store/report-types.js";
7
18
  import { ReportStore } from "./store/report-store.js";
8
- import { settingsSchema } from "./settings.js";
19
+ export { ReportsDashboardView } from "./dashboard-view.js";
9
20
 
10
21
  const plugin = definePlugin({
11
22
  manifest: {
@@ -21,6 +32,17 @@ const plugin = definePlugin({
21
32
  hooks: {
22
33
  onSchemaInit: ensureReportSchema,
23
34
  },
35
+ routes: [...createReportListRoutes(), ...createReportExportRoutes(), ...createReportApprovalRoutes()],
36
+ dashboardViews: [
37
+ {
38
+ viewId: "reports",
39
+ label: "Reports",
40
+ componentPath: "./dashboard-view",
41
+ icon: "FileText",
42
+ placement: "primary",
43
+ order: 35,
44
+ },
45
+ ],
24
46
  });
25
47
 
26
48
  export interface RunGeneratedReportReviewInput {
@@ -73,15 +95,39 @@ export async function runGeneratedReportReview(input: RunGeneratedReportReviewIn
73
95
  cwd: input.cwd,
74
96
  }, ctx);
75
97
 
76
- store.attachReview(report.id, combinedReview);
98
+ const reviewed = store.attachReview(report.id, combinedReview);
99
+
100
+ const nextApprovalState = initializeApprovalState(reviewed.status, {
101
+ approvalRequired: getApprovalRequired(ctx.settings),
102
+ autoPublishOnApproval: getAutoPublishOnApproval(ctx.settings),
103
+ approverAgentIds: getApproverAgentIds(ctx.settings),
104
+ publishTargets: getPublishTargets(ctx.settings),
105
+ });
106
+
107
+ if (nextApprovalState !== "not_required") {
108
+ const now = new Date().toISOString();
109
+ store.updateReport(report.id, {
110
+ approvalState: nextApprovalState,
111
+ ...(nextApprovalState === "approved"
112
+ ? { status: "approved", approvedAt: now, approvedBy: "system" }
113
+ : {}),
114
+ ...(nextApprovalState === "published" ? { status: "published", publishedAt: now } : {}),
115
+ });
116
+ }
117
+
77
118
  return combinedReview;
78
119
  }
79
120
 
80
121
  export default plugin;
81
122
 
82
123
  export * from "./settings.js";
124
+ export * from "./cadence.js";
125
+ export * from "./aggregation.js";
126
+ export * from "./pipeline.js";
127
+ export * from "./runs-store.js";
83
128
  export * from "./review-types.js";
84
129
  export * from "./review-panel.js";
85
130
  export { ensureReportSchema } from "./report-schema.js";
86
131
  export { ReportStore, ReportStoreError, type ReportStoreEvents } from "./store/report-store.js";
87
132
  export * from "./store/report-types.js";
133
+ export * from "./render/index.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * INTERIM ORCHESTRATOR. FN-3779 replaces this with cadence-registry + cron-sentinel wiring;
3
+ * FN-3780 replaces the aggregate dependency with the real aggregation layer.
4
+ * Keep the `ReportsPipelineDependencies` shape stable so both can plug in without callsite churn.
5
+ */
6
+ import type { ReportsAggregator } from "./aggregation.js";
7
+ import type { ReportsCadence } from "./cadence.js";
8
+ import type { ReportRunRecord, ReportsRunsStore } from "./runs-store.js";
9
+
10
+ export interface ReportsPipelineDependencies {
11
+ runsStore: ReportsRunsStore;
12
+ aggregate: ReportsAggregator;
13
+ }
14
+
15
+ export interface StartPipelineInput {
16
+ runId: string;
17
+ cadence: ReportsCadence;
18
+ settings: Record<string, unknown>;
19
+ now?: Date;
20
+ }
21
+
22
+ export async function startReportsPipeline(
23
+ input: StartPipelineInput,
24
+ deps: ReportsPipelineDependencies,
25
+ ): Promise<ReportRunRecord> {
26
+ const nowIso = (input.now ?? new Date()).toISOString();
27
+
28
+ const created = await deps.runsStore.create({
29
+ id: input.runId,
30
+ cadence: input.cadence,
31
+ status: "queued",
32
+ createdAt: nowIso,
33
+ updatedAt: nowIso,
34
+ });
35
+
36
+ await deps.runsStore.update(input.runId, { status: "running", updatedAt: nowIso });
37
+
38
+ try {
39
+ await deps.aggregate({
40
+ runId: input.runId,
41
+ cadence: input.cadence,
42
+ settings: input.settings,
43
+ });
44
+
45
+ const updatedAt = new Date().toISOString();
46
+ const reviewed = await deps.runsStore.update(input.runId, { status: "review", updatedAt });
47
+ return reviewed ?? { ...created, status: "review", updatedAt };
48
+ } catch (error) {
49
+ const updatedAt = new Date().toISOString();
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ const failed = await deps.runsStore.update(input.runId, {
52
+ status: "failed",
53
+ error: message,
54
+ updatedAt,
55
+ });
56
+ return failed ?? { ...created, status: "failed", error: message, updatedAt };
57
+ }
58
+ }
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { escapeAttr, escapeHtml } from "../escape.js";
3
+
4
+ describe("escape", () => {
5
+ it("escapes html special characters", () => {
6
+ expect(escapeHtml("&<>'\"")).toBe("&amp;&lt;&gt;&#39;&quot;");
7
+ });
8
+
9
+ it("escapes attribute-sensitive characters", () => {
10
+ expect(escapeAttr("a`b&c")).toBe("a&#96;b&amp;c");
11
+ });
12
+
13
+ it("passes unicode through", () => {
14
+ expect(escapeHtml("こんにちは 🌍")).toBe("こんにちは 🌍");
15
+ });
16
+
17
+ it("passes unicode through in attributes", () => {
18
+ expect(escapeAttr("こんにちは 🌍")).toBe("こんにちは 🌍");
19
+ });
20
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Report } from "../../store/report-types.js";
3
+ import { renderReportHtml } from "../html-template.js";
4
+
5
+ function createRecord(overrides: Partial<Report> = {}, metadata: Record<string, unknown> = {}): Report {
6
+ const base: Report = {
7
+ id: "rep_1",
8
+ cadence: "daily",
9
+ periodStart: "2026-05-01",
10
+ periodEnd: "2026-05-02",
11
+ title: "Weekly report",
12
+ status: "review_complete",
13
+ generationStartedAt: "2026-05-02T00:00:00.000Z",
14
+ generationCompletedAt: "2026-05-02T00:01:00.000Z",
15
+ reviewStartedAt: null,
16
+ reviewCompletedAt: null,
17
+ approvedAt: null,
18
+ approvedBy: null,
19
+ publishedAt: null,
20
+ archivedAt: null,
21
+ failureReason: null,
22
+ approvalState: "not_required",
23
+ approvalHistory: [],
24
+ draftMarkdown: null,
25
+ renderedHtmlPath: null,
26
+ combinedReview: null,
27
+ createdAt: "2026-05-02T00:00:00.000Z",
28
+ updatedAt: "2026-05-02T00:01:00.000Z",
29
+ metadata,
30
+ renderedHtml: null,
31
+ renderedHtmlGeneratedAt: null,
32
+ };
33
+ return {
34
+ ...base,
35
+ ...overrides,
36
+ renderedHtml: overrides.renderedHtml ?? base.renderedHtml,
37
+ renderedHtmlGeneratedAt: overrides.renderedHtmlGeneratedAt ?? base.renderedHtmlGeneratedAt,
38
+ };
39
+ }
40
+
41
+ describe("renderReportHtml", () => {
42
+ it("renders shell for empty record", () => {
43
+ const html = renderReportHtml(createRecord());
44
+ expect(html).toContain("<!doctype html>");
45
+ expect(html).toContain("data-section=\"data-coverage\"");
46
+ });
47
+
48
+ it("renders mixed sections with markers", () => {
49
+ const html = renderReportHtml(createRecord({}, {
50
+ sections: {
51
+ summary: "hello",
52
+ system: { wins: ["w1"], highlights: ["h1"], lowlights: ["l1"], proposals: ["p1"], deepDives: ["d1"] },
53
+ perAgent: [{ agentId: "a1", wins: ["x"] }],
54
+ },
55
+ }));
56
+ expect(html).toContain('data-section="summary"');
57
+ expect(html).toContain('data-section="system-wins"');
58
+ expect(html).toContain('data-section="system-highlights"');
59
+ expect(html).toContain('data-section="system-lowlights"');
60
+ expect(html).toContain('data-section="system-proposals"');
61
+ expect(html).toContain('data-section="system-deep-dives"');
62
+ expect(html).toContain('data-section="agent-card"');
63
+ });
64
+
65
+ it("omits toggled off sections", () => {
66
+ const html = renderReportHtml(createRecord({}, {
67
+ settings: { enabledSections: ["wins"] },
68
+ sections: { system: { wins: ["w1"], highlights: ["h1"] } },
69
+ }));
70
+ expect(html).toContain('data-section="system-wins"');
71
+ expect(html).not.toContain('data-section="system-highlights"');
72
+ });
73
+
74
+ it("respects section order", () => {
75
+ const html = renderReportHtml(createRecord({}, {
76
+ settings: { sectionOrder: ["proposals", "wins"], enabledSections: ["wins", "proposals"] },
77
+ sections: { system: { wins: ["w1"], proposals: ["p1"] } },
78
+ }));
79
+ expect(html.indexOf('data-section="system-proposals"')).toBeLessThan(html.indexOf('data-section="system-wins"'));
80
+ });
81
+
82
+ it("renders per-agent cards with stable ids", () => {
83
+ const html = renderReportHtml(createRecord({}, {
84
+ sections: { perAgent: [{ agentId: "agent-1" }, { agentId: "agent-2" }] },
85
+ }));
86
+ expect(html).toContain('data-agent-id="agent-1"');
87
+ expect(html).toContain('data-agent-id="agent-2"');
88
+ });
89
+
90
+ it("escapes hostile inputs", () => {
91
+ const html = renderReportHtml(createRecord({}, {
92
+ sections: { summary: '<script>alert(1)</script> " onmouseover=' },
93
+ }));
94
+ expect(html).not.toContain("<script>alert(1)</script>");
95
+ expect(html).not.toContain("javascript:");
96
+ });
97
+
98
+ it("applies explicit theme", () => {
99
+ const dark = renderReportHtml(createRecord(), { theme: "dark" });
100
+ const light = renderReportHtml(createRecord(), { theme: "light" });
101
+ expect(dark).toContain('data-theme="dark"');
102
+ expect(light).toContain('data-theme="light"');
103
+ });
104
+
105
+ it("returns body-only when includeChrome false", () => {
106
+ const body = renderReportHtml(createRecord(), { includeChrome: false });
107
+ expect(body.startsWith("<!doctype html>")).toBe(false);
108
+ expect(body.startsWith("<article")).toBe(true);
109
+ });
110
+ });