From e712c731fa36e842c08b2568f345d3118aa73a1e Mon Sep 17 00:00:00 2001 From: OpenCode Date: Sun, 12 Apr 2026 17:10:08 +0800 Subject: [PATCH 01/16] test(ui): add vitest infrastructure for tauri vue app --- .github/workflows/ci.yml | 13 +- .sisyphus/evidence/task-1-ci-step.txt | 2 + .sisyphus/evidence/task-1-npm-test.txt | Bin 0 -> 1248 bytes .../acp-ui-composer-attachments/learnings.md | 22 + package-lock.json | 1150 ++++++++++++++++- package.json | 4 + src/__tests__/smoke.test.ts | 11 + src/test-setup.ts | 8 + vite.config.ts | 20 +- 9 files changed, 1204 insertions(+), 26 deletions(-) create mode 100644 .sisyphus/evidence/task-1-ci-step.txt create mode 100644 .sisyphus/evidence/task-1-npm-test.txt create mode 100644 .sisyphus/notepads/acp-ui-composer-attachments/learnings.md create mode 100644 src/__tests__/smoke.test.ts create mode 100644 src/test-setup.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c126b6..5bdea7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,11 +56,14 @@ jobs: sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - name: Install frontend dependencies - run: npm ci - - - name: Type check - run: npm run build + - name: Install frontend dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Type check + run: npm run build - name: Build Tauri app uses: tauri-apps/tauri-action@v0 diff --git a/.sisyphus/evidence/task-1-ci-step.txt b/.sisyphus/evidence/task-1-ci-step.txt new file mode 100644 index 0000000..5fa0826 --- /dev/null +++ b/.sisyphus/evidence/task-1-ci-step.txt @@ -0,0 +1,2 @@ +CI workflow includes a `Run tests` step before `Type check` in `.github/workflows/ci.yml`. +Step order: Checkout -> Setup Node.js -> Install Rust stable -> Rust cache -> Install frontend dependencies -> Run tests -> Type check -> Build Tauri app. diff --git a/.sisyphus/evidence/task-1-npm-test.txt b/.sisyphus/evidence/task-1-npm-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..ff344e15d57595a0a943a1319b0c76d4dca5c0b6 GIT binary patch literal 1248 zcmbVM%TB^j6r8n*U*P60F_A(k5$eK6LR`A=v2tMuqN22t7WoiASe-eymr@cFF-?2h z^PD-4et+#_52tV#V}b?R_`nS3=psUFcbB>)rYQLgwkz5MnL|OJ+;r0GKEOS_Gdue@ z=9|Mc?r_CZx<1lM+e6Q^k|t)k;u%pI(aKvVYwMJ_GK_G74)Hz{MVpulyyKM!C&VOq z&BI4ku7E5=RMv~nGs`NyBfo;49X#*~+)DfEP?s^s8*@Z!o!-g{p`x{nndhUtlEu`A zSY9S+&>n7iBvmbvwME7ZO+Q}?vH2g9G4U*|MlVD%W#7bNH&*Eet8IMZ_ZzSDPrSnX zD3Zf%HF*w~?DpKgJoC-T3A5%a*rVTq97|qPJX+RzhUyOaX>#mbufkkcH}uFcR=5I| zy6YaJY?}yT&grfgM literal 0 HcmV?d00001 diff --git a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md new file mode 100644 index 0000000..d59fab0 --- /dev/null +++ b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md @@ -0,0 +1,22 @@ +# Learnings + +## [2026-04-12] Initial Context +- Repo uses `vue-tsc --noEmit && vite build` as build script +- No test infrastructure exists — zero tests, no vitest, no @vue/test-utils +- TypeScript strict mode with noUnusedLocals/noUnusedParameters +- `@tauri-apps/plugin-dialog` already a dependency (used in App.vue:67-80 for folder picker) +- `@tauri-apps/plugin-fs` already a dependency (stat() available for file size validation) +- ACP SDK `@agentclientprotocol/sdk` v0.13.1 — PromptRequest.prompt accepts Array +- acp-bridge.ts already imports PromptRequest type and has prompt() method that passes through +- sendPrompt() in session.ts:579-621 creates text-only prompt with `[{type:'text', text}]` +- ChatMessage type in types.ts:38-45 has id, role, content, thought?, timestamp, toolCalls? +- vite.config.ts uses async config function pattern +- CI workflow has "Type check" (npm run build) then "Build Tauri app" steps +- Vitest setup works with async `defineConfig(async () => ({ ... }))` when the `/// ` directive is added at the top of `vite.config.ts` +- `happy-dom` is sufficient for the smoke test and keeps the test environment lightweight +- Tauri plugin modules can be fully mocked in `src/test-setup.ts` with `vi.mock()` so tests do not require the native runtime +[2026-04-12] Attachment validation +- Added pure utility module for attachment metadata validation with extension, size, count, and dedup checks. +- Validation uses case-insensitive path normalization for v1, matching the planned shared contract. +- Vitest coverage includes valid sets, disallowed extensions, oversized files, duplicate paths, over-count, mixed results, and MIME mapping. +- Test setup already exists, so the new test file runs without extra config. diff --git a/package-lock.json b/package-lock.json index 9b9f91c..a7caa17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "acp-ui", - "version": "0.1.0", + "version": "0.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "acp-ui", - "version": "0.1.0", + "version": "0.1.10", "dependencies": { "@agentclientprotocol/sdk": "^0.13.1", "@microsoft/applicationinsights-web": "^3.3.11", @@ -24,8 +24,11 @@ "@tauri-apps/cli": "^2", "@types/node": "^25.1.0", "@vitejs/plugin-vue": "^5.2.1", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.6.3", "typescript": "~5.6.2", "vite": "^6.0.3", + "vitest": "^3.2.4", "vue-tsc": "^2.1.10" } }, @@ -542,6 +545,24 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -755,6 +776,24 @@ "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", "license": "MIT" }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -1368,6 +1407,24 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1381,7 +1438,6 @@ "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1400,6 +1456,131 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.15", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", @@ -1625,6 +1806,27 @@ "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1644,6 +1846,42 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", @@ -1702,6 +1940,43 @@ "balanced-match": "^1.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -1717,12 +1992,53 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1738,6 +2054,21 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1751,6 +2082,67 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1763,6 +2155,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1811,6 +2210,16 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -1834,6 +2243,23 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1849,6 +2275,42 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/happy-dom": { + "version": "17.6.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.6.3.tgz", + "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1865,6 +2327,23 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", @@ -1877,14 +2356,76 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { "node": ">=6" } @@ -1918,6 +2459,20 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1970,6 +2525,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2005,6 +2570,13 @@ "pathe": "^2.0.1" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -2029,6 +2601,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -2036,12 +2631,49 @@ "dev": true, "license": "MIT" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -2071,7 +2703,6 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -2127,6 +2758,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -2213,6 +2851,62 @@ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2231,6 +2925,137 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -2243,6 +3068,20 @@ "node": ">=16" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2259,6 +3098,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2272,7 +3141,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2331,7 +3199,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2401,6 +3268,102 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -2413,7 +3376,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -2430,6 +3392,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-router": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.1.tgz", @@ -2531,12 +3500,163 @@ "typescript": ">=5.0.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", diff --git a/package.json b/package.json index 9f5478e..dc5e14b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", + "test": "vitest run", "preview": "vite preview", "tauri": "tauri" }, @@ -26,8 +27,11 @@ "@tauri-apps/cli": "^2", "@types/node": "^25.1.0", "@vitejs/plugin-vue": "^5.2.1", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.6.3", "typescript": "~5.6.2", "vite": "^6.0.3", + "vitest": "^3.2.4", "vue-tsc": "^2.1.10" } } diff --git a/src/__tests__/smoke.test.ts b/src/__tests__/smoke.test.ts new file mode 100644 index 0000000..cb983c2 --- /dev/null +++ b/src/__tests__/smoke.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest'; +import type { ChatMessage } from '../lib/types'; + +describe('smoke test', () => { + it('can import types', () => { + const msg: ChatMessage = { + id: '1', role: 'user', content: 'hello', timestamp: Date.now(), + }; + expect(msg.role).toBe('user'); + }); +}); diff --git a/src/test-setup.ts b/src/test-setup.ts new file mode 100644 index 0000000..c2a7272 --- /dev/null +++ b/src/test-setup.ts @@ -0,0 +1,8 @@ +import { vi } from 'vitest'; + +vi.mock('@tauri-apps/plugin-dialog', () => ({ open: vi.fn(), save: vi.fn(), message: vi.fn(), ask: vi.fn(), confirm: vi.fn() })); +vi.mock('@tauri-apps/plugin-fs', () => ({ readTextFile: vi.fn(), writeTextFile: vi.fn(), stat: vi.fn(), exists: vi.fn() })); +vi.mock('@tauri-apps/plugin-store', () => ({ Store: vi.fn(), load: vi.fn() })); +vi.mock('@tauri-apps/plugin-opener', () => ({ openUrl: vi.fn() })); +vi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn(), emit: vi.fn() })); +vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() })); diff --git a/vite.config.ts b/vite.config.ts index feb91a5..2d78fa5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,22 @@ -import { defineConfig } from "vite"; -import vue from "@vitejs/plugin-vue"; +/// + +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ -export default defineConfig(async () => ({ - plugins: [vue()], - - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` +export default defineConfig(async () => ({ + plugins: [vue()], + test: { + environment: "happy-dom", + globals: true, + include: ["src/**/*.test.ts"], + setupFiles: ["./src/test-setup.ts"], + }, + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors clearScreen: false, From 98649af3e462805177addb57ab827c875d4a8f87 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 12 Apr 2026 17:11:03 +0800 Subject: [PATCH 02/16] feat(attachments): add shared attachment model and validation helpers Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .sisyphus/evidence/task-2-validation-fail.txt | 2 + .sisyphus/evidence/task-2-validation-pass.txt | 3 + .../acp-ui-composer-attachments/learnings.md | 4 + src/__tests__/attachments.test.ts | 108 ++++++++++++++++ src/lib/attachments.ts | 118 ++++++++++++++++++ src/lib/types.ts | 19 ++- 6 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 .sisyphus/evidence/task-2-validation-fail.txt create mode 100644 .sisyphus/evidence/task-2-validation-pass.txt create mode 100644 src/__tests__/attachments.test.ts create mode 100644 src/lib/attachments.ts diff --git a/.sisyphus/evidence/task-2-validation-fail.txt b/.sisyphus/evidence/task-2-validation-fail.txt new file mode 100644 index 0000000..a42eeae --- /dev/null +++ b/.sisyphus/evidence/task-2-validation-fail.txt @@ -0,0 +1,2 @@ +FAIL: npm test +Result: none diff --git a/.sisyphus/evidence/task-2-validation-pass.txt b/.sisyphus/evidence/task-2-validation-pass.txt new file mode 100644 index 0000000..e98a9b7 --- /dev/null +++ b/.sisyphus/evidence/task-2-validation-pass.txt @@ -0,0 +1,3 @@ +PASS: npm test +Result: 2 test files passed, 10 tests passed +Attachment validation tests: passed diff --git a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md index d59fab0..9614b79 100644 --- a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md +++ b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md @@ -20,3 +20,7 @@ - Validation uses case-insensitive path normalization for v1, matching the planned shared contract. - Vitest coverage includes valid sets, disallowed extensions, oversized files, duplicate paths, over-count, mixed results, and MIME mapping. - Test setup already exists, so the new test file runs without extra config. +- Centralized AttachmentRef -> ACP resource_link serialization in src/lib/attachments.ts to keep file URI encoding consistent across callers. +- ACP PromptRequest content blocks accept resource_link with nested resource { uri, name, mimeType }, so session prompt payloads can carry attachment metadata without file content. +- Windows drive-letter paths are safely serialized with encodeURIComponent on path segments, yielding file:///C%3A/... URIs. + diff --git a/src/__tests__/attachments.test.ts b/src/__tests__/attachments.test.ts new file mode 100644 index 0000000..04afc33 --- /dev/null +++ b/src/__tests__/attachments.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; +import { ALLOWED_EXTENSIONS, MAX_FILE_COUNT, MAX_FILE_SIZE, getMimeType, validateAttachments } from '../lib/attachments'; +import type { AttachmentRef } from '../lib/types'; + +function makeAttachment(overrides: Partial & Pick): AttachmentRef { + return { + mimeType: getMimeType(overrides.name), + size: 1024, + source: 'local', + ...overrides, + }; +} + +describe('attachments helpers', () => { + it('allows a valid multi-file set', () => { + const candidates = [ + makeAttachment({ id: '1', name: 'notes.md', path: '/files/notes.md' }), + makeAttachment({ id: '2', name: 'data.json', path: '/files/data.json' }), + makeAttachment({ id: '3', name: 'table.csv', path: '/files/table.csv' }), + ]; + + const result = validateAttachments(candidates, []); + + expect(result.valid).toHaveLength(3); + expect(result.rejected).toEqual([]); + }); + + it('rejects disallowed extensions', () => { + const result = validateAttachments([makeAttachment({ id: '1', name: 'virus.exe', path: '/files/virus.exe' })], []); + + expect(result.valid).toEqual([]); + expect(result.rejected).toEqual([{ name: 'virus.exe', reason: 'disallowed extension' }]); + }); + + it('rejects oversized files', () => { + const result = validateAttachments( + [makeAttachment({ id: '1', name: 'big.pdf', path: '/files/big.pdf', size: MAX_FILE_SIZE + 1 })], + [], + ); + + expect(result.valid).toEqual([]); + expect(result.rejected).toEqual([{ name: 'big.pdf', reason: 'file too large' }]); + }); + + it('rejects duplicate paths', () => { + const existing = [makeAttachment({ id: 'existing', name: 'report.md', path: 'C:/Docs/Report.md' })]; + const result = validateAttachments( + [makeAttachment({ id: '1', name: 'report.md', path: 'c:/docs/report.md' })], + existing, + ); + + expect(result.valid).toEqual([]); + expect(result.rejected).toEqual([{ name: 'report.md', reason: 'duplicate path' }]); + }); + + it('rejects attachments over the max count', () => { + const existing = Array.from({ length: MAX_FILE_COUNT }, (_, index) => + makeAttachment({ id: `existing-${index}`, name: `existing-${index}.txt`, path: `/files/existing-${index}.txt` }), + ); + + const result = validateAttachments([ + makeAttachment({ id: '1', name: 'extra.txt', path: '/files/extra.txt' }), + ], existing); + + expect(result.valid).toEqual([]); + expect(result.rejected).toEqual([{ name: 'extra.txt', reason: 'too many attachments' }]); + }); + + it('maps extensions to mime types', () => { + expect(getMimeType('doc.md')).toBe('text/markdown'); + expect(getMimeType('doc.txt')).toBe('text/plain'); + expect(getMimeType('doc.pdf')).toBe('application/pdf'); + expect(getMimeType('doc.doc')).toBe('application/msword'); + expect(getMimeType('doc.docx')).toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + expect(getMimeType('doc.json')).toBe('application/json'); + expect(getMimeType('doc.csv')).toBe('text/csv'); + expect(getMimeType('doc.unknown')).toBe('application/octet-stream'); + }); + + it('returns valid and rejected arrays for mixed input', () => { + const result = validateAttachments( + [ + makeAttachment({ id: '1', name: 'ok.md', path: '/files/ok.md' }), + makeAttachment({ id: '2', name: 'bad.exe', path: '/files/bad.exe' }), + makeAttachment({ id: '3', name: 'large.pdf', path: '/files/large.pdf', size: MAX_FILE_SIZE + 1 }), + ], + [], + ); + + expect(result.valid).toHaveLength(1); + expect(result.valid[0].name).toBe('ok.md'); + expect(result.rejected).toEqual([ + { name: 'bad.exe', reason: 'disallowed extension' }, + { name: 'large.pdf', reason: 'file too large' }, + ]); + }); + + it('returns empty arrays for no candidates', () => { + const result = validateAttachments([], []); + + expect(result.valid).toEqual([]); + expect(result.rejected).toEqual([]); + }); + + it('exports allowed extension list', () => { + expect(ALLOWED_EXTENSIONS).toContain('.md'); + }); +}); diff --git a/src/lib/attachments.ts b/src/lib/attachments.ts new file mode 100644 index 0000000..b40228c --- /dev/null +++ b/src/lib/attachments.ts @@ -0,0 +1,118 @@ +import type { ContentBlock } from '@agentclientprotocol/sdk'; +import type { AttachmentRef } from './types'; + +export const ALLOWED_EXTENSIONS = ['.md', '.txt', '.pdf', '.doc', '.docx', '.json', '.csv'] as const; +export const MAX_FILE_SIZE = 10 * 1024 * 1024; +export const MAX_FILE_COUNT = 10; + +const MIME_TYPES: Record = { + '.md': 'text/markdown', + '.txt': 'text/plain', + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.json': 'application/json', + '.csv': 'text/csv', +}; + +export interface ValidationResult { + valid: AttachmentRef[]; + rejected: Array<{ name: string; reason: string }>; +} + +function getFileNameFromPath(filename: string): string { + return filename.split(/[\\/]/).pop() ?? filename; +} + +function getExtension(filename: string): string { + const baseName = getFileNameFromPath(filename); + const lastDotIndex = baseName.lastIndexOf('.'); + + if (lastDotIndex <= 0) { + return ''; + } + + return baseName.slice(lastDotIndex).toLowerCase(); +} + +function normalizePath(path: string): string { + return path.trim().replace(/\\/g, '/').toLowerCase(); +} + +function isAllowedExtension(filename: string): boolean { + const extension = getExtension(filename); + return ALLOWED_EXTENSIONS.some((allowedExtension) => allowedExtension === extension); +} + +export function getMimeType(filename: string): string { + const extension = getExtension(filename); + return MIME_TYPES[extension] ?? 'application/octet-stream'; +} + +export function validateAttachments(candidates: AttachmentRef[], existing: AttachmentRef[]): ValidationResult { + const valid: AttachmentRef[] = []; + const rejected: Array<{ name: string; reason: string }> = []; + const seenPaths = new Set(existing.map((attachment) => normalizePath(attachment.path))); + let totalCount = existing.length; + + for (const candidate of candidates) { + const candidatePath = normalizePath(candidate.path); + + if (!isAllowedExtension(candidate.name)) { + rejected.push({ name: candidate.name, reason: 'disallowed extension' }); + continue; + } + + if (candidate.size > MAX_FILE_SIZE) { + rejected.push({ name: candidate.name, reason: 'file too large' }); + continue; + } + + if (seenPaths.has(candidatePath)) { + rejected.push({ name: candidate.name, reason: 'duplicate path' }); + continue; + } + + if (totalCount >= MAX_FILE_COUNT) { + rejected.push({ name: candidate.name, reason: 'too many attachments' }); + continue; + } + + valid.push(candidate); + seenPaths.add(candidatePath); + totalCount += 1; + } + + return { valid, rejected }; +} + +/** + * Converts a local file path to a file:// URI. + * Handles Windows paths (C:\foo\bar → file:///C:/foo/bar) + * and encodes special characters (spaces, unicode). + */ +export function toFileUri(filePath: string): string { + let normalized = filePath.replace(/\\/g, '/'); + + if (!normalized.startsWith('/')) { + normalized = '/' + normalized; + } + + const encoded = normalized.split('/').map((segment) => encodeURIComponent(segment)).join('/'); + return 'file://' + encoded; +} + +/** + * Serializes AttachmentRef[] into ACP-compatible content blocks. + * Each attachment becomes a resource_link content block. + */ +export function serializeAttachmentsToContentBlocks(attachments: AttachmentRef[]): ContentBlock[] { + return attachments.map((att) => ({ + type: 'resource_link' as const, + resource: { + uri: toFileUri(att.path), + name: att.name, + mimeType: att.mimeType, + }, + })); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index bd10ee6..1648a01 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -84,8 +84,17 @@ export interface SlashCommand { } // Models -export interface ModelInfo { - modelId: string; - name: string; - description?: string; -} +export interface ModelInfo { + modelId: string; + name: string; + description?: string; +} + +export interface AttachmentRef { + id: string; + name: string; + path: string; + mimeType: string; + size: number; + source: 'local' | 'web'; +} From ec3c1f16c023a08ff13f1701af17916e0491b8fa Mon Sep 17 00:00:00 2001 From: OpenCode Date: Sun, 12 Apr 2026 17:11:24 +0800 Subject: [PATCH 03/16] feat(attachments): add acp resource link serialization helper --- .sisyphus/evidence/task-4-serializer-edge.txt | 2 + .sisyphus/evidence/task-4-serializer-pass.txt | 1 + .../acp-ui-composer-attachments/learnings.md | 4 + src/__tests__/attachments-serializer.test.ts | 80 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 .sisyphus/evidence/task-4-serializer-edge.txt create mode 100644 .sisyphus/evidence/task-4-serializer-pass.txt create mode 100644 src/__tests__/attachments-serializer.test.ts diff --git a/.sisyphus/evidence/task-4-serializer-edge.txt b/.sisyphus/evidence/task-4-serializer-edge.txt new file mode 100644 index 0000000..ebae296 --- /dev/null +++ b/.sisyphus/evidence/task-4-serializer-edge.txt @@ -0,0 +1,2 @@ +Windows path encoding: file:///C%3A/Users/docs/file.md +Unicode/space encoding: file:///home/user/%E7%9F%A5%E8%AF%86%E5%BA%93%20%E6%96%87%E6%A1%A3.pdf diff --git a/.sisyphus/evidence/task-4-serializer-pass.txt b/.sisyphus/evidence/task-4-serializer-pass.txt new file mode 100644 index 0000000..0de1abc --- /dev/null +++ b/.sisyphus/evidence/task-4-serializer-pass.txt @@ -0,0 +1 @@ +npm test passed: 4 files, 21 tests diff --git a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md index 9614b79..2b9423b 100644 --- a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md +++ b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md @@ -20,6 +20,10 @@ - Validation uses case-insensitive path normalization for v1, matching the planned shared contract. - Vitest coverage includes valid sets, disallowed extensions, oversized files, duplicate paths, over-count, mixed results, and MIME mapping. - Test setup already exists, so the new test file runs without extra config. +\n+[2026-04-12] File picker adapter +- Added a mockable `_rawPicker` seam plus `_setRawPicker`/`_resetRawPicker` helpers so tests can bypass native dialogs. +- `pickFiles()` normalizes `open({ multiple: true })` results into `AttachmentRef[]`, derives names from path separators, and safely falls back to `size = 0` when `stat()` fails. +- Test coverage verifies multi-file, single-file, cancel/null, empty selection, and stat failure behavior. - Centralized AttachmentRef -> ACP resource_link serialization in src/lib/attachments.ts to keep file URI encoding consistent across callers. - ACP PromptRequest content blocks accept resource_link with nested resource { uri, name, mimeType }, so session prompt payloads can carry attachment metadata without file content. - Windows drive-letter paths are safely serialized with encodeURIComponent on path segments, yielding file:///C%3A/... URIs. diff --git a/src/__tests__/attachments-serializer.test.ts b/src/__tests__/attachments-serializer.test.ts new file mode 100644 index 0000000..00b0abf --- /dev/null +++ b/src/__tests__/attachments-serializer.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { serializeAttachmentsToContentBlocks, toFileUri } from '../lib/attachments'; +import type { AttachmentRef } from '../lib/types'; + +function makeAttachment(overrides: Partial & Pick): AttachmentRef { + return { + id: overrides.id, + name: overrides.name, + path: overrides.path, + mimeType: overrides.mimeType ?? 'application/octet-stream', + size: overrides.size ?? 1024, + source: overrides.source ?? 'local', + }; +} + +describe('attachment serialization', () => { + it('serializes attachments into resource_link content blocks', () => { + const attachments = [ + makeAttachment({ id: '1', name: 'notes.md', path: '/files/notes.md', mimeType: 'text/markdown' }), + makeAttachment({ id: '2', name: 'data.json', path: '/files/data.json', mimeType: 'application/json' }), + ]; + + const blocks = serializeAttachmentsToContentBlocks(attachments); + + expect(blocks).toEqual([ + { + type: 'resource_link', + resource: { + uri: 'file:///files/notes.md', + name: 'notes.md', + mimeType: 'text/markdown', + }, + }, + { + type: 'resource_link', + resource: { + uri: 'file:///files/data.json', + name: 'data.json', + mimeType: 'application/json', + }, + }, + ]); + }); + + it('handles Windows paths', () => { + expect(toFileUri('C:\\Users\\docs\\file.md')).toBe('file:///C%3A/Users/docs/file.md'); + }); + + it('handles Unix paths', () => { + expect(toFileUri('/home/user/docs/file.txt')).toBe('file:///home/user/docs/file.txt'); + }); + + it('encodes spaces and unicode characters', () => { + expect(toFileUri('/home/user/知识库 文档.pdf')).toBe('file:///home/user/%E7%9F%A5%E8%AF%86%E5%BA%93%20%E6%96%87%E6%A1%A3.pdf'); + }); + + it('returns an empty array for no attachments', () => { + expect(serializeAttachmentsToContentBlocks([])).toEqual([]); + }); + + it('serializes Windows file names with encoded uris', () => { + const blocks = serializeAttachmentsToContentBlocks([ + makeAttachment({ + id: '3', + name: '知识库 文档.pdf', + path: 'C:\\Users\\me\\知识库 文档.pdf', + mimeType: 'application/pdf', + }), + ]); + + expect(blocks[0]).toEqual({ + type: 'resource_link', + resource: { + uri: 'file:///C%3A/Users/me/%E7%9F%A5%E8%AF%86%E5%BA%93%20%E6%96%87%E6%A1%A3.pdf', + name: '知识库 文档.pdf', + mimeType: 'application/pdf', + }, + }); + }); +}); From c5db8643560aea654a35daaed3be309a2a41f208 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 12 Apr 2026 17:11:39 +0800 Subject: [PATCH 04/16] feat(attachments): add platform attachment normalization helper --- src/__tests__/file-picker.test.ts | 83 +++++++++++++++++++++++++++++++ src/lib/file-picker.ts | 72 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 src/__tests__/file-picker.test.ts create mode 100644 src/lib/file-picker.ts diff --git a/src/__tests__/file-picker.test.ts b/src/__tests__/file-picker.test.ts new file mode 100644 index 0000000..59793c7 --- /dev/null +++ b/src/__tests__/file-picker.test.ts @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { stat } from '@tauri-apps/plugin-fs'; +import { _resetRawPicker, _setRawPicker, pickFiles } from '../lib/file-picker'; + +vi.mock('@tauri-apps/plugin-dialog', () => ({ open: vi.fn() })); +vi.mock('@tauri-apps/plugin-fs', () => ({ stat: vi.fn() })); + +beforeEach(() => { + vi.mocked(stat).mockReset(); + _resetRawPicker(); +}); + +describe('pickFiles', () => { + it('normalizes multi-file selections into attachment refs', async () => { + _setRawPicker(async () => ['C:/docs/readme.md', 'C:/docs/data.csv']); + vi.mocked(stat).mockResolvedValueOnce({ size: 11 } as Awaited>); + vi.mocked(stat).mockResolvedValueOnce({ size: 22 } as Awaited>); + + const attachments = await pickFiles(); + + expect(attachments).toHaveLength(2); + expect(attachments[0]).toMatchObject({ + name: 'readme.md', + path: 'C:/docs/readme.md', + mimeType: 'text/markdown', + size: 11, + source: 'local', + }); + expect(attachments[1]).toMatchObject({ + name: 'data.csv', + path: 'C:/docs/data.csv', + mimeType: 'text/csv', + size: 22, + source: 'local', + }); + expect(attachments[0].id).toBeTypeOf('string'); + expect(attachments[1].id).toBeTypeOf('string'); + }); + + it('returns an empty array for cancel or null selection', async () => { + _setRawPicker(async () => null); + + await expect(pickFiles()).resolves.toEqual([]); + }); + + it('returns an empty array for empty selection', async () => { + _setRawPicker(async () => []); + + await expect(pickFiles()).resolves.toEqual([]); + }); + + it('normalizes a single-file selection', async () => { + _setRawPicker(async () => ['D:/file.pdf']); + vi.mocked(stat).mockResolvedValueOnce({ size: 4096 } as Awaited>); + + const attachments = await pickFiles(); + + expect(attachments).toHaveLength(1); + expect(attachments[0]).toMatchObject({ + name: 'file.pdf', + path: 'D:/file.pdf', + mimeType: 'application/pdf', + size: 4096, + source: 'local', + }); + }); + + it('keeps attachments when stat fails', async () => { + _setRawPicker(async () => ['D:/file.pdf']); + vi.mocked(stat).mockRejectedValueOnce(new Error('stat failed')); + + const attachments = await pickFiles(); + + expect(attachments).toHaveLength(1); + expect(attachments[0]).toMatchObject({ + name: 'file.pdf', + path: 'D:/file.pdf', + mimeType: 'application/pdf', + size: 0, + source: 'local', + }); + }); +}); diff --git a/src/lib/file-picker.ts b/src/lib/file-picker.ts new file mode 100644 index 0000000..0f2ffa4 --- /dev/null +++ b/src/lib/file-picker.ts @@ -0,0 +1,72 @@ +import { open } from '@tauri-apps/plugin-dialog'; +import { stat } from '@tauri-apps/plugin-fs'; +import type { AttachmentRef } from './types'; +import { getMimeType, ALLOWED_EXTENSIONS } from './attachments'; + +const defaultRawPicker = async (): Promise => { + const result = await open({ + multiple: true, + title: 'Select Files to Attach', + filters: [ + { + name: 'Documents', + extensions: ALLOWED_EXTENSIONS.map((extension) => extension.slice(1)), + }, + ], + }); + + if (!result) { + return null; + } + + return Array.isArray(result) ? result : [result]; +}; + +// Mockable picker seam for tests. +export let _rawPicker: () => Promise = defaultRawPicker; + +export function _setRawPicker(picker: () => Promise): void { + _rawPicker = picker; +} + +export function _resetRawPicker(): void { + _rawPicker = defaultRawPicker; +} + +function getFileName(filePath: string): string { + return filePath.split(/[\\/]/).pop() ?? filePath; +} + +export async function pickFiles(): Promise { + const paths = await _rawPicker(); + + if (!paths || paths.length === 0) { + return []; + } + + const attachments: AttachmentRef[] = []; + + for (const filePath of paths) { + const name = getFileName(filePath); + const mimeType = getMimeType(name); + + let size = 0; + try { + const fileInfo = await stat(filePath); + size = fileInfo.size; + } catch { + // Keep size at 0 when file metadata is unavailable. + } + + attachments.push({ + id: crypto.randomUUID(), + name, + path: filePath, + mimeType, + size, + source: 'local', + }); + } + + return attachments; +} From db1c0db8b5ea7827c9e3d47e59d94d08c9b94b5a Mon Sep 17 00:00:00 2001 From: OpenCode Date: Sun, 12 Apr 2026 17:15:41 +0800 Subject: [PATCH 05/16] fix(attachments): align resource_link shape with ACP SDK types --- .../acp-ui-composer-attachments/learnings.md | 5 +++- src/__tests__/attachments-serializer.test.ts | 24 +++++++------------ src/lib/attachments.ts | 8 +++---- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md index 2b9423b..9e446f7 100644 --- a/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md +++ b/.sisyphus/notepads/acp-ui-composer-attachments/learnings.md @@ -27,4 +27,7 @@ - Centralized AttachmentRef -> ACP resource_link serialization in src/lib/attachments.ts to keep file URI encoding consistent across callers. - ACP PromptRequest content blocks accept resource_link with nested resource { uri, name, mimeType }, so session prompt payloads can carry attachment metadata without file content. - Windows drive-letter paths are safely serialized with encodeURIComponent on path segments, yielding file:///C%3A/... URIs. - + +- ACP esource_link content blocks use flat top-level fields (uri, name, mimeType); nesting under esource breaks vue-tsc/build even if tests pass. +- The serializer should mirror the SDK's ContentBlock union exactly to keep type-checking aligned with runtime payloads. + diff --git a/src/__tests__/attachments-serializer.test.ts b/src/__tests__/attachments-serializer.test.ts index 00b0abf..def712b 100644 --- a/src/__tests__/attachments-serializer.test.ts +++ b/src/__tests__/attachments-serializer.test.ts @@ -25,19 +25,15 @@ describe('attachment serialization', () => { expect(blocks).toEqual([ { type: 'resource_link', - resource: { - uri: 'file:///files/notes.md', - name: 'notes.md', - mimeType: 'text/markdown', - }, + uri: 'file:///files/notes.md', + name: 'notes.md', + mimeType: 'text/markdown', }, { type: 'resource_link', - resource: { - uri: 'file:///files/data.json', - name: 'data.json', - mimeType: 'application/json', - }, + uri: 'file:///files/data.json', + name: 'data.json', + mimeType: 'application/json', }, ]); }); @@ -70,11 +66,9 @@ describe('attachment serialization', () => { expect(blocks[0]).toEqual({ type: 'resource_link', - resource: { - uri: 'file:///C%3A/Users/me/%E7%9F%A5%E8%AF%86%E5%BA%93%20%E6%96%87%E6%A1%A3.pdf', - name: '知识库 文档.pdf', - mimeType: 'application/pdf', - }, + uri: 'file:///C%3A/Users/me/%E7%9F%A5%E8%AF%86%E5%BA%93%20%E6%96%87%E6%A1%A3.pdf', + name: '知识库 文档.pdf', + mimeType: 'application/pdf', }); }); }); diff --git a/src/lib/attachments.ts b/src/lib/attachments.ts index b40228c..5e699c2 100644 --- a/src/lib/attachments.ts +++ b/src/lib/attachments.ts @@ -109,10 +109,8 @@ export function toFileUri(filePath: string): string { export function serializeAttachmentsToContentBlocks(attachments: AttachmentRef[]): ContentBlock[] { return attachments.map((att) => ({ type: 'resource_link' as const, - resource: { - uri: toFileUri(att.path), - name: att.name, - mimeType: att.mimeType, - }, + uri: toFileUri(att.path), + name: att.name, + mimeType: att.mimeType, })); } From 7afe60b6e6547ed605bef743e1522070c21fc7e3 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 12 Apr 2026 17:23:04 +0800 Subject: [PATCH 06/16] feat(chat): add composer attachment ui --- src/__tests__/chatview-composer.test.ts | 85 +++++++++++++ src/components/ChatView.vue | 162 +++++++++++++++++++++--- 2 files changed, 229 insertions(+), 18 deletions(-) create mode 100644 src/__tests__/chatview-composer.test.ts diff --git a/src/__tests__/chatview-composer.test.ts b/src/__tests__/chatview-composer.test.ts new file mode 100644 index 0000000..5cc8407 --- /dev/null +++ b/src/__tests__/chatview-composer.test.ts @@ -0,0 +1,85 @@ +import { mount } from '@vue/test-utils'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createPinia, setActivePinia } from 'pinia'; +import ChatView from '../components/ChatView.vue'; +import { useSessionStore } from '../stores/session'; +import { _setRawPicker, _resetRawPicker } from '../lib/file-picker'; + +describe('ChatView Composer Attachments', () => { + beforeEach(() => { + setActivePinia(createPinia()); + _resetRawPicker(); + }); + + it('renders attach button', () => { + const wrapper = mount(ChatView); + expect(wrapper.find('.attach-btn').exists()).toBe(true); + }); + + it('selecting files shows chips', async () => { + const wrapper = mount(ChatView); + + // Mock the file picker to return 2 files + _setRawPicker(async () => ['/path/to/file1.txt', '/path/to/file2.md']); + + // Click attach button + await wrapper.find('.attach-btn').trigger('click'); + + // Wait for reactivity and async picker + await new Promise(r => setTimeout(r, 50)); + + const chips = wrapper.findAll('.attachment-chip'); + expect(chips.length).toBe(2); + expect(chips[0].text()).toContain('file1.txt'); + expect(chips[1].text()).toContain('file2.md'); + }); + + it('remove chip updates list', async () => { + const wrapper = mount(ChatView); + + _setRawPicker(async () => ['/path/to/file1.txt', '/path/to/file2.md']); + await wrapper.find('.attach-btn').trigger('click'); + await new Promise(r => setTimeout(r, 50)); + + let chips = wrapper.findAll('.attachment-chip'); + expect(chips.length).toBe(2); + + // Click remove on the first chip + await chips[0].find('.attachment-chip-remove').trigger('click'); + await new Promise(r => setTimeout(r, 10)); // allow Vue to re-render + + chips = wrapper.findAll('.attachment-chip'); + expect(chips.length).toBe(1); + expect(chips[0].text()).toContain('file2.md'); + }); + + it('send clears pending attachments and passes them to store', async () => { + const wrapper = mount(ChatView); + const store = useSessionStore(); + + // Mock the store sendPrompt + store.sendPrompt = vi.fn().mockResolvedValue(undefined); + + // Set text so send button is enabled + await wrapper.find('textarea').setValue('Hello with files'); + + // Add attachments + _setRawPicker(async () => ['/path/to/file1.txt']); + await wrapper.find('.attach-btn').trigger('click'); + await new Promise(r => setTimeout(r, 50)); + + expect(wrapper.findAll('.attachment-chip').length).toBe(1); + + // Send + await wrapper.find('.send-btn').trigger('click'); + await new Promise(r => setTimeout(r, 50)); + + // Chips should be cleared + expect(wrapper.findAll('.attachment-chip').length).toBe(0); + + // Store should have been called with text and attachments + expect(store.sendPrompt).toHaveBeenCalledWith('Hello with files', expect.arrayContaining([ + expect.objectContaining({ name: 'file1.txt' }) + ])); + }); +}); diff --git a/src/components/ChatView.vue b/src/components/ChatView.vue index 9d005fe..88cf782 100644 --- a/src/components/ChatView.vue +++ b/src/components/ChatView.vue @@ -5,13 +5,28 @@ import { useSessionStore } from '../stores/session'; import ModePicker from './ModePicker.vue'; import ModelPicker from './ModelPicker.vue'; import CommandPalette from './CommandPalette.vue'; -import type { SlashCommand } from '../lib/types'; +import type { SlashCommand, AttachmentRef } from '../lib/types'; +import { pickFiles } from '../lib/file-picker'; const sessionStore = useSessionStore(); const inputText = ref(''); const messagesContainer = ref(null); const commandPaletteRef = ref | null>(null); +// Pending attachments state +const pendingAttachments = ref([]); + +async function handleAttach() { + const selected = await pickFiles(); + if (selected.length > 0) { + pendingAttachments.value = [...pendingAttachments.value, ...selected]; + } +} + +function removeAttachment(id: string) { + pendingAttachments.value = pendingAttachments.value.filter(a => a.id !== id); +} + // Track expanded thought sections by message id const expandedThoughts = ref>(new Set()); @@ -52,11 +67,21 @@ async function handleSend() { const text = inputText.value.trim(); if (!text || isLoading.value) return; + const attachments = pendingAttachments.value.length > 0 + ? [...pendingAttachments.value] + : undefined; + inputText.value = ''; + pendingAttachments.value = []; // Clear pending BEFORE async send + try { - await sessionStore.sendPrompt(text); + await sessionStore.sendPrompt(text, attachments); } catch (e) { console.error('Failed to send prompt:', e); + // Restore attachments on failure + if (attachments) { + pendingAttachments.value = attachments; + } } } @@ -236,20 +261,46 @@ function getStatusIcon(status: string): string { @select="handleCommandSelect" @close="handleCommandClose" /> -