Move Todo Lists into a bundled, project-enabled plugin package. - Move Todo UI, client API, and server routes into the plugin package. - Register and bundle Todo as an enabled plugin dashboard view rather than a static host feature. - Preserve Todo route validation and server-error semantics, including task assignment agent lookup. - Keep disabled and legacy Todo views out of project navigation and main content. Files changed: .changeset/fn-8762-todos-plugin.md | 7 + AGENTS.md | 3 +- docs/PLUGIN_AUTHORING.md | 4 + docs/dashboard-guide.md | 4 + docs/todo-view.md | 151 +---- .../cli/src/plugins/staged-bundled-plugin-ids.ts | 1 + packages/cli/tsup.config.ts | 8 + .../core/src/board/mobile-nav-primary-items.ts | 2 - .../__tests__/bundled-plugin-install.test.ts | 2 + .../core/src/plugins/bundled-plugin-install.ts | 1 + packages/dashboard/app/App.tsx | 18 +- .../app/__tests__/lazy-loaded-views-docs.test.ts | 9 +- packages/dashboard/app/api/legacy.ts | 15 - packages/dashboard/app/api/system/index.ts | 1 - packages/dashboard/app/api/system/todo.ts | 85 --- packages/dashboard/app/components/Header.tsx | 23 +- .../dashboard/app/components/LeftSidebarNav.tsx | 1 - packages/dashboard/app/components/MobileNavBar.tsx | 4 - .../dashboard/app/components/SettingsModal.tsx | 2 - .../app/components/__tests__/App.test.tsx | 7 - .../app/components/__tests__/Header.test.tsx | 42 -- .../app/components/__tests__/RightDock.test.tsx | 18 +- ...skDetail.mobile-transition.board-panel.test.tsx | 1 - .../__tests__/TaskDetail.swipe-back.test.tsx | 1 - .../__tests__/TodoView.mobile-css.test.ts | 66 --- .../app/components/__tests__/TodoView.test.tsx | 649 --------------------- .../__tests__/navigation-history.test.tsx | 3 - .../__tests__/overflowViewRegistry.test.tsx | 118 +--- .../app/components/dashboard/MainContent.tsx | 27 +- .../dashboard/app/components/dashboard/types.ts | 6 +- .../app/components/overflowViewRegistry.tsx | 21 +- .../app/hooks/__tests__/useTodoLists.test.ts | 291 --------- .../app/hooks/__tests__/useViewState.test.ts | 11 + packages/dashboard/app/hooks/useAppSettings.ts | 5 - packages/dashboard/app/hooks/useViewState.ts | 5 + .../__tests__/registerBundledPluginViews.test.tsx | 14 + packages/dashboard/app/plugins/bundled-todos.d.ts | 5 + .../app/plugins/registerBundledPluginViews.ts | 18 + packages/dashboard/app/plugins/types.ts | 4 + .../src/__tests__/todo-documentation.test.ts | 68 --- .../dashboard/src/__tests__/todo-routes.test.ts | 577 ------------------ packages/dashboard/src/registry-manifest.json | 101 +++- packages/dashboard/src/routes.ts | 1 - .../src/routes/plugin-bundled-runtimes.ts | 1 + .../src/routes/register-integrated-routers.ts | 2 - packages/dashboard/src/shared/dashboard-views.ts | 6 - packages/dashboard/src/todo-routes.ts | 342 ----------- packages/dashboard/vite.config.ts | 8 + packages/dashboard/vitest.config.ts | 8 + packages/desktop/scripts/workspace-tools.ts | 1 + plugins/fusion-plugin-todos/README.md | 20 + plugins/fusion-plugin-todos/manifest.json | 6 + plugins/fusion-plugin-todos/package.json | 38 ++ .../fusion-plugin-todos/src/dashboard-interop.d.ts | 12 + plugins/fusion-plugin-todos/src/dashboard-view.tsx | 4 + .../src/dashboard/LoadingSpinner.tsx | 1 + .../src/dashboard}/TodoView.css | 0 .../src/dashboard}/TodoView.tsx | 16 +- plugins/fusion-plugin-todos/src/dashboard/api.ts | 15 + .../src/dashboard/projectStorage.ts | 3 + .../fusion-plugin-todos/src/dashboard/swrCache.ts | 6 + .../src/dashboard/useConfirm.ts | 1 + .../src/dashboard}/useTodoLists.ts | 4 +- plugins/fusion-plugin-todos/src/index.ts | 4 + .../fusion-plugin-todos/src/todo-routes.test.ts | 46 ++ plugins/fusion-plugin-todos/src/todo-routes.ts | 156 +++++ plugins/fusion-plugin-todos/tsconfig.json | 28 + plugins/fusion-plugin-todos/vitest.config.ts | 9 + pnpm-lock.yaml | 64 +- pnpm-workspace.yaml | 1 + 70 files changed, 671 insertions(+), 2531 deletions(-) Fusion-Task-Id: FN-8762 Fusion-Task-Lineage: 3beb502c-3793-451b-b357-50c243395410 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
267 lines
11 KiB
TypeScript
267 lines
11 KiB
TypeScript
import { defineConfig, type Plugin } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import { dirname, resolve } from "node:path";
|
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { execSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
|
|
/**
|
|
* Generate a deterministic build version string.
|
|
*
|
|
* Uses the short git commit hash + a content hash of key files so the version
|
|
* only changes when the actual source (or uncommitted changes to those files)
|
|
* changes. Falls back to package.json version when git is unavailable.
|
|
*/
|
|
function computeBuildVersion(): string {
|
|
// Get git short hash or fall back to package.json version
|
|
let prefix: string;
|
|
try {
|
|
prefix = execSync("git rev-parse --short HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
} catch {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf-8"));
|
|
prefix = typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
} catch {
|
|
prefix = "0.0.0";
|
|
}
|
|
}
|
|
|
|
// Content hash of the entire app/ source tree + package.json. Hashing only
|
|
// a couple of entry files (the previous behavior) meant that edits to any
|
|
// other component or stylesheet produced an identical build version, so the
|
|
// dashboard's version-check poll never noticed the new bundle and the
|
|
// "reload available" prompt never fired (FN-3333 follow-up).
|
|
const hasher = createHash("sha1");
|
|
const appDir = resolve(__dirname, "app");
|
|
// Collect, sort, then hash so the order is stable across platforms and runs.
|
|
const files: string[] = [];
|
|
const walk = (dir: string): void => {
|
|
let entries: string[];
|
|
try {
|
|
entries = readdirSync(dir);
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry === "node_modules" || entry === "__tests__" || entry.startsWith(".")) continue;
|
|
const full = resolve(dir, entry);
|
|
let info: ReturnType<typeof statSync>;
|
|
try {
|
|
info = statSync(full);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (info.isDirectory()) {
|
|
walk(full);
|
|
} else if (info.isFile()) {
|
|
files.push(full);
|
|
}
|
|
}
|
|
};
|
|
walk(appDir);
|
|
files.sort();
|
|
for (const f of files) {
|
|
try {
|
|
hasher.update(f.slice(appDir.length));
|
|
hasher.update(readFileSync(f));
|
|
} catch {
|
|
// file may have been deleted between readdir and read — skip
|
|
}
|
|
}
|
|
try {
|
|
hasher.update(readFileSync(resolve(__dirname, "package.json")));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
const contentHash = hasher.digest("hex").slice(0, 8);
|
|
|
|
return `${prefix}-${contentHash}`;
|
|
}
|
|
|
|
const buildVersion = computeBuildVersion();
|
|
|
|
function emitVersionJson(): Plugin {
|
|
return {
|
|
name: "fusion-emit-version-json",
|
|
apply: "build",
|
|
closeBundle() {
|
|
const outFile = resolve(__dirname, "dist/client/version.json");
|
|
mkdirSync(dirname(outFile), { recursive: true });
|
|
writeFileSync(outFile, `${JSON.stringify({ version: buildVersion })}\n`);
|
|
console.log(`[fusion] build version: ${buildVersion}`);
|
|
},
|
|
};
|
|
}
|
|
|
|
function ensureThemeDataStylesheetOrder(): Plugin {
|
|
return {
|
|
name: "fusion-theme-data-link-order",
|
|
apply: "build",
|
|
enforce: "post",
|
|
transformIndexHtml(html) {
|
|
const headMatch = html.match(/<head>[\s\S]*?<\/head>/i);
|
|
if (!headMatch) return html;
|
|
|
|
const head = headMatch[0];
|
|
const themeLinkMatch = head.match(/<link[^>]*id=["']theme-data["'][^>]*>/i);
|
|
if (!themeLinkMatch) return html;
|
|
|
|
const themeLink = themeLinkMatch[0];
|
|
const headWithoutThemeLink = head.replace(themeLink, "");
|
|
const reorderedHead = headWithoutThemeLink.replace(/<\/head>$/i, `${themeLink}\n </head>`);
|
|
|
|
return html.replace(head, reorderedHead);
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
root: "app",
|
|
plugins: [react(), ensureThemeDataStylesheetOrder(), emitVersionJson()],
|
|
define: {
|
|
__BUILD_VERSION__: JSON.stringify(buildVersion),
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
/*
|
|
FNXC:GitHubImportTranslate 2026-07-15-09:30:
|
|
The browser bundle aliases `@fusion/core` to the leaf `types.ts` to keep Node-only deps out of the client, so anything the app imports from core must resolve to a browser-safe module.
|
|
Language detection is pure string logic shared with the server; alias its subpath explicitly rather than widening the `@fusion/core` alias, which would drag the full index (and its Node deps) into the bundle.
|
|
This alias MUST precede the `@fusion/core` entry — Vite matches aliases in order, so the broader key would otherwise swallow the subpath.
|
|
*/
|
|
/*
|
|
FNXC:DashboardBrowserSafeCore 2026-07-16-12:00:
|
|
The dashboard core-import scanner enforces this alias boundary for both relative core/src and package-subpath value imports.
|
|
Add a new browser leaf only after its full dependency graph is reviewed and it has a dated entry in scripts/lib/dashboard-browser-safe-core-modules.json.
|
|
*/
|
|
"@fusion/core/detect-content-language": resolve(__dirname, "../core/src/i18n/detect-content-language.ts"),
|
|
/*
|
|
FNXC:TaskDeleteAttribution 2026-07-26-17:05:
|
|
The delete-attribution constants (`x-fusion-client` header name + the dashboard-UI token) are shared by the browser client that STAMPS the header and the route that READS it, so both sides cannot drift apart into two spellings of the same string.
|
|
`task-delete-attribution.ts` imports nothing at all, so it is a safe browser leaf; alias its subpath rather than widening the `@fusion/core` alias, which would drag the Node-heavy index into the client bundle.
|
|
Ordered before the `@fusion/core` entry because Vite matches aliases in order and the broader key would otherwise swallow this subpath — the exact failure this line was added to fix (`"FUSION_CLIENT_HEADER" is not exported by ../core/src/types.ts`).
|
|
*/
|
|
"@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"),
|
|
"@fusion/core/column-roles": resolve(__dirname, "../core/src/column-roles.ts"),
|
|
"@fusion/core": resolve(__dirname, "../core/src/types.ts"),
|
|
"@fusion/dashboard/app/components/TaskCard": resolve(__dirname, "app/components/TaskCard.tsx"),
|
|
// FNXC:PluginBuild 2026-06-22-03:50: Bundled plugin source can import the dashboard's shared ViewHeader through the package export; Vite needs the same source alias during dashboard builds so plugin UI normalization does not fail only in CI merge builds.
|
|
"@fusion/dashboard/app/components/ViewHeader": resolve(__dirname, "app/components/ViewHeader.tsx"),
|
|
// FNXC:Quality 2026-07-19-12:00: The bundled Quality plugin needs the host's token-appended artifact URL helper because native video loads cannot attach authorization headers.
|
|
"@fusion/dashboard/app/api/tasks/task-content": resolve(__dirname, "app/api/tasks/task-content.ts"),
|
|
"@fusion/dashboard/app/plugins/types": resolve(__dirname, "app/plugins/types.ts"),
|
|
"@fusion/dashboard/app/utils/projectStorage": resolve(__dirname, "app/utils/projectStorage.ts"),
|
|
"@fusion/dashboard/app/utils/taskStuck": resolve(__dirname, "app/utils/taskStuck.ts"),
|
|
"@fusion-plugin-examples/compound-engineering/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-compound-engineering/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/compound-engineering": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-compound-engineering/src/index.ts",
|
|
),
|
|
"@fusion-plugin-examples/dependency-graph/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/dependency-graph": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-dependency-graph/src/index.ts",
|
|
),
|
|
"@fusion-plugin-examples/linear-import/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-linear-import/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/linear-import": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-linear-import/src/index.ts",
|
|
),
|
|
"@fusion-plugin-examples/quality/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-quality/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/roadmap/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-roadmap/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/todos/dashboard-view": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-todos/src/dashboard-view.tsx",
|
|
),
|
|
"@fusion-plugin-examples/todos": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-todos/src/index.ts",
|
|
),
|
|
"@fusion-plugin-examples/quality/qa-tab": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-quality/src/qa-tab.tsx",
|
|
),
|
|
"@fusion-plugin-examples/quality": resolve(
|
|
__dirname,
|
|
"../../plugins/fusion-plugin-quality/src/index.ts",
|
|
),
|
|
},
|
|
},
|
|
optimizeDeps: {
|
|
include: [
|
|
"@xterm/xterm",
|
|
"@xterm/addon-fit",
|
|
"@xterm/addon-web-links",
|
|
"@xterm/addon-webgl",
|
|
],
|
|
},
|
|
build: {
|
|
outDir: "../dist/client",
|
|
emptyOutDir: true,
|
|
manifest: true,
|
|
target: "es2022",
|
|
cssCodeSplit: true,
|
|
sourcemap: false,
|
|
assetsInlineLimit: 4096,
|
|
rollupOptions: {
|
|
output: {
|
|
entryFileNames: "assets/[name]-[hash].js",
|
|
chunkFileNames: "assets/[name]-[hash].js",
|
|
assetFileNames: "assets/[name]-[hash][extname]",
|
|
manualChunks: (id) => {
|
|
if (id.includes("/node_modules/react/") || id.includes("/node_modules/react-dom/")) {
|
|
return "vendor-react";
|
|
}
|
|
|
|
if (id.includes("/node_modules/@xterm/xterm/")) {
|
|
return "vendor-xterm";
|
|
}
|
|
|
|
if (id.includes("/node_modules/@codemirror/")) {
|
|
return "vendor-codemirror";
|
|
}
|
|
|
|
if (
|
|
id.includes("/node_modules/i18next/") ||
|
|
id.includes("/node_modules/react-i18next/") ||
|
|
id.includes("/node_modules/i18next-browser-languagedetector/") ||
|
|
id.includes("/node_modules/i18next-resources-to-backend/")
|
|
) {
|
|
return "vendor-i18n";
|
|
}
|
|
if (id.includes("/node_modules/@xyflow/")) {
|
|
return "vendor-reactflow";
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
proxy: {
|
|
// Keep Vite source modules under app/api* on the dev server while proxying real API endpoints.
|
|
"^/api(?!/.*\\.[jt]sx?(?:\\?|$))(/|$)": {
|
|
target: `http://localhost:${process.env.FUSION_API_PORT ?? "4040"}`,
|
|
changeOrigin: true,
|
|
ws: true,
|
|
},
|
|
},
|
|
},
|
|
});
|