diff --git a/.github/workflows/ci-web.yaml b/.github/workflows/ci-web.yaml new file mode 100644 index 0000000..1be4197 --- /dev/null +++ b/.github/workflows/ci-web.yaml @@ -0,0 +1,33 @@ +name: CI Web + +# Thin caller over the org reusable front-end CI for the agent-team dashboard SPA +# (agent-team/web — Vite + React + TypeScript + vitest). Distinct job id `ci-web` +# (→ status context `ci-web / ci`) so it does NOT collide with the Python +# `ci / ci` context emitted by ci.yaml. +# +# Our package.json currently exposes build + test (+ typecheck); `build` runs +# `tsc -b && vite build`, so the type-check happens inside build. Prettier +# (format:check), ESLint (lint), and Playwright (test:e2e) are not wired yet, so +# those reusable steps are toggled off until those tools are added. + +on: + pull_request: + branches: [main] + paths: + - "agent-team/web/**" + - ".github/workflows/ci-web.yaml" + +permissions: + contents: read + +jobs: + ci-web: + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@main + with: + node-version: "24" + working-directory: agent-team/web + cache-dependency-path: agent-team/web/package-lock.json + required-scripts: "build,test" + run-format-check: false + run-lint: false + run-e2e: false diff --git a/agent-team/web/package-lock.json b/agent-team/web/package-lock.json index 9aebf63..962de26 100644 --- a/agent-team/web/package-lock.json +++ b/agent-team/web/package-lock.json @@ -9,10 +9,21 @@ "version": "1.0.0", "dependencies": { "@dagrejs/dagre": "1.1.4", + "@radix-ui/react-collapsible": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.12", + "@radix-ui/react-select": "^2.3.1", + "@radix-ui/react-separator": "^1.1.10", + "@radix-ui/react-slot": "^1.3.0", + "@radix-ui/react-tabs": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.10", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.21.0", "react": "18.3.1", "react-dom": "18.3.1", "react-markdown": "9.0.1", - "reactflow": "11.11.4" + "reactflow": "11.11.4", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@testing-library/jest-dom": "6.4.8", @@ -21,7 +32,11 @@ "@types/react": "18.3.5", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.3.1", + "autoprefixer": "^10.5.1", "jsdom": "24.1.3", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", "typescript": "5.5.4", "vite": "5.4.8", "vitest": "1.6.0" @@ -34,6 +49,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -871,6 +899,44 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -934,6 +1000,743 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz", + "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", + "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", + "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz", + "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz", + "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", + "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", + "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", + "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", + "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "license": "MIT" + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -1900,7 +2703,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -2145,6 +2948,46 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2172,6 +3015,43 @@ "dev": true, "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.1.tgz", + "integrity": "sha512-jwM2pcTuCWUoN70FEvf5XrXyDbUgRURK4FnU8v0jWZZYU/KkVvN9T33mu1sVLFY9JW3kTWzKheEpn6xYLRc/VA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2195,6 +3075,32 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", @@ -2253,6 +3159,16 @@ "node": ">= 0.4" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -2370,12 +3286,71 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2419,6 +3394,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -2455,6 +3440,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -2670,6 +3668,12 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2683,6 +3687,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2693,6 +3704,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -2884,6 +3902,59 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -2901,6 +3972,20 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2971,6 +4056,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -2998,6 +4092,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3217,6 +4324,35 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -3227,6 +4363,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -3237,6 +4396,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3276,6 +4445,16 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3349,6 +4528,26 @@ "node": ">=6" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -3415,6 +4614,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3606,6 +4814,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -4048,6 +5266,20 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4120,6 +5352,18 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", @@ -4149,6 +5393,16 @@ "node": ">=18" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -4185,6 +5439,26 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -4265,6 +5539,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -4289,6 +5570,39 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -4337,6 +5651,140 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4407,6 +5855,27 @@ "dev": true, "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4476,6 +5945,75 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reactflow": { "version": "11.11.4", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", @@ -4494,6 +6032,29 @@ "react-dom": ">=17" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4548,6 +6109,39 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -4600,6 +6194,30 @@ "dev": true, "license": "MIT" }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4794,6 +6412,29 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4807,6 +6448,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -4814,6 +6468,87 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4821,6 +6556,54 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", @@ -4841,6 +6624,19 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -4890,6 +6686,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", @@ -5067,6 +6876,49 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -5076,6 +6928,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/agent-team/web/package.json b/agent-team/web/package.json index 77b8085..8efbdef 100644 --- a/agent-team/web/package.json +++ b/agent-team/web/package.json @@ -10,14 +10,25 @@ "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc -b --noEmit" + "typecheck": "tsc -b" }, "dependencies": { "@dagrejs/dagre": "1.1.4", + "@radix-ui/react-collapsible": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.12", + "@radix-ui/react-select": "^2.3.1", + "@radix-ui/react-separator": "^1.1.10", + "@radix-ui/react-slot": "^1.3.0", + "@radix-ui/react-tabs": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.10", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.21.0", "react": "18.3.1", "react-dom": "18.3.1", "react-markdown": "9.0.1", - "reactflow": "11.11.4" + "reactflow": "11.11.4", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@testing-library/jest-dom": "6.4.8", @@ -26,7 +37,11 @@ "@types/react": "18.3.5", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.3.1", + "autoprefixer": "^10.5.1", "jsdom": "24.1.3", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", "typescript": "5.5.4", "vite": "5.4.8", "vitest": "1.6.0" diff --git a/agent-team/web/postcss.config.js b/agent-team/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/agent-team/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/agent-team/web/src/App.tsx b/agent-team/web/src/App.tsx index b04ec85..bd9e5ba 100644 --- a/agent-team/web/src/App.tsx +++ b/agent-team/web/src/App.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { LayoutGrid, Workflow as WorkflowIcon } from "lucide-react"; import { fetchState, fetchTask, @@ -7,17 +8,22 @@ import { type TaskDetail, type Topology, } from "./api"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { Board } from "./components/Board"; import { PipelineMap } from "./components/PipelineMap"; import { TaskDrawer } from "./components/TaskDrawer"; -import { TaskList } from "./components/TaskList"; import { TopBar } from "./components/TopBar"; const POLL_MS = 4000; +type View = "board" | "pipeline"; + export default function App() { const [state, setState] = useState(null); const [topology, setTopology] = useState(null); const [connected, setConnected] = useState(false); + const [view, setView] = useState("board"); const [selectedId, setSelectedId] = useState(null); const [detail, setDetail] = useState(null); @@ -81,6 +87,7 @@ export default function App() { const toggleNodeFilter = useCallback((nodeId: string) => { setNodeFilter((cur) => (cur === nodeId ? null : nodeId)); + setView("board"); // node clicks filter the board, so surface it }, []); // Highlight the nodes a selected task has visited. @@ -105,42 +112,69 @@ export default function App() { return looped; }, [selectedId, detail, topology]); + const drawerOpen = selectedId != null; + return ( -
- -
- +
+ setNodeFilter(null)} + connected={connected} + generatedAt={state?.generated_at ?? null} /> -
- {topology ? ( - + setView(v as View)} + className="flex min-w-0 flex-1 flex-col" + aria-label="Main view" + > +
+ + + Board + + + Pipeline + + +
+ + setNodeFilter(null)} + /> + + + {topology ? ( + + ) : ( +
+ Loading pipeline… +
+ )} +
+
+ {drawerOpen && ( + - ) : ( -
Loading pipeline…
)} -
- +
-
+ ); } diff --git a/agent-team/web/src/api.ts b/agent-team/web/src/api.ts index 75d4ac8..71b72b8 100644 --- a/agent-team/web/src/api.ts +++ b/agent-team/web/src/api.ts @@ -13,11 +13,28 @@ export interface TaskView { waiting_since: string | null; } +// One pipeline stage as grouped + ordered by the backend (status_page.snapshot). +// Each stage maps to a Kanban column; `tasks` are the live tasks sitting in it. +export interface Stage { + key: string; + label: string; + agent: string; + phases: string[]; + gated: boolean; + role_node: boolean; + state: NodeState; + count: number; + tasks: TaskView[]; +} + export interface StateResponse { ok: boolean; error: string | null; generated_at: string; db_path: string; + // Backend already groups tasks into ordered pipeline stages; the Board renders + // one column per stage. Optional for resilience against older daemons. + stages?: Stage[]; tasks: TaskView[]; waiting: TaskView[]; question_counts: Record; diff --git a/agent-team/web/src/components/Board.test.tsx b/agent-team/web/src/components/Board.test.tsx new file mode 100644 index 0000000..02ade2f --- /dev/null +++ b/agent-team/web/src/components/Board.test.tsx @@ -0,0 +1,275 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { ReactNode } from "react"; +import { Board } from "./Board"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import type { StateResponse, Topology } from "../api"; + +/** Wrap in TooltipProvider — the real app mounts this at the root. */ +function renderWithProviders(ui: ReactNode) { + return render({ui}); +} + +const TOPO: Topology = { + trees: [ + { id: "core", label: "Core" }, + { id: "sdlc", label: "SDLC" }, + ], + nodes: [ + { id: "intake", label: "Intake", agent: "coordinator", tree: "core", kind: "phase", gated: false }, + { id: "clarify", label: "Clarify", agent: "coordinator", tree: "core", kind: "gate", gated: true }, + { id: "plan", label: "Plan", agent: "Claude", tree: "sdlc", kind: "phase", gated: false }, + ], + edges: [], +}; + +function makeState(): StateResponse { + return { + ok: true, + error: null, + generated_at: "now", + db_path: "x", + stages: [ + { + key: "intake", + label: "Intake", + agent: "coordinator", + phases: ["intake"], + gated: false, + role_node: false, + state: "idle", + count: 1, + tasks: [ + { + thread_id: "aaaa1111", + short_id: "aaaa1111", + task: "remediate CVE", + current_phase: "intake", + status: "active", + waiting: false, + waiting_since: null, + }, + ], + }, + { + key: "clarify", + label: "Clarify", + agent: "coordinator", + phases: ["clarify"], + gated: true, + role_node: false, + state: "awaiting_human", + count: 1, + tasks: [ + { + thread_id: "bbbb2222", + short_id: "bbbb2222", + task: "fix login bug", + current_phase: "clarify", + status: "waiting_human", + waiting: true, + waiting_since: "2024-01-01T00:00:00Z", + }, + ], + }, + ], + tasks: [ + { + thread_id: "aaaa1111", + short_id: "aaaa1111", + task: "remediate CVE", + current_phase: "intake", + status: "active", + waiting: false, + waiting_since: null, + }, + { + thread_id: "bbbb2222", + short_id: "bbbb2222", + task: "fix login bug", + current_phase: "clarify", + status: "waiting_human", + waiting: true, + waiting_since: "2024-01-01T00:00:00Z", + }, + ], + waiting: [], + question_counts: {}, + summary: { active: 1, parked: 0, waiting: 1, open_questions: 0, total: 2 }, + budget: { available: false }, + nodes: {}, + }; +} + +describe("Board", () => { + it("renders a column per stage with correct labels", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + expect(screen.getByLabelText("Stage: Intake")).toBeInTheDocument(); + expect(screen.getByLabelText("Stage: Clarify")).toBeInTheDocument(); + }); + + it("shows task count badge in each column header", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + const intakeCol = screen.getByLabelText("Stage: Intake"); + // Both columns have 1 task each + expect(within(intakeCol).getByText("1")).toBeInTheDocument(); + + const clarifyCol = screen.getByLabelText("Stage: Clarify"); + expect(within(clarifyCol).getByText("1")).toBeInTheDocument(); + }); + + it("renders task descriptions inside columns", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + expect(screen.getByText("remediate CVE")).toBeInTheDocument(); + expect(screen.getByText("fix login bug")).toBeInTheDocument(); + }); + + it("shows a waiting affordance (clock icon) for a waiting task", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + // The Clock icon has aria-label="awaiting human" + expect(screen.getByLabelText("awaiting human")).toBeInTheDocument(); + }); + + it("shows lock icon on a gated stage column", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + expect(screen.getByLabelText("human gate")).toBeInTheDocument(); + }); + + it("clicking a card fires onSelect with the thread_id", () => { + const onSelect = vi.fn(); + renderWithProviders( + {}} + />, + ); + + fireEvent.click(screen.getByText("remediate CVE")); + expect(onSelect).toHaveBeenCalledWith("aaaa1111"); + }); + + it("selected card gets aria-selected=true", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + const cards = screen.getAllByRole("option", { selected: true }); + expect(cards).toHaveLength(1); + }); + + it("shows node filter chip when nodeFilter is set", () => { + const onClearNodeFilter = vi.fn(); + renderWithProviders( + {}} + onClearNodeFilter={onClearNodeFilter} + />, + ); + + const chip = screen.getByLabelText("Clear node filter: intake"); + expect(chip).toBeInTheDocument(); + fireEvent.click(chip); + expect(onClearNodeFilter).toHaveBeenCalled(); + }); + + it("falls back to deriving stages from tasks when stages is undefined", () => { + const stateNoStages: StateResponse = { + ...makeState(), + stages: undefined, + }; + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + // Should still show the tasks grouped by phase + expect(screen.getByText("remediate CVE")).toBeInTheDocument(); + expect(screen.getByText("fix login bug")).toBeInTheDocument(); + }); + + it("renders null state gracefully with loading message", () => { + renderWithProviders( + {}} + onClearNodeFilter={() => {}} + />, + ); + + expect(screen.getByText("Loading…")).toBeInTheDocument(); + }); +}); diff --git a/agent-team/web/src/components/Board.tsx b/agent-team/web/src/components/Board.tsx new file mode 100644 index 0000000..a2d1dbf --- /dev/null +++ b/agent-team/web/src/components/Board.tsx @@ -0,0 +1,287 @@ +import { useMemo, useState } from "react"; +import { Lock, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; +import { TaskCard } from "./TaskCard"; +import { buildTreeByNode, filterTasks, phaseToNode } from "../taskFilters"; +import type { Stage, StateResponse, TaskView, Topology } from "../api"; + +interface Props { + state: StateResponse | null; + topology: Topology | null; + selectedId: string | null; + nodeFilter: string | null; + onSelect: (threadId: string) => void; + onClearNodeFilter: () => void; +} + +const STATUS_OPTIONS = [ + { value: "all", label: "All statuses" }, + { value: "active", label: "Active" }, + { value: "waiting_human", label: "Waiting human" }, + { value: "parked", label: "Parked" }, + { value: "done", label: "Done" }, + { value: "failed", label: "Failed" }, +]; + +/** Derive ordered stages from topology node order when state.stages is absent. */ +function deriveStages(tasks: TaskView[], topology: Topology | null): Stage[] { + // Build node order from topology; fall back to natural insertion order. + const nodeOrder: string[] = topology?.nodes.map((n) => n.id) ?? []; + + const grouped = new Map(); + for (const t of tasks) { + const node = phaseToNode(t.current_phase); + if (!grouped.has(node)) grouped.set(node, []); + grouped.get(node)!.push(t); + } + + // Sort by topology order; unknown nodes go at end. + const nodeIds = [...grouped.keys()].sort((a, b) => { + const ai = nodeOrder.indexOf(a); + const bi = nodeOrder.indexOf(b); + if (ai === -1 && bi === -1) return 0; + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); + + return nodeIds.map((nodeId) => { + const topoNode = topology?.nodes.find((n) => n.id === nodeId); + const stageTasks = grouped.get(nodeId) ?? []; + return { + key: nodeId, + label: topoNode?.label ?? nodeId, + agent: topoNode?.agent ?? "", + phases: [nodeId], + gated: topoNode?.gated ?? false, + role_node: false, + state: "idle" as const, + count: stageTasks.length, + tasks: stageTasks, + }; + }); +} + +/** + * A human-decision gate — distinct from `stage.gated`, which means the stage is + * INERT (a disabled/not-yet-wired P3 node like build/verify/dispatch). A gate is + * the "gate" stage or a topology node of kind "gate" (e.g. clarify). + */ +function isGate(stage: Stage, topology: Topology | null): boolean { + if (stage.key === "gate") return true; + const topoNode = topology?.nodes.find((n) => n.id === stage.key); + return topoNode?.kind === "gate"; +} + +export function Board({ + state, + topology, + selectedId, + nodeFilter, + onSelect, + onClearNodeFilter, +}: Props) { + const [query, setQuery] = useState(""); + const [status, setStatus] = useState("all"); + const [tree, setTree] = useState("all"); + + const treeByNode = useMemo(() => buildTreeByNode(topology), [topology]); + + // Use backend-provided stages or fall back to derived grouping. + const allStages: Stage[] = useMemo(() => { + if (!state) return []; + if (state.stages && state.stages.length > 0) return state.stages; + return deriveStages(state.tasks ?? [], topology); + }, [state, topology]); + + // Filter each stage's tasks by the toolbar + nodeFilter params. + const filteredStages = useMemo(() => { + return allStages.map((stage) => { + const filtered = filterTasks(stage.tasks, { + query, + status, + tree, + nodeFilter, + treeByNode, + }); + return { ...stage, tasks: filtered, count: filtered.length }; + }); + }, [allStages, query, status, tree, nodeFilter, treeByNode]); + + if (!state) { + return ( +
+ Loading… +
+ ); + } + + return ( +
+ {/* Toolbar */} +
+ setQuery(e.target.value)} + className="h-8 w-48 text-sm" + /> + + + + + + {/* Node filter chip */} + {nodeFilter && ( + + )} +
+ + {/* Kanban columns — horizontal scroll. Each column is its own listbox of + task cards (valid ARIA nesting), so this row is just a labelled list. */} +
+ {filteredStages.map((stage) => ( + + ))} + + {filteredStages.length === 0 && ( +
+ No stages available. +
+ )} +
+
+ ); +} + +interface ColumnProps { + stage: Stage; + topology: Topology | null; + selectedId: string | null; + onSelect: (threadId: string) => void; +} + +function KanbanColumn({ stage, topology, selectedId, onSelect }: ColumnProps) { + const gate = isGate(stage, topology); + + // NOTE: we intentionally do NOT dim stages from the backend `stage.gated` + // flag. In the live deployment build/verify/dispatch are wired and active even + // though they're still flagged gated, so that flag is a stale "P3 opt-in" hint, + // not a real "inactive" signal. Activity is conveyed by live task counts/status. + + return ( +
+ {/* Column header */} +
+ + {stage.label} + +
+ + {stage.count} + + {gate && ( + + + + + human gate + + )} +
+
+ + {/* Task cards — scrollable vertically. flex-1 + min-h-0 lets the list cap + at the real column height (no magic viewport math). */} + +
+ {stage.tasks.length === 0 ? ( +

—

+ ) : ( + stage.tasks.map((t) => ( + + )) + )} +
+
+
+ ); +} diff --git a/agent-team/web/src/components/MapNode.tsx b/agent-team/web/src/components/MapNode.tsx index 6f888fd..c4a45e7 100644 --- a/agent-team/web/src/components/MapNode.tsx +++ b/agent-team/web/src/components/MapNode.tsx @@ -1,5 +1,8 @@ import { memo } from "react"; import { Handle, Position, type NodeProps } from "reactflow"; +import { Lock, RotateCcw } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import type { RFData } from "../layout"; // One pipeline stage on the map. Color encodes live state; a badge shows the @@ -7,49 +10,71 @@ import type { RFData } from "../layout"; // label keep the map reachable for screen readers / keyboard users. function MapNodeImpl({ data }: NodeProps) { const { meta, state, count, dimmed, onPath, loopBackTo } = data; - const cls = [ - "node", - `state-${state}`, - meta.gated ? "gated" : "", - dimmed ? "dimmed" : "", - onPath ? "on-path" : "", - ] - .filter(Boolean) - .join(" "); const canLoop = loopBackTo.length > 0; return (
-
- {meta.label} - {meta.kind === "gate" && ( - - ⛋ - - )} - {canLoop && ( - - ↺ - - )} -
-
{meta.agent}
+ + {/* Count badge — top-right corner */} {count > 0 && ( - + {count} )} - {meta.gated && inert} + + {/* Label row */} +
+ {meta.label} + + {meta.kind === "gate" && ( + + + + )} + + {canLoop && ( + + + + + + + + can send work back to {loopBackTo.join(", ")} + + + )} +
+ + {/* Agent name */} +
+ {meta.agent} +
+
); diff --git a/agent-team/web/src/components/PipelineMap.tsx b/agent-team/web/src/components/PipelineMap.tsx index 7a842a0..b6d25f2 100644 --- a/agent-team/web/src/components/PipelineMap.tsx +++ b/agent-team/web/src/components/PipelineMap.tsx @@ -39,7 +39,7 @@ export function PipelineMap({ const handleNodeClick: NodeMouseHandler = (_evt, node) => onNodeClick(node.id); return ( -
+
void; +} + +export function TaskCard({ task, selected, onSelect }: TaskCardProps) { + function handleClick() { + onSelect(task.thread_id); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelect(task.thread_id); + } + } + + return ( +
+ {/* Top row: status badge + waiting clock */} +
+ + {task.status.replace("_", " ")} + + {task.waiting && ( + + )} +
+ + {/* Task description — 2-line clamp */} +

+ {task.task} +

+ + {/* Short ID in mono muted text */} +

{task.short_id}

+
+ ); +} diff --git a/agent-team/web/src/components/TaskDrawer.tsx b/agent-team/web/src/components/TaskDrawer.tsx index 7f97dd4..7a29839 100644 --- a/agent-team/web/src/components/TaskDrawer.tsx +++ b/agent-team/web/src/components/TaskDrawer.tsx @@ -1,5 +1,18 @@ +import { MessageSquare, X, ChevronDown } from "lucide-react"; import type { TaskDetail } from "../api"; import { Markdown } from "./Markdown"; +import { Badge, statusVariant } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; +import { formatEastern } from "@/lib/time"; interface Props { detail: TaskDetail | null; @@ -24,91 +37,246 @@ function fmtCost(usd: number): string { export function TaskDrawer({ detail, loading, error, onClose }: Props) { if (!detail && !loading && !error) return null; + const isWaiting = detail?.ok && detail.status === "waiting_human"; + const isClarifyGate = + isWaiting && detail?.current_phase === "clarify"; + return ( -
-
-

Task history

- +
+ {/* Sticky header */} +
+

Task history

+
- {loading &&
Loading…
} - {error &&
{error}
} + {loading && ( +
+ Loading… +
+ )} + {error && ( +
+ {error} +
+ )} {detail && detail.ok && ( - <> -
-
{detail.task}
-
- {detail.status} - {detail.short_id} - spend {fmtCost(detail.total_usd)} -
- {detail.partial && ( -
- Partial history — recorded since the v3 ledger; earlier steps are - reconstructed. -
- )} -
+ +
+ {/* Summary card */} + + +

+ {detail.task} +

+
+ + {detail.status} + + + {detail.short_id} + + + spend {fmtCost(detail.total_usd)} + +
+ {detail.partial && ( +

+ Partial history — recorded since the v3 ledger; earlier + steps are reconstructed. +

+ )} +
+
-
    - {detail.timeline.map((step, i) => { - const open = step.exited_at == null; - return ( -
  1. - -
    -
    {step.to_phase}
    -
    - {step.entered_at ?? "—"} - - {open ? "in progress" : fmtDuration(step.duration_s)} - -
    -
    - {step.status && {step.status}} - {fmtCost(step.cost_usd)} - {step.note && {step.note}} + {/* Gate panel — waiting_human informational only */} + {isWaiting && ( + + +
    + +
    +

    + Input requested +

    +

    + {isClarifyGate + ? "The pipeline needs clarification before planning." + : "The pipeline is awaiting a human decision to proceed."} +

    +

    + Answer in Slack{" "} + #agent-team +

    -
  2. - ); - })} - {detail.timeline.length === 0 && ( -
  3. No transitions recorded.
  4. + + )} -
- {detail.qa_history.length > 0 && ( -
- Q&A ({detail.qa_history.length}) - -
- )} - {detail.review_verdicts.length > 0 && ( -
- Review verdicts ({detail.review_verdicts.length}) - -
- )} - {detail.plan && ( -
- Plan - -
- )} - + {/* Timeline */} +
+

+ Timeline +

+
    + {detail.timeline.map((step, i) => { + const open = step.exited_at == null; + const isLast = i === detail.timeline.length - 1; + return ( +
  1. + {/* Dot + connector */} +
    + + {!isLast && ( + + )} +
    + + {/* Step body */} +
    +
    + + {step.to_phase} + + + {open ? "in progress" : fmtDuration(step.duration_s)} + +
    +
    + {step.entered_at && ( + + {formatEastern(step.entered_at)} + + )} + {step.status && ( + + {step.status} + + )} + {step.cost_usd > 0 && ( + + {fmtCost(step.cost_usd)} + + )} + {step.note && ( + + {step.note} + + )} +
    +
    +
  2. + ); + })} + {detail.timeline.length === 0 && ( +
  3. + No transitions recorded. +
  4. + )} +
+
+ + {/* Q&A section */} + {detail.qa_history.length > 0 && ( + <> + + + + + + )} + + {/* Review verdicts */} + {detail.review_verdicts.length > 0 && ( + <> + + + + + + )} + + {/* Plan */} + {detail.plan && ( + <> + + + + + + )} +
+
)} {detail && !detail.ok && ( -
{detail.error ?? "Could not load task."}
+
+ {detail.error ?? "Could not load task."} +
)}
); } +// Collapsible section wrapper used for Q&A / Verdicts / Plan +function CollapsibleSection({ + title, + defaultOpen, + children, +}: { + title: string; + defaultOpen: boolean; + children: React.ReactNode; +}) { + return ( + + +

+ {title} +

+ +
+ +
{children}
+
+
+ ); +} + function asText(v: unknown): string { return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v, null, 2); } @@ -118,17 +286,23 @@ function asText(v: unknown): string { // as markdown so numbered sub-answers / bold render readably. function QASection({ entries }: { entries: unknown[] }) { return ( -
+
{entries.map((entry, i) => { const obj = entry && typeof entry === "object" ? (entry as Record) : null; const turn = obj && "turn" in obj ? Number(obj.turn) : i; const answer = obj && "answer" in obj ? asText(obj.answer) : asText(entry); return ( -
-
Turn {turn + 1}
- {answer} -
+ + + + Turn {turn + 1} + + + + {answer} + + ); })}
@@ -139,7 +313,7 @@ function QASection({ entries }: { entries: unknown[] }) { // verdict badge + round + outcome header and the findings rendered as markdown. function VerdictsSection({ entries }: { entries: unknown[] }) { return ( -
+
{entries.map((entry, i) => { const v = entry && typeof entry === "object" ? (entry as Record) : {}; @@ -147,16 +321,29 @@ function VerdictsSection({ entries }: { entries: unknown[] }) { const round = "round_index" in v ? Number(v.round_index) : i + 1; const outcome = asText(v.outcome); const findings = asText(v.findings); - const cls = verdict.replace(/[^a-z_]/gi, "_").toLowerCase(); return ( -
-
- Round {round} - {verdict.replace(/_/g, " ")} - {outcome && {outcome.replace(/_/g, " ")}} -
- {findings && {findings}} -
+ + +
+ + Round {round} + + + {verdict.replace(/_/g, " ")} + + {outcome && ( + + {outcome.replace(/_/g, " ")} + + )} +
+
+ {findings && ( + + {findings} + + )} +
); })}
@@ -176,27 +363,40 @@ function PlanSection({ plan }: { plan: Record }) { if (!summary && !phases) { return ( -
{JSON.stringify(plan, null, 2)}
+
+        {JSON.stringify(plan, null, 2)}
+      
); } return ( -
- {summary && {summary}} +
+ {summary && ( +
+ {summary} +
+ )} {phases && ( -
    +
      {phases.map((p, i) => { const ph = p && typeof p === "object" ? (p as Record) : {}; const name = asText(ph.name) || `Phase ${i + 1}`; const steps = Array.isArray(ph.steps) ? ph.steps : []; return ( -
    1. -
      {name}
      +
    2. +

      + {name} +

      {steps.length > 0 && ( -
        +
          {steps.map((s, j) => ( -
        • {asText(s)}
        • +
        • + {asText(s)} +
        • ))}
        )} @@ -206,11 +406,11 @@ function PlanSection({ plan }: { plan: Record }) {
    )} {extra.length > 0 && ( -
    +
    {extra.map(([k, val]) => ( -
    -
    {k}
    -
    {asText(val)}
    +
    +
    {k}
    +
    {asText(val)}
    ))}
    diff --git a/agent-team/web/src/components/TaskList.test.tsx b/agent-team/web/src/components/TaskList.test.tsx deleted file mode 100644 index a3e0f94..0000000 --- a/agent-team/web/src/components/TaskList.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { TaskList } from "./TaskList"; -import type { StateResponse, Topology } from "../api"; - -const TOPO: Topology = { - trees: [ - { id: "core", label: "Core" }, - { id: "sdlc", label: "SDLC" }, - ], - nodes: [ - { id: "intake", label: "Intake", agent: "coordinator", tree: "core", kind: "phase", gated: false }, - { id: "plan", label: "Plan", agent: "Claude", tree: "sdlc", kind: "phase", gated: false }, - ], - edges: [], -}; - -function makeState(): StateResponse { - return { - ok: true, - error: null, - generated_at: "now", - db_path: "x", - tasks: [ - { thread_id: "aaaa1111", short_id: "aaaa1111", task: "remediate CVE", current_phase: "plan", status: "active", waiting: false, waiting_since: null }, - { thread_id: "bbbb2222", short_id: "bbbb2222", task: "fix login bug", current_phase: "intake", status: "parked", waiting: false, waiting_since: null }, - ], - waiting: [], - question_counts: {}, - summary: { active: 1, parked: 1, waiting: 0, open_questions: 0, total: 2 }, - budget: { available: false }, - nodes: {}, - }; -} - -describe("TaskList", () => { - it("renders all tasks", () => { - render( - {}} onClearNodeFilter={() => {}} />, - ); - expect(screen.getByText("remediate CVE")).toBeInTheDocument(); - expect(screen.getByText("fix login bug")).toBeInTheDocument(); - }); - - it("filters by status", () => { - render( - {}} onClearNodeFilter={() => {}} />, - ); - fireEvent.change(screen.getByLabelText("Filter by status"), { target: { value: "parked" } }); - expect(screen.queryByText("remediate CVE")).not.toBeInTheDocument(); - expect(screen.getByText("fix login bug")).toBeInTheDocument(); - }); - - it("honors a node filter (map click)", () => { - render( - {}} onClearNodeFilter={() => {}} />, - ); - expect(screen.getByText("remediate CVE")).toBeInTheDocument(); - expect(screen.queryByText("fix login bug")).not.toBeInTheDocument(); - }); - - it("fires onSelect when a row is clicked", () => { - const onSelect = vi.fn(); - render( - {}} />, - ); - fireEvent.click(screen.getByText("remediate CVE")); - expect(onSelect).toHaveBeenCalledWith("aaaa1111"); - }); -}); diff --git a/agent-team/web/src/components/TaskList.tsx b/agent-team/web/src/components/TaskList.tsx deleted file mode 100644 index 50088c9..0000000 --- a/agent-team/web/src/components/TaskList.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useMemo, useState } from "react"; -import type { StateResponse, TaskView, Topology } from "../api"; - -// Mirror of the backend phase->node mapping (topology.PHASE_TO_NODE) so the list -// can resolve a task's tree and honor a map-node filter. Identity except the P3 -// build/verify vertices. -const PHASE_TO_NODE: Record = { - build: "build_node", - verify: "verify_node", -}; -const phaseToNode = (phase: string): string => PHASE_TO_NODE[phase] ?? phase; - -interface Props { - state: StateResponse | null; - topology: Topology | null; - selectedId: string | null; - nodeFilter: string | null; - onSelect: (threadId: string) => void; - onClearNodeFilter: () => void; -} - -const STATUS_OPTIONS = ["all", "active", "waiting_human", "parked", "done", "failed"]; - -export function TaskList({ - state, - topology, - selectedId, - nodeFilter, - onSelect, - onClearNodeFilter, -}: Props) { - const [query, setQuery] = useState(""); - const [status, setStatus] = useState("all"); - const [tree, setTree] = useState("all"); - - const treeByNode = useMemo(() => { - const m: Record = {}; - for (const n of topology?.nodes ?? []) m[n.id] = n.tree; - return m; - }, [topology]); - - const tasks = state?.tasks ?? []; - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - return tasks.filter((t) => { - if (nodeFilter && phaseToNode(t.current_phase) !== nodeFilter) return false; - if (status !== "all" && t.status !== status) return false; - if (tree !== "all" && treeByNode[phaseToNode(t.current_phase)] !== tree) - return false; - if (q && !(`${t.task} ${t.short_id}`.toLowerCase().includes(q))) return false; - return true; - }); - }, [tasks, query, status, tree, nodeFilter, treeByNode]); - - return ( - - ); -} - -function TaskRow({ - task, - selected, - onSelect, -}: { - task: TaskView; - selected: boolean; - onSelect: (id: string) => void; -}) { - return ( -
  1. onSelect(task.thread_id)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelect(task.thread_id); - } - }} - > -
    - {task.status.replace("_", " ")} - {task.current_phase} - {task.waiting && ⏳} -
    -
    {task.task}
    -
    {task.short_id}
    -
  2. - ); -} diff --git a/agent-team/web/src/components/TopBar.tsx b/agent-team/web/src/components/TopBar.tsx index 8e872dc..57820f0 100644 --- a/agent-team/web/src/components/TopBar.tsx +++ b/agent-team/web/src/components/TopBar.tsx @@ -1,3 +1,5 @@ +import { cn } from "@/lib/utils"; +import { formatEastern } from "@/lib/time"; import type { StateResponse } from "../api"; interface Props { @@ -6,36 +8,119 @@ interface Props { generatedAt: string | null; } -const CHIPS: Array<{ key: keyof StateResponse["summary"]; label: string; cls: string }> = - [ - { key: "active", label: "Active", cls: "active" }, - { key: "waiting", label: "Awaiting human", cls: "waiting" }, - { key: "parked", label: "Parked", cls: "parked" }, - { key: "open_questions", label: "Open questions", cls: "open" }, - { key: "total", label: "Total", cls: "total" }, - ]; +interface ChipDef { + key: keyof StateResponse["summary"]; + label: string; + numCls: string; + bgCls: string; +} -// Top status bar: summary chips, a connection/health dot, and the snapshot clock. +const CHIPS: ChipDef[] = [ + { + key: "active", + label: "ACTIVE", + numCls: "text-status-active", + bgCls: "bg-status-active-bg", + }, + { + key: "waiting", + label: "AWAITING HUMAN", + numCls: "text-status-waiting", + bgCls: "bg-status-waiting-bg", + }, + { + key: "parked", + label: "PARKED", + numCls: "text-status-parked", + bgCls: "bg-status-parked-bg", + }, + { + key: "open_questions", + label: "OPEN QUESTIONS", + numCls: "text-status-waiting", + bgCls: "bg-status-waiting-bg", + }, + { + key: "total", + label: "TOTAL", + numCls: "text-muted-foreground", + bgCls: "bg-muted", + }, +]; + +// Top status bar: brand, summary chips, connection health, and snapshot clock. export function TopBar({ state, connected, generatedAt }: Props) { const summary = state?.summary; + const snapshot = formatEastern(generatedAt); + const title = connected + ? `Live — snapshot at ${snapshot}` + : `Disconnected — last snapshot at ${snapshot}`; + return ( -
    -
    - - agent-team - pipeline +
    + {/* Brand */} +
    + + + agent-team + + + pipeline +
    -
    + + {/* Divider */} +
    + + {/* Summary chips */} +
    {CHIPS.map((c) => ( -
    - {summary ? summary[c.key] : "–"} - {c.label} +
    + + {summary ? summary[c.key] : "–"} + + + {c.label} +
    ))}
    -
    - - {generatedAt ?? "—"} + + {/* Health indicator */} +
    + + + {snapshot} +
    ); diff --git a/agent-team/web/src/components/ui/badge.tsx b/agent-team/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..bec48a1 --- /dev/null +++ b/agent-team/web/src/components/ui/badge.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold leading-none", + { + variants: { + variant: { + default: "bg-secondary text-secondary-foreground", + outline: "border border-border text-muted-foreground", + // Pipeline status variants — tinted surface + bright foreground. + active: "bg-status-active-bg text-status-active", + waiting: "bg-status-waiting-bg text-status-waiting", + parked: "bg-status-parked-bg text-status-parked", + done: "bg-status-done-bg text-status-done", + failed: "bg-status-failed-bg text-status-failed", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + + ); +} + +/** + * Map a backend task status / verdict string to a Badge variant. Centralized so + * TaskCard, the Board, and TaskDrawer agree on color. + */ +export function statusVariant( + status: string, +): NonNullable { + switch (status) { + case "active": + return "active"; + case "waiting_human": + case "waiting": + return "waiting"; + case "parked": + case "request_changes": + return "parked"; + case "done": + case "approve": + case "approved": + case "pass": + return status === "done" ? "done" : "active"; + case "failed": + return "failed"; + default: + return "outline"; + } +} + +export { Badge, badgeVariants }; diff --git a/agent-team/web/src/components/ui/button.tsx b/agent-team/web/src/components/ui/button.tsx new file mode 100644 index 0000000..c899f11 --- /dev/null +++ b/agent-team/web/src/components/ui/button.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-6", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/agent-team/web/src/components/ui/card.tsx b/agent-team/web/src/components/ui/card.tsx new file mode 100644 index 0000000..933e30a --- /dev/null +++ b/agent-team/web/src/components/ui/card.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardTitle.displayName = "CardTitle"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +CardContent.displayName = "CardContent"; + +export { Card, CardHeader, CardTitle, CardContent }; diff --git a/agent-team/web/src/components/ui/collapsible.tsx b/agent-team/web/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..a43a135 --- /dev/null +++ b/agent-team/web/src/components/ui/collapsible.tsx @@ -0,0 +1,7 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/agent-team/web/src/components/ui/input.tsx b/agent-team/web/src/components/ui/input.tsx new file mode 100644 index 0000000..c0c0131 --- /dev/null +++ b/agent-team/web/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef< + HTMLInputElement, + React.InputHTMLAttributes +>(({ className, type, ...props }, ref) => ( + +)); +Input.displayName = "Input"; + +export { Input }; diff --git a/agent-team/web/src/components/ui/scroll-area.tsx b/agent-team/web/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..721854e --- /dev/null +++ b/agent-team/web/src/components/ui/scroll-area.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import { cn } from "@/lib/utils"; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/agent-team/web/src/components/ui/select.tsx b/agent-team/web/src/components/ui/select.tsx new file mode 100644 index 0000000..cd93fa5 --- /dev/null +++ b/agent-team/web/src/components/ui/select.tsx @@ -0,0 +1,85 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:truncate", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +export { + Select, + SelectValue, + SelectTrigger, + SelectContent, + SelectItem, +}; diff --git a/agent-team/web/src/components/ui/separator.tsx b/agent-team/web/src/components/ui/separator.tsx new file mode 100644 index 0000000..475b6d8 --- /dev/null +++ b/agent-team/web/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/agent-team/web/src/components/ui/tabs.tsx b/agent-team/web/src/components/ui/tabs.tsx new file mode 100644 index 0000000..c3631fc --- /dev/null +++ b/agent-team/web/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/agent-team/web/src/components/ui/tooltip.tsx b/agent-team/web/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..ab11e28 --- /dev/null +++ b/agent-team/web/src/components/ui/tooltip.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { cn } from "@/lib/utils"; + +const TooltipProvider = TooltipPrimitive.Provider; +const Tooltip = TooltipPrimitive.Root; +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/agent-team/web/src/index.css b/agent-team/web/src/index.css new file mode 100644 index 0000000..c5cd8d4 --- /dev/null +++ b/agent-team/web/src/index.css @@ -0,0 +1,162 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * Design tokens for the agent-team dashboard. + * + * shadcn/ui HSL semantic tokens (background/foreground/card/primary/…) plus a + * pipeline `status.*` palette (active/waiting/parked/done/failed), each with a + * tinted surface and a bright foreground. Values are ported from the original + * theme.css hex palette so the dark look stays recognizable. Density follows + * vibe-kanban: tight spacing, 0.5rem radius. + */ +@layer base { + :root { + color-scheme: dark; + + --background: 223 23% 6%; /* #0c0e13 */ + --foreground: 225 18% 92%; /* #e6e8ee */ + + --card: 220 19% 13%; /* #1a1e26 */ + --card-foreground: 225 18% 92%; + + --popover: 222 22% 9%; /* #12151c */ + --popover-foreground: 225 18% 92%; + + --primary: 251 100% 68%; /* #7a5cff accent purple */ + --primary-foreground: 223 23% 6%; + + --secondary: 220 19% 13%; + --secondary-foreground: 225 18% 92%; + + --muted: 222 22% 9%; + --muted-foreground: 221 14% 60%; /* #8a93a6 */ + + --accent: 220 19% 16%; /* subtle hover surface */ + --accent-foreground: 225 18% 92%; + + --destructive: 345 86% 73%; /* #f57f9c */ + --destructive-foreground: 223 23% 6%; + + --border: 218 21% 17%; /* #232a36 */ + --input: 221 19% 23%; /* #2f3645 */ + --ring: 251 100% 68%; + + --radius: 0.5rem; + + /* Pipeline status palette (foreground + tinted surface). */ + --status-active: 149 72% 67%; /* #6ee7a8 */ + --status-active-bg: 155 45% 16%; /* #163a2b */ + --status-waiting: 47 87% 70%; /* #f5d76e */ + --status-waiting-bg: 47 45% 16%; /* #3a3216 */ + --status-parked: 24 85% 69%; /* #f3a36e */ + --status-parked-bg: 15 45% 16%; /* #3a1f16 */ + --status-done: 214 86% 73%; /* #7fb3f5 */ + --status-done-bg: 210 35% 17%; /* #1c2b3a */ + --status-failed: 345 86% 73%; /* #f57f9c */ + --status-failed-bg: 343 45% 16%; /* #3a1620 */ + + /* Brand gradient stops (logo dot, accents). */ + --brand-from: 251 100% 68%; + --brand-to: 215 66% 55%; /* #3f7fd8 */ + } + + * { + @apply border-border; + box-sizing: border-box; + } + + html, + body, + #root { + height: 100%; + margin: 0; + } + + body { + @apply bg-background text-foreground; + font-family: theme("fontFamily.sans"); + line-height: 1.45; + -webkit-font-smoothing: antialiased; + } +} + +@layer utilities { + .mono { + font-family: theme("fontFamily.mono"); + } +} + +/* React Flow: hide the attribution badge (LAN-only, offline). */ +.react-flow__attribution { + display: none; +} + +/* + * Markdown content (Q&A answers / review findings / plan summaries), rendered by + * the XSS-safe component. Ported from theme.css onto the new tokens. + */ +.md { + font-size: 0.82rem; + color: hsl(var(--foreground)); + overflow-wrap: anywhere; +} +.md > :first-child { + margin-top: 0; +} +.md > :last-child { + margin-bottom: 0; +} +.md p { + margin: 0.4rem 0; +} +.md ul, +.md ol { + margin: 0.4rem 0; + padding-left: 1.2rem; +} +.md li { + margin: 0.15rem 0; +} +.md strong { + color: hsl(var(--foreground)); + font-weight: 700; +} +.md h1, +.md h2, +.md h3, +.md h4 { + font-size: 0.9rem; + margin: 0.6rem 0 0.3rem; + color: hsl(var(--status-done)); +} +.md code { + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 4px; + padding: 0 0.25rem; + font-size: 0.92em; + font-family: theme("fontFamily.mono"); +} +.md pre { + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 6px; + padding: 0.5rem; + overflow-x: auto; +} +.md pre code { + border: 0; + padding: 0; + background: none; +} +.md a { + color: hsl(var(--status-done)); +} +.md blockquote { + margin: 0.4rem 0; + padding-left: 0.7rem; + border-left: 2px solid hsl(var(--input)); + color: hsl(var(--muted-foreground)); +} diff --git a/agent-team/web/src/lib/time.test.ts b/agent-team/web/src/lib/time.test.ts new file mode 100644 index 0000000..6a930ee --- /dev/null +++ b/agent-team/web/src/lib/time.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { formatEastern, parseServerDate } from "./time"; + +describe("formatEastern", () => { + it("formats the snapshot 'YYYY-MM-DD HH:MM:SS UTC' shape as Eastern 24h (EDT in summer)", () => { + // 22:54:48 UTC on 2026-06-24 is 18:54:48 EDT (UTC-4). + expect(formatEastern("2026-06-24 22:54:48 UTC")).toBe( + "2026-06-24 18:54:48 EDT", + ); + }); + + it("formats ISO offset timestamps as Eastern 24h", () => { + expect(formatEastern("2026-06-24T22:48:10.437876+00:00")).toBe( + "2026-06-24 18:48:10 EDT", + ); + }); + + it("uses EST in winter (DST-aware)", () => { + // 12:00 UTC on 2026-01-15 is 07:00 EST (UTC-5). + expect(formatEastern("2026-01-15 12:00:00 UTC")).toBe( + "2026-01-15 07:00:00 EST", + ); + }); + + it("crosses the day boundary correctly (late UTC -> earlier Eastern, prior day)", () => { + // 02:30 UTC on 2026-06-25 is 22:30 EDT on 2026-06-24. + expect(formatEastern("2026-06-25T02:30:00+00:00")).toBe( + "2026-06-24 22:30:00 EDT", + ); + }); + + it("returns the em-dash for null / undefined / unparseable input", () => { + expect(formatEastern(null)).toBe("—"); + expect(formatEastern(undefined)).toBe("—"); + expect(formatEastern("not a date")).toBe("—"); + }); +}); + +describe("parseServerDate", () => { + it("treats the bare 'UTC'-suffixed shape as UTC", () => { + const d = parseServerDate("2026-06-24 22:54:48 UTC"); + expect(d?.toISOString()).toBe("2026-06-24T22:54:48.000Z"); + }); + + it("parses ISO offsets", () => { + const d = parseServerDate("2026-06-24T22:48:10.000+00:00"); + expect(d?.toISOString()).toBe("2026-06-24T22:48:10.000Z"); + }); + + it("returns null for empty/unparseable input", () => { + expect(parseServerDate(null)).toBeNull(); + expect(parseServerDate("")).toBeNull(); + expect(parseServerDate("nope")).toBeNull(); + }); +}); diff --git a/agent-team/web/src/lib/time.ts b/agent-team/web/src/lib/time.ts new file mode 100644 index 0000000..ae30268 --- /dev/null +++ b/agent-team/web/src/lib/time.ts @@ -0,0 +1,43 @@ +// Time formatting for the dashboard. All backend timestamps are UTC; we render +// them in US Eastern (America/New_York — EDT in summer, EST in winter, DST-aware) +// as 24-hour `YYYY-MM-DD HH:MM:SS ZZZ`. +// +// The backend emits two shapes: +// - "2026-06-24 22:54:48 UTC" (status snapshot generated_at) +// - "2026-06-24T22:48:10.437876+00:00" (ISO transition timestamps) +// parseServerDate normalizes both to a Date; everything else is ISO that the +// JS engine parses natively. + +const EASTERN_FMT = new Intl.DateTimeFormat("en-CA", { + timeZone: "America/New_York", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", // 24-hour, 00–23 (never "24:xx") + timeZoneName: "short", +}); + +/** Parse a backend timestamp (UTC) into a Date, or null if unparseable. */ +export function parseServerDate(raw: string | null | undefined): Date | null { + if (!raw) return null; + let s = String(raw).trim(); + // "YYYY-MM-DD HH:MM:SS UTC" -> ISO "YYYY-MM-DDTHH:MM:SSZ" + if (s.endsWith(" UTC")) s = s.slice(0, -4).trim().replace(" ", "T") + "Z"; + const d = new Date(s); + return Number.isNaN(d.getTime()) ? null : d; +} + +/** + * Format a backend UTC timestamp as Eastern 24-hour time: + * "2026-06-24 18:54:48 EDT". Returns "—" for null/unparseable input. + */ +export function formatEastern(raw: string | null | undefined): string { + const d = parseServerDate(raw); + if (!d) return "—"; + const p: Record = {}; + for (const part of EASTERN_FMT.formatToParts(d)) p[part.type] = part.value; + return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second} ${p.timeZoneName}`; +} diff --git a/agent-team/web/src/lib/utils.ts b/agent-team/web/src/lib/utils.ts new file mode 100644 index 0000000..c6819b6 --- /dev/null +++ b/agent-team/web/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** shadcn/ui class-name helper: merge conditional classes, dedupe Tailwind. */ +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} diff --git a/agent-team/web/src/main.tsx b/agent-team/web/src/main.tsx index db0df62..c2a145c 100644 --- a/agent-team/web/src/main.tsx +++ b/agent-team/web/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; -import "./theme.css"; +import "./index.css"; createRoot(document.getElementById("root")!).render( diff --git a/agent-team/web/src/taskFilters.test.ts b/agent-team/web/src/taskFilters.test.ts new file mode 100644 index 0000000..21ae462 --- /dev/null +++ b/agent-team/web/src/taskFilters.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; +import { filterTasks, phaseToNode } from "./taskFilters"; +import type { TaskView } from "./api"; + +const TREE_BY_NODE: Record = { + intake: "core", + clarify: "core", + plan: "sdlc", + review: "sdlc", + build_node: "sdlc", + verify_node: "sdlc", +}; + +function task(overrides: Partial = {}): TaskView { + return { + thread_id: "aaaa1111", + short_id: "aaaa1111", + task: "remediate CVE-2024-1234", + current_phase: "plan", + status: "active", + waiting: false, + waiting_since: null, + ...overrides, + }; +} + +const BASE_PARAMS = { + query: "", + status: "all", + tree: "all", + nodeFilter: null, + treeByNode: TREE_BY_NODE, +}; + +describe("phaseToNode", () => { + it("returns the phase itself for standard phases", () => { + expect(phaseToNode("intake")).toBe("intake"); + expect(phaseToNode("plan")).toBe("plan"); + expect(phaseToNode("clarify")).toBe("clarify"); + }); + + it("maps build -> build_node and verify -> verify_node", () => { + expect(phaseToNode("build")).toBe("build_node"); + expect(phaseToNode("verify")).toBe("verify_node"); + }); +}); + +describe("filterTasks", () => { + it("returns all tasks when no filters applied", () => { + const tasks = [task(), task({ thread_id: "bbbb2222", short_id: "bbbb2222" })]; + expect(filterTasks(tasks, BASE_PARAMS)).toHaveLength(2); + }); + + it("filters by query matching task description", () => { + const tasks = [ + task({ task: "remediate CVE" }), + task({ thread_id: "bbbb", short_id: "bbbb", task: "fix login bug" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, query: "CVE" }); + expect(result).toHaveLength(1); + expect(result[0].task).toBe("remediate CVE"); + }); + + it("filters by query matching short_id", () => { + const tasks = [ + task({ short_id: "abc123" }), + task({ thread_id: "bbbb", short_id: "xyz789", task: "other task" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, query: "abc" }); + expect(result).toHaveLength(1); + expect(result[0].short_id).toBe("abc123"); + }); + + it("query matching is case-insensitive", () => { + const tasks = [task({ task: "Remediate CVE" })]; + expect(filterTasks(tasks, { ...BASE_PARAMS, query: "remediate" })).toHaveLength(1); + }); + + it("filters by status", () => { + const tasks = [ + task({ status: "active" }), + task({ thread_id: "bbbb", short_id: "bbbb", status: "parked" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, status: "parked" }); + expect(result).toHaveLength(1); + expect(result[0].status).toBe("parked"); + }); + + it("status='all' does not filter by status", () => { + const tasks = [ + task({ status: "active" }), + task({ thread_id: "bbbb", short_id: "bbbb", status: "done" }), + ]; + expect(filterTasks(tasks, { ...BASE_PARAMS, status: "all" })).toHaveLength(2); + }); + + it("filters by tree", () => { + const tasks = [ + task({ current_phase: "intake" }), // core tree + task({ thread_id: "bbbb", short_id: "bbbb", current_phase: "plan" }), // sdlc tree + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, tree: "sdlc" }); + expect(result).toHaveLength(1); + expect(result[0].current_phase).toBe("plan"); + }); + + it("filters by nodeFilter (exact node match)", () => { + const tasks = [ + task({ current_phase: "plan" }), + task({ thread_id: "bbbb", short_id: "bbbb", current_phase: "intake" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, nodeFilter: "plan" }); + expect(result).toHaveLength(1); + expect(result[0].current_phase).toBe("plan"); + }); + + it("nodeFilter resolves build -> build_node", () => { + const tasks = [ + task({ current_phase: "build" }), + task({ thread_id: "bbbb", short_id: "bbbb", current_phase: "plan" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, nodeFilter: "build_node" }); + expect(result).toHaveLength(1); + expect(result[0].current_phase).toBe("build"); + }); + + it("combines query + status filters", () => { + const tasks = [ + task({ task: "fix login", status: "active" }), + task({ thread_id: "b", short_id: "b", task: "fix login", status: "parked" }), + task({ thread_id: "c", short_id: "c", task: "other task", status: "active" }), + ]; + const result = filterTasks(tasks, { ...BASE_PARAMS, query: "fix login", status: "active" }); + expect(result).toHaveLength(1); + expect(result[0].thread_id).toBe("aaaa1111"); + }); + + it("returns empty array when nothing matches", () => { + const tasks = [task()]; + expect(filterTasks(tasks, { ...BASE_PARAMS, query: "nonexistent" })).toHaveLength(0); + }); + + it("handles empty task list", () => { + expect(filterTasks([], BASE_PARAMS)).toHaveLength(0); + }); +}); diff --git a/agent-team/web/src/taskFilters.ts b/agent-team/web/src/taskFilters.ts new file mode 100644 index 0000000..755a423 --- /dev/null +++ b/agent-team/web/src/taskFilters.ts @@ -0,0 +1,47 @@ +// Pure filter helpers — shared by Board and any future list views. +// Lifted from TaskList.tsx so logic is testable in isolation. + +import type { TaskView, Topology } from "./api"; + +// Mirror of the backend phase->node mapping. Identity for most phases; +// build/verify are the only overrides (P3 build-pipeline vertices). +const PHASE_TO_NODE_MAP: Record = { + build: "build_node", + verify: "verify_node", +}; + +export function phaseToNode(phase: string): string { + return PHASE_TO_NODE_MAP[phase] ?? phase; +} + +/** Build a node-id -> tree-id lookup from topology. */ +export function buildTreeByNode(topology: Topology | null): Record { + const m: Record = {}; + for (const n of topology?.nodes ?? []) m[n.id] = n.tree; + return m; +} + +export interface FilterParams { + query: string; + status: string; // "all" | task status value + tree: string; // "all" | tree id + nodeFilter: string | null; + treeByNode: Record; +} + +/** Filter an array of TaskView objects by the given params. */ +export function filterTasks(tasks: TaskView[], params: FilterParams): TaskView[] { + const { query, status, tree, nodeFilter, treeByNode } = params; + const q = query.trim().toLowerCase(); + + return tasks.filter((t) => { + const node = phaseToNode(t.current_phase); + + if (nodeFilter && node !== nodeFilter) return false; + if (status !== "all" && t.status !== status) return false; + if (tree !== "all" && treeByNode[node] !== tree) return false; + if (q && !`${t.task} ${t.short_id}`.toLowerCase().includes(q)) return false; + + return true; + }); +} diff --git a/agent-team/web/src/theme.css b/agent-team/web/src/theme.css deleted file mode 100644 index 15a9bf4..0000000 --- a/agent-team/web/src/theme.css +++ /dev/null @@ -1,227 +0,0 @@ -:root { - --bg: #0c0e13; - --bg-1: #12151c; - --bg-2: #1a1e26; - --border: #232a36; - --border-2: #2f3645; - --text: #e6e8ee; - --text-dim: #8a93a6; - --accent: #7a5cff; - --accent-2: #3f7fd8; - --ok: #2f8f5e; - --ok-bg: #163a2b; - --ok-fg: #6ee7a8; - --warn-bg: #3a3216; - --warn-fg: #f5d76e; - --park-bg: #3a1f16; - --park-fg: #f3a36e; - --done-bg: #1c2b3a; - --done-fg: #7fb3f5; - --fail-bg: #3a1620; - --fail-fg: #f57f9c; - color-scheme: dark; -} - -* { box-sizing: border-box; } -html, body, #root { height: 100%; margin: 0; } -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, - Arial, sans-serif; - background: var(--bg); - color: var(--text); - line-height: 1.45; -} -.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } - -.app { display: flex; flex-direction: column; height: 100%; } -.body { display: grid; grid-template-columns: 320px 1fr 360px; flex: 1; min-height: 0; } - -/* --- Top bar -------------------------------------------------------------- */ -.topbar { - display: flex; align-items: center; gap: 1.5rem; - padding: .6rem 1rem; background: var(--bg-1); - border-bottom: 1px solid var(--border); -} -.brand { display: flex; align-items: baseline; gap: .5rem; } -.brand-dot { - width: 10px; height: 10px; border-radius: 50%; - background: linear-gradient(135deg, var(--accent), var(--accent-2)); - align-self: center; -} -.brand-name { font-weight: 700; letter-spacing: .01em; } -.brand-sub { color: var(--text-dim); font-size: .85rem; } -.chips { display: flex; gap: .6rem; flex: 1; } -.chip { - display: flex; flex-direction: column; align-items: center; - background: var(--bg-2); border: 1px solid var(--border); - border-radius: 8px; padding: .3rem .7rem; min-width: 4.5rem; -} -.chip-num { font-size: 1.25rem; font-weight: 700; } -.chip-lbl { font-size: .66rem; text-transform: uppercase; letter-spacing: .05em; - color: var(--text-dim); } -.chip-active .chip-num { color: var(--ok-fg); } -.chip-waiting .chip-num { color: var(--warn-fg); } -.chip-parked .chip-num { color: var(--park-fg); } -.health { display: flex; align-items: center; gap: .5rem; color: var(--text-dim); - font-size: .8rem; } -.health-dot { width: 8px; height: 8px; border-radius: 50%; } -.health-dot.ok { background: var(--ok-fg); box-shadow: 0 0 6px var(--ok); } -.health-dot.down { background: var(--fail-fg); } - -/* --- Task list ------------------------------------------------------------ */ -.tasklist { background: var(--bg-1); border-right: 1px solid var(--border); - display: flex; flex-direction: column; min-height: 0; } -.tasklist-controls { padding: .75rem; border-bottom: 1px solid var(--border); - display: flex; flex-direction: column; gap: .5rem; } -.search, select { - background: var(--bg-2); color: var(--text); border: 1px solid var(--border-2); - border-radius: 6px; padding: .4rem .55rem; font-size: .85rem; width: 100%; -} -.filters { display: flex; gap: .5rem; } -.filter-chip { background: var(--bg-2); color: var(--accent); cursor: pointer; - border: 1px solid var(--accent); border-radius: 999px; padding: .2rem .6rem; - font-size: .75rem; align-self: flex-start; } -.tasks { list-style: none; margin: 0; padding: .5rem; overflow-y: auto; flex: 1; } -.task-row { background: var(--bg-2); border: 1px solid var(--border); - border-radius: 8px; padding: .55rem .65rem; margin-bottom: .5rem; cursor: pointer; - transition: border-color .15s, transform .05s; } -.task-row:hover { border-color: var(--border-2); } -.task-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } -.task-row.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } -.task-row-top { display: flex; align-items: center; gap: .5rem; margin-bottom: .25rem; } -.task-phase { color: var(--text-dim); font-size: .75rem; } -.task-wait { margin-left: auto; } -.task-desc { font-size: .85rem; overflow: hidden; text-overflow: ellipsis; - display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } -.task-id { color: var(--text-dim); font-size: .72rem; margin-top: .25rem; } -.empty { color: var(--text-dim); font-style: italic; padding: .5rem; } - -/* --- Badges -------------------------------------------------------------- */ -.badge { display: inline-block; padding: .08rem .45rem; border-radius: 999px; - font-size: .72rem; font-weight: 600; } -.badge.active { background: var(--ok-bg); color: var(--ok-fg); } -.badge.waiting_human, .badge.waiting { background: var(--warn-bg); color: var(--warn-fg); } -.badge.parked { background: var(--park-bg); color: var(--park-fg); } -.badge.done { background: var(--done-bg); color: var(--done-fg); } -.badge.failed { background: var(--fail-bg); color: var(--fail-fg); } -.badge.unknown { background: var(--bg-2); color: var(--text-dim); } - -/* --- Map ----------------------------------------------------------------- */ -.center { min-height: 0; min-width: 0; position: relative; } -.map-pane { width: 100%; height: 100%; } -.loading { padding: 2rem; color: var(--text-dim); } -.react-flow__attribution { display: none; } - -.node { - width: 184px; height: 72px; background: var(--bg-2); - border: 1.5px solid var(--border-2); border-radius: 10px; - padding: .5rem .6rem; display: flex; flex-direction: column; justify-content: center; - position: relative; transition: border-color .25s, background .25s, opacity .2s; -} -.node:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -.node-label { font-weight: 700; font-size: .92rem; display: flex; gap: .3rem; - align-items: center; } -.gate-pip { color: var(--warn-fg); font-size: .8rem; } -.node-agent { color: var(--text-dim); font-size: .72rem; margin-top: .15rem; } -.node-count { position: absolute; top: -8px; right: -8px; min-width: 20px; height: 20px; - border-radius: 999px; background: var(--accent); color: #0c0e13; font-weight: 800; - font-size: .72rem; display: flex; align-items: center; justify-content: center; - padding: 0 5px; } -.node-gated-tag { position: absolute; bottom: 4px; right: 8px; font-size: .58rem; - letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim); } -.node.state-active { border-color: var(--ok); background: var(--ok-bg); } -.node.state-awaiting_human { border-color: var(--warn-fg); background: var(--warn-bg); } -.node.state-parked { border-color: var(--park-fg); background: var(--park-bg); } -.node.gated { opacity: .62; } -.node.dimmed { opacity: .28; } -.node.on-path { box-shadow: 0 0 0 2px var(--accent), 0 0 14px rgba(122, 92, 255, .4); } - -/* --- Drawer -------------------------------------------------------------- */ -.drawer { background: var(--bg-1); border-left: 1px solid var(--border); - display: flex; flex-direction: column; min-height: 0; overflow-y: auto; } -.drawer-head { display: flex; align-items: center; justify-content: space-between; - padding: .75rem 1rem; border-bottom: 1px solid var(--border); position: sticky; - top: 0; background: var(--bg-1); } -.drawer-head h2 { font-size: 1rem; margin: 0; color: #9fb4d6; } -.drawer-close { background: none; border: none; color: var(--text-dim); - font-size: 1rem; cursor: pointer; } -.drawer-close:focus-visible { outline: 2px solid var(--accent); } -.drawer-loading, .drawer-error, .drawer-section { padding: 1rem; } -.drawer-error { color: var(--fail-fg); } -.drawer-summary { padding: 1rem; border-bottom: 1px solid var(--border); } -.drawer-task { font-size: .95rem; margin-bottom: .5rem; } -.drawer-meta { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; - font-size: .8rem; color: var(--text-dim); } -.spend { margin-left: auto; color: var(--text); } -.partial-banner { margin-top: .6rem; background: var(--warn-bg); color: var(--warn-fg); - border-radius: 6px; padding: .5rem .6rem; font-size: .76rem; } - -.timeline { list-style: none; margin: 0; padding: 1rem 1rem 0; } -.tl-step { display: flex; gap: .7rem; padding-bottom: 1rem; position: relative; } -.tl-step:not(:last-child)::before { content: ""; position: absolute; left: 5px; - top: 14px; bottom: 0; width: 2px; background: var(--border-2); } -.tl-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--accent); - margin-top: 2px; flex-shrink: 0; z-index: 1; } -.tl-step.open .tl-dot { background: var(--warn-fg); box-shadow: 0 0 6px var(--warn-fg); } -.tl-body { flex: 1; } -.tl-node { font-weight: 700; font-size: .88rem; } -.tl-times { display: flex; justify-content: space-between; color: var(--text-dim); - font-size: .72rem; margin: .15rem 0; } -.tl-dur { color: var(--text); } -.tl-foot { display: flex; gap: .5rem; align-items: center; font-size: .72rem; } -.tl-cost { color: var(--ok-fg); } -.tl-note { color: var(--text-dim); font-style: italic; } -.drawer-section summary { cursor: pointer; color: #9fb4d6; font-weight: 600; - font-size: .85rem; } -.json { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; - padding: .6rem; font-size: .72rem; overflow-x: auto; max-height: 16rem; - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } - -/* --- Loop-back affordance on a node ------------------------------------- */ -.loop-chip { margin-left: .35rem; color: var(--accent); font-weight: 700; - font-size: .9rem; cursor: help; } -.badge.request_changes { background: var(--park-bg); color: var(--park-fg); } -.badge.approve, .badge.approved, .badge.pass { background: var(--ok-bg); color: var(--ok-fg); } - -/* --- Markdown content (Q&A / verdicts / plan) --------------------------- */ -.md { font-size: .82rem; color: var(--text); overflow-wrap: anywhere; } -.md > :first-child { margin-top: 0; } -.md > :last-child { margin-bottom: 0; } -.md p { margin: .4rem 0; } -.md ul, .md ol { margin: .4rem 0; padding-left: 1.2rem; } -.md li { margin: .15rem 0; } -.md strong { color: #fff; font-weight: 700; } -.md h1, .md h2, .md h3, .md h4 { font-size: .9rem; margin: .6rem 0 .3rem; - color: #9fb4d6; } -.md code { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; - padding: 0 .25rem; font-size: .92em; - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } -.md pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; - padding: .5rem; overflow-x: auto; } -.md pre code { border: 0; padding: 0; background: none; } -.md a { color: var(--done-fg); } -.md blockquote { margin: .4rem 0; padding-left: .7rem; border-left: 2px solid var(--border-2); - color: var(--text-dim); } - -/* --- Q&A cards --------------------------------------------------------- */ -.qa-card, .verdict-card { background: var(--bg-2); border: 1px solid var(--border); - border-radius: 8px; padding: .55rem .65rem; margin-bottom: .6rem; } -.qa-turn { font-size: .72rem; font-weight: 700; color: var(--text-dim); - text-transform: uppercase; letter-spacing: .04em; margin-bottom: .3rem; } - -/* --- Verdict cards ----------------------------------------------------- */ -.verdict-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; - margin-bottom: .35rem; } -.verdict-round { font-size: .72rem; font-weight: 700; color: var(--text-dim); } -.verdict-outcome { font-size: .72rem; color: var(--text-dim); } - -/* --- Plan -------------------------------------------------------------- */ -.plan-phases { margin: .5rem 0 0; padding-left: 1.1rem; } -.plan-phase { margin-bottom: .5rem; } -.plan-phase-name { font-weight: 700; font-size: .84rem; } -.plan-steps { margin: .2rem 0 0; padding-left: 1.1rem; } -.plan-steps li { font-size: .8rem; margin: .15rem 0; } -.plan-extra { margin: .6rem 0 0; font-size: .78rem; } -.plan-extra-row { display: flex; gap: .5rem; } -.plan-extra dt { color: var(--text-dim); min-width: 5rem; } -.plan-extra dd { margin: 0; } diff --git a/agent-team/web/tailwind.config.js b/agent-team/web/tailwind.config.js new file mode 100644 index 0000000..bb14c7e --- /dev/null +++ b/agent-team/web/tailwind.config.js @@ -0,0 +1,100 @@ +import animate from "tailwindcss-animate"; + +/** + * Tailwind config for the agent-team dashboard. + * + * Colors are driven by HSL CSS variables defined in src/index.css (shadcn/ui + * convention) so the dark theme stays in one place. On top of the standard + * shadcn tokens we add pipeline `status.*` colors (active/waiting/parked/done/ + * failed) that the Board, MapNode, and badges share. + */ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + // Pipeline status palette — each has a tinted surface (`bg`) and a + // bright foreground (`fg`), mirroring the original theme.css mapping. + status: { + active: "hsl(var(--status-active))", + "active-bg": "hsl(var(--status-active-bg))", + waiting: "hsl(var(--status-waiting))", + "waiting-bg": "hsl(var(--status-waiting-bg))", + parked: "hsl(var(--status-parked))", + "parked-bg": "hsl(var(--status-parked-bg))", + done: "hsl(var(--status-done))", + "done-bg": "hsl(var(--status-done-bg))", + failed: "hsl(var(--status-failed))", + "failed-bg": "hsl(var(--status-failed-bg))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: [ + "-apple-system", + "BlinkMacSystemFont", + "Segoe UI", + "Roboto", + "Helvetica", + "Arial", + "sans-serif", + ], + mono: ["ui-monospace", "SFMono-Regular", "Menlo", "monospace"], + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [animate], +}; diff --git a/agent-team/web/tsconfig.json b/agent-team/web/tsconfig.json index 98df511..654befa 100644 --- a/agent-team/web/tsconfig.json +++ b/agent-team/web/tsconfig.json @@ -15,6 +15,10 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "types": ["node", "vitest/globals", "@testing-library/jest-dom"] }, "include": ["src", "vite.config.ts"] diff --git a/agent-team/web/vite.config.ts b/agent-team/web/vite.config.ts index 320ad12..cbc3927 100644 --- a/agent-team/web/vite.config.ts +++ b/agent-team/web/vite.config.ts @@ -1,4 +1,5 @@ /// +import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; @@ -8,10 +9,45 @@ import react from "@vitejs/plugin-react"; // locally (the dashboard is offline/LAN-only). export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, build: { outDir: "dist", emptyOutDir: true, sourcemap: false, + rollupOptions: { + output: { + // Split heavy vendors into their own chunks so the app/UI code isn't + // re-downloaded when only a component changes, and to clear the 500 kB + // single-chunk warning. Function form (not object) so react-dom and + // transitive deps bucket precisely instead of collapsing into a stub. + manualChunks(id) { + if (!id.includes("node_modules")) return; + if (/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/.test(id)) + return "react"; + if (id.includes("reactflow") || id.includes("dagre")) return "flow"; + if (id.includes("@radix-ui")) return "radix"; + if ( + id.includes("react-markdown") || + id.includes("micromark") || + id.includes("mdast") || + id.includes("hast") || + id.includes("unist") || + id.includes("unified") || + id.includes("remark") || + id.includes("vfile") || + id.includes("property-information") || + id.includes("/decode-named-character-reference/") || + id.includes("character-entities") + ) + return "markdown"; + return "vendor"; + }, + }, + }, }, server: { proxy: {