From a5951849c864eaad42251e0acd1ec02de94cf5b1 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Sun, 6 Sep 2026 09:57:14 +0800 Subject: [PATCH] =?UTF-8?q?v0.6.0:=20beautifului=20=E6=95=B4=E5=90=88=20+?= =?UTF-8?q?=20=E4=BC=9A=E8=AF=9D/=E5=B7=A5=E4=BD=9C=E5=8C=BA/=E9=98=9F?= =?UTF-8?q?=E5=88=97/=E6=9D=83=E9=99=90/=E5=91=BD=E4=BB=A4/=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E5=85=A8=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search --- .gitignore | 3 + .npmrc | 2 - README.md | 28 - apps/desktop/build/icon.ico | Bin 0 -> 79761 bytes apps/desktop/build/icon.png | Bin 0 -> 43125 bytes apps/desktop/build/icon.svg | 14 + apps/desktop/components.json | 19 + apps/desktop/electron.vite.config.ts | 1 + apps/desktop/package-lock.json | 2365 ++++++++++++ apps/desktop/package.json | 16 +- apps/desktop/preview/index.html | 12 + apps/desktop/preview/main.tsx | 210 ++ apps/desktop/preview/preview.css | 6 + apps/desktop/scripts/capture-app.cjs | 263 ++ apps/desktop/scripts/capture-preview.cjs | 95 + apps/desktop/scripts/debug-collapse.cjs | 21 + apps/desktop/scripts/debug-preview.cjs | 41 + apps/desktop/scripts/inspect-bundle.cjs | 17 + apps/desktop/scripts/make-icons.cjs | 87 + apps/desktop/scripts/read-dts.cjs | 15 + apps/desktop/scripts/read-dts2.cjs | 22 + .../desktop/scripts/repro-empty-workspace.mjs | 21 + apps/desktop/scripts/repro-event-flow.mjs | 103 + apps/desktop/scripts/repro-markdown.mjs | 41 + apps/desktop/scripts/repro-render-logic.mjs | 81 + apps/desktop/src/main/index.ts | 56 +- apps/desktop/src/main/ipc.ts | 82 +- apps/desktop/src/main/pi/agent-service.ts | 327 +- apps/desktop/src/main/pi/approval.ts | 21 +- apps/desktop/src/preload/index.ts | 39 +- apps/desktop/src/renderer/index.html | 6 +- apps/desktop/src/renderer/src/App.tsx | 83 +- .../renderer/src/components/ApprovalCard.tsx | 341 +- .../renderer/src/components/BottomSlot.tsx | 93 + .../src/renderer/src/components/ChatView.tsx | 101 +- .../src/renderer/src/components/CodeBlock.tsx | 196 + .../src/renderer/src/components/Composer.tsx | 889 ++++- .../src/renderer/src/components/GlideMenu.tsx | 56 + .../src/renderer/src/components/Loading.tsx | 75 + .../src/renderer/src/components/Markdown.tsx | 469 +-- .../renderer/src/components/MessageItem.tsx | 247 +- .../src/renderer/src/components/Search.tsx | 193 + .../src/components/SelectionActions.tsx | 203 + .../renderer/src/components/SettingsModal.tsx | 531 +-- .../src/renderer/src/components/Sidebar.tsx | 336 -- .../renderer/src/components/SidebarNav.tsx | 528 +++ .../renderer/src/components/StreamingText.tsx | 99 + .../src/renderer/src/components/Thinking.tsx | 123 + .../src/renderer/src/components/TitleBar.tsx | 58 + .../src/renderer/src/components/ToolCard.tsx | 160 - .../src/renderer/src/components/ToolChips.tsx | 376 ++ .../renderer/src/components/atoms/Button.tsx | 45 + .../src/renderer/src/hooks/useAgent.ts | 88 +- .../src/renderer/src/hooks/useEntrance.ts | 26 + apps/desktop/src/renderer/src/lib/api.ts | 25 +- apps/desktop/src/renderer/src/lib/utils.ts | 6 + .../src/renderer/src/styles/beautifului.css | 384 ++ .../src/renderer/src/styles/globals.css | 37 +- apps/desktop/src/shared/types.ts | 58 + apps/desktop/tsconfig.web.json | 1 + apps/desktop/vite.preview.config.ts | 21 + docs/references.md | 9 +- docs/tech-stack.md | 2 +- package.json | 22 - pnpm-lock.yaml | 3284 ----------------- pnpm-workspace.yaml | 3 - scripts/beautifului/approval-card.json | 20 + scripts/beautifului/button.json | 21 + scripts/beautifului/foundation.json | 15 + scripts/beautifului/glide-menu.json | 18 + scripts/beautifului/loading-state.json | 18 + scripts/beautifului/prompt-bar.json | 21 + scripts/beautifului/registry.json | 142 + scripts/beautifului/selection-actions.json | 24 + scripts/beautifului/shimmer.json | 18 + scripts/beautifului/sidebar-nav.json | 28 + scripts/beautifului/stage/ApprovalCard.tsx | 419 +++ scripts/beautifului/stage/Button.tsx | 47 + scripts/beautifului/stage/GlideMenu.tsx | 59 + scripts/beautifului/stage/LoadingState.tsx | 152 + scripts/beautifului/stage/SidebarNav.tsx | 449 +++ scripts/beautifului/stage/StreamingText.tsx | 252 ++ scripts/beautifului/stage/ThinkingState.tsx | 273 ++ scripts/beautifului/stage/ToolChips.tsx | 336 ++ scripts/beautifului/stage/foundation.css | 643 ++++ scripts/beautifului/stage/sidebar-nav.css | 81 + scripts/beautifului/stream-text.json | 18 + scripts/beautifului/streaming-text.json | 18 + scripts/beautifului/task-rows.json | 18 + scripts/beautifului/thinking-state.json | 18 + scripts/beautifului/tool-chips.json | 18 + scripts/check-lu-icons.mjs | 18 + scripts/extract-beautifului.mjs | 52 + scripts/fetch-beautifului.mjs | 47 + scripts/install-beautifului.mjs | 20 + scripts/probe-pi-sdk.mjs | 32 + 96 files changed, 11828 insertions(+), 4649 deletions(-) delete mode 100644 .npmrc delete mode 100644 README.md create mode 100644 apps/desktop/build/icon.ico create mode 100644 apps/desktop/build/icon.png create mode 100644 apps/desktop/build/icon.svg create mode 100644 apps/desktop/components.json create mode 100644 apps/desktop/package-lock.json create mode 100644 apps/desktop/preview/index.html create mode 100644 apps/desktop/preview/main.tsx create mode 100644 apps/desktop/preview/preview.css create mode 100644 apps/desktop/scripts/capture-app.cjs create mode 100644 apps/desktop/scripts/capture-preview.cjs create mode 100644 apps/desktop/scripts/debug-collapse.cjs create mode 100644 apps/desktop/scripts/debug-preview.cjs create mode 100644 apps/desktop/scripts/inspect-bundle.cjs create mode 100644 apps/desktop/scripts/make-icons.cjs create mode 100644 apps/desktop/scripts/read-dts.cjs create mode 100644 apps/desktop/scripts/read-dts2.cjs create mode 100644 apps/desktop/scripts/repro-empty-workspace.mjs create mode 100644 apps/desktop/scripts/repro-event-flow.mjs create mode 100644 apps/desktop/scripts/repro-markdown.mjs create mode 100644 apps/desktop/scripts/repro-render-logic.mjs create mode 100644 apps/desktop/src/renderer/src/components/BottomSlot.tsx create mode 100644 apps/desktop/src/renderer/src/components/CodeBlock.tsx create mode 100644 apps/desktop/src/renderer/src/components/GlideMenu.tsx create mode 100644 apps/desktop/src/renderer/src/components/Loading.tsx create mode 100644 apps/desktop/src/renderer/src/components/Search.tsx create mode 100644 apps/desktop/src/renderer/src/components/SelectionActions.tsx delete mode 100644 apps/desktop/src/renderer/src/components/Sidebar.tsx create mode 100644 apps/desktop/src/renderer/src/components/SidebarNav.tsx create mode 100644 apps/desktop/src/renderer/src/components/StreamingText.tsx create mode 100644 apps/desktop/src/renderer/src/components/Thinking.tsx create mode 100644 apps/desktop/src/renderer/src/components/TitleBar.tsx delete mode 100644 apps/desktop/src/renderer/src/components/ToolCard.tsx create mode 100644 apps/desktop/src/renderer/src/components/ToolChips.tsx create mode 100644 apps/desktop/src/renderer/src/components/atoms/Button.tsx create mode 100644 apps/desktop/src/renderer/src/hooks/useEntrance.ts create mode 100644 apps/desktop/src/renderer/src/lib/utils.ts create mode 100644 apps/desktop/src/renderer/src/styles/beautifului.css create mode 100644 apps/desktop/vite.preview.config.ts delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml delete mode 100644 pnpm-workspace.yaml create mode 100644 scripts/beautifului/approval-card.json create mode 100644 scripts/beautifului/button.json create mode 100644 scripts/beautifului/foundation.json create mode 100644 scripts/beautifului/glide-menu.json create mode 100644 scripts/beautifului/loading-state.json create mode 100644 scripts/beautifului/prompt-bar.json create mode 100644 scripts/beautifului/registry.json create mode 100644 scripts/beautifului/selection-actions.json create mode 100644 scripts/beautifului/shimmer.json create mode 100644 scripts/beautifului/sidebar-nav.json create mode 100644 scripts/beautifului/stage/ApprovalCard.tsx create mode 100644 scripts/beautifului/stage/Button.tsx create mode 100644 scripts/beautifului/stage/GlideMenu.tsx create mode 100644 scripts/beautifului/stage/LoadingState.tsx create mode 100644 scripts/beautifului/stage/SidebarNav.tsx create mode 100644 scripts/beautifului/stage/StreamingText.tsx create mode 100644 scripts/beautifului/stage/ThinkingState.tsx create mode 100644 scripts/beautifului/stage/ToolChips.tsx create mode 100644 scripts/beautifului/stage/foundation.css create mode 100644 scripts/beautifului/stage/sidebar-nav.css create mode 100644 scripts/beautifului/stream-text.json create mode 100644 scripts/beautifului/streaming-text.json create mode 100644 scripts/beautifului/task-rows.json create mode 100644 scripts/beautifului/thinking-state.json create mode 100644 scripts/beautifului/tool-chips.json create mode 100644 scripts/check-lu-icons.mjs create mode 100644 scripts/extract-beautifului.mjs create mode 100644 scripts/fetch-beautifului.mjs create mode 100644 scripts/install-beautifului.mjs create mode 100644 scripts/probe-pi-sdk.mjs diff --git a/.gitignore b/.gitignore index bdb2433..f499a46 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ out/ .env .env.local release/ +preview-shots/ +*.tsbuildinfo +capture-log.txt diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 8084498..0000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -save-exact=true -engine-strict=false diff --git a/README.md b/README.md deleted file mode 100644 index f8a06b8..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# agent-store-operations - -店铺运营 Agent 桌面工具 —— Electron for [pi agent](https://pi.dev/)。 - -## 仓库结构 - -``` -apps/desktop/ Electron 客户端(React + Tailwind + GSAP,主进程内嵌 pi SDK) -docs/ 架构决策、pi 集成笔记、参考项目取舍、路线图与版本自省 -``` - -> 后端(NestJS)、后台(Ant Design Pro)、数据库尚未设计,按约定不创建。见 `docs/architecture.md`。 - -## 开发 - -```bash -pnpm install -pnpm dev # 启动 desktop(electron-vite HMR) -pnpm typecheck -``` - -## 文档 - -- [architecture.md](docs/architecture.md) — 架构决策记录(ADR) -- [pi-sdk-notes.md](docs/pi-sdk-notes.md) — pi SDK 集成笔记 -- [references.md](docs/references.md) — 参考项目取舍(pi-web / Proma / beautifului) -- [tech-stack.md](docs/tech-stack.md) — 技术栈 -- [roadmap.md](docs/roadmap.md) — 版本路线图与自省日志 diff --git a/apps/desktop/build/icon.ico b/apps/desktop/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..64c6c40651fb2d12f8273c75fa6e2579b8f7dd9b GIT binary patch literal 79761 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 q}@B>&)QO+&y>8J8hPJnpcr z5m9*%j`5kT04qLdLDP1}D;1%gUL=xgCQJyD z(^d2{I#j))&u2<&yn&xbkM3}I28HBOOX89d%e5zv=dm@Ru2Ko7>W8i4GbWZbSfRNc zP`R0zi{*U2B^4I}=O63^F<_sa$b3%G1;zMgJ>sc2m=*$>Ax|BV<~oi8fjiEfYqH1M~x`w(x;zm2BKf}P)6jUb_2)DXkP+X>u}SzzVqUa z!v-D8C4Ycz#i5N+Q^0yrb5&kDaeBF@@}hM+XjFQA_XeaAa|c^@^H5p`Wty_ZVB+3*0}q=Pnh$& zK!+yQ*7BO-!CmuxU7C+#AJ_3v%FzKFXhTfks=hfyu9brHZBXtGrqqaS=*NY1+AF}y zBi#%re$gqK1*M{oY-(%^wm*J;U{1ZcK1r1mtn|fg2RzpMxKJg5`T8<_Ig@z2z|-R~ z?OSY{`K1I=)Nn>O+EN;N$#du!0e1BzRimEGte7(Z8JV5ixQ zB+C}cZUO955m?>H0)oZWw~YW<_&IoRR7NKvuNd7P5~c)*r41$VdKinm6Tg(cBsw5o z>OR95c>&m6r@q+$o9!G^pH`<_v+ZR(4mN6CX&+l6N)i~=VU-aVY6QTv87GsE>aXJ# zf*)gh>uER46UK=OL<$ArWgvKP0vI@OgzCrY5l6iCmWs}GN027O8lof&b z=kvbv<=2S)A3XiK7)C|f(V?6kDkqHqDjW1s=EofJL^)Lt2<78MzHgnBL|a-n#YX-8 z!ApvwoE~<65z~Lg$gxm4M0QdF5*N|;8almuRsHJLSwhV%r--ScM3LGZ)R<~>f8s?C zQV5(Y?$yMPA)RG-3R}nHL}C|S(BMcGnTd-EkhMcggnkmSXnXuR%34!Z3Ra%Y#h?Gx zRkp@%SqrxR{eqay4F%K$8Qd40Pr}hDu>xw;h6-vXpp*YnnRE*{r8B?hqd41&PH;Ce z{w>20s{dtM^I~JA0rBLg9ub6;=q6K(j4MBHY#LyRQZJC{1H1U-N7Sl&b)6Rx&MBp` z0WGffjc0AtP=>BnbB>jS+wDK?BqkzaIW$WQXuVMHNdVb4#+O6LDH>n)VR|x(S@*NE z=Vi-BW5ahiVg}$1B&rl&4AhNk-9v{7yg;KG!P)gl{*%(EdW`wBiLzx7A0JVP%> z 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/build/icon.svg b/apps/desktop/build/icon.svg new file mode 100644 index 0000000..3564271 --- /dev/null +++ b/apps/desktop/build/icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/desktop/components.json b/apps/desktop/components.json new file mode 100644 index 0000000..25ea240 --- /dev/null +++ b/apps/desktop/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/renderer/src/styles/globals.css", + "baseColor": "zinc", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 3bbdeca..4e8b8e6 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -48,6 +48,7 @@ export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { + '@': resolve(__dirname, 'src/renderer/src'), '@renderer': resolve(__dirname, 'src/renderer/src'), '@shared': resolve(__dirname, 'src/shared') } diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json new file mode 100644 index 0000000..9801e9e --- /dev/null +++ b/apps/desktop/package-lock.json @@ -0,0 +1,2365 @@ +{ + "name": "@aso/desktop", + "version": "0.5.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@aso/desktop", + "version": "0.5.0", + "dependencies": { + "@earendil-works/pi-ai": "0.85.0", + "@earendil-works/pi-coding-agent": "0.85.0", + "@earendil-works/pi-server": "0.85.0", + "@llm-ui/code": "^0.13.3", + "@llm-ui/markdown": "^0.13.3", + "@llm-ui/react": "^0.13.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "gsap": "3.15.0", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-icons": "5.7.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "shadow-plugin": "2.1.0", + "shiki": "^1.29.2", + "tailwind-merge": "3.6.0" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.3", + "@types/node": "26.4.1", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.7", + "@vitejs/plugin-react": "6.1.1", + "electron": "44.2.0", + "electron-vite": "5.0.0", + "tailwindcss": "4.3.3", + "typescript": "7.0.2", + "vite": "8.2.2" + } + }, + "../../node_modules/.pnpm/@earendil-works+pi-ai@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-ai": { + "version": "0.85.0", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.123.0", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.85.0", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "22.19.19", + "canvas": "3.2.3", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "../../node_modules/.pnpm/@earendil-works+pi-coding-agent@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-coding-agent": { + "version": "0.85.0", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.0", + "@earendil-works/pi-agent-core": "^0.85.0", + "@earendil-works/pi-ai": "^0.85.0", + "@earendil-works/pi-client": "^0.85.0", + "@earendil-works/pi-protocol": "^0.85.0", + "@earendil-works/pi-tui": "^0.85.0", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "grok-mermaid": "0.2.2", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.3.7", + "undici": "8.9.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/bundle/cli.js" + }, + "devDependencies": { + "@types/cross-spawn": "6.0.6", + "@types/hosted-git-info": "3.0.5", + "@types/node": "22.19.19", + "@types/proper-lockfile": "4.1.4", + "@types/semver": "7.7.1", + "shx": "0.4.0", + "typescript": "5.9.3", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "../../node_modules/.pnpm/@earendil-works+pi-server@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-server": { + "version": "0.85.0", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.0", + "@earendil-works/pi-agent-core": "^0.85.0", + "@earendil-works/pi-protocol": "^0.85.0" + }, + "devDependencies": { + "shx": "0.4.0", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "../../node_modules/.pnpm/@tailwindcss+vite@4.3.3_vit_a9eca87458f3ee6e74129ebf668d736e/node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "devDependencies": { + "@types/node": "22.19.21", + "vite": "^8.1.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "../../node_modules/.pnpm/@types+node@26.4.1/node_modules/@types/node": { + "version": "26.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "../../node_modules/.pnpm/@types+react-dom@19.2.7_@types+react@19.2.18/node_modules/@types/react-dom": { + "version": "19.2.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "../../node_modules/.pnpm/@types+react@19.2.18/node_modules/@types/react": { + "version": "19.2.18", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "../../node_modules/.pnpm/@vitejs+plugin-react@6.1.1__53224dc688df1e7fe4782d8236195344/node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "devDependencies": { + "@babel/core": "8.0.1", + "@rolldown/plugin-babel": "^0.2.3", + "@vitejs/react-common": "workspace:*", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.147.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "rolldown": "^1.2.6", + "tsdown": "^0.22.14", + "vite": "^8.2.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "devDependencies": { + "@swc/cli": "0.3.12", + "@swc/core": "1.4.16", + "@types/node": "20.12.7", + "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", + "bundlesize": "0.18.2", + "npm-run-all": "4.1.5", + "react": "18.2.0", + "react-dom": "18.2.0", + "ts-node": "10.9.2", + "typescript": "5.4.5" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "devDependencies": { + "esm": "3.2.25", + "terser": "4.8.0", + "uvu": "0.5.4" + }, + "engines": { + "node": ">=6" + } + }, + "../../node_modules/.pnpm/electron-vite@5.0.0_vite@8._18387c8b1a9e0f53f139a78b83265a64/node_modules/electron-vite": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.1.4", + "@swc/core": "^1.13.5", + "@types/babel__core": "^7.20.5", + "@types/node": "^22.18.11", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.4.0", + "lint-staged": "^16.2.4", + "prettier": "^3.6.2", + "rollup": "^4.52.4", + "rollup-plugin-dts": "^6.2.3", + "rollup-plugin-rm": "^1.0.2", + "simple-git-hooks": "^2.13.1", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1", + "vite": "^7.1.10" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/electron@44.2.0/node_modules/electron": { + "version": "44.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "../../node_modules/.pnpm/gsap@3.15.0/node_modules/gsap": { + "version": "3.15.0", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, + "../../node_modules/.pnpm/react-dom@19.2.8_react@19.2.8/node_modules/react-dom": { + "version": "19.2.8", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "../../node_modules/.pnpm/react-icons@5.7.0_react@19.2.8/node_modules/react-icons": { + "version": "5.7.0", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "../../node_modules/.pnpm/react@19.2.8/node_modules/react": { + "version": "19.2.8", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../node_modules/.pnpm/shadow-plugin@2.1.0/node_modules/shadow-plugin": { + "version": "2.1.0", + "license": "MIT", + "devDependencies": { + "@tailwindcss/cli": "^4.3.3", + "tailwindcss": "^4.3.3" + } + }, + "../../node_modules/.pnpm/tailwind-merge@3.6.0/node_modules/tailwind-merge": { + "version": "3.6.0", + "license": "MIT", + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.3", + "@codspeed/vitest-plugin": "^5.4.0", + "@rollup/plugin-babel": "^7.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.3.0", + "@types/node": "^24.12.2", + "@vitest/coverage-v8": "^4.1.5", + "@vitest/eslint-plugin": "^1.6.16", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.6.8", + "eslint": "^9.39.4", + "eslint-plugin-import": "^2.32.0", + "globby": "^16.2.0", + "prettier": "^3.8.3", + "rollup": "^4.60.2", + "rollup-plugin-delete": "^3.0.2", + "rollup-plugin-dts": "^6.4.1", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.1", + "vitest": "^4.1.5", + "zx": "^8.8.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "../../node_modules/.pnpm/tailwindcss@4.3.3/node_modules/tailwindcss": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "devDependencies": { + "@jridgewell/remapping": "^2.3.5", + "@tailwindcss/oxide": "^4.3.3", + "@types/node": "22.19.21", + "dedent": "1.7.2", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1" + } + }, + "../../node_modules/.pnpm/typescript@7.0.2/node_modules/typescript": { + "version": "7.0.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "../../node_modules/.pnpm/vite@8.2.2_@types+node@26.4_9aec1c9b231ec5c84774cec00150d533/node_modules/vite": { + "version": "8.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "devDependencies": { + "@babel/parser": "^7.29.8", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/trace-mapping": "^0.3.31", + "@polka/compression": "^1.0.0-next.25", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-dynamic-import-vars": "2.1.4", + "@rollup/pluginutils": "^5.4.0", + "@types/escape-html": "^1.0.4", + "@types/pnpapi": "^0.0.5", + "@vercel/detect-agent": "^1.2.5", + "@vitejs/devtools": "^0.4.12", + "@vitest/utils": "4.1.10", + "@voidzero-dev/vite-task-client": "^0.2.0", + "artichokie": "^0.4.4", + "baseline-browser-mapping": "^2.11.14", + "cac": "^7.0.0", + "chokidar": "^3.6.0", + "connect": "^3.7.0", + "convert-source-map": "^2.0.0", + "cors": "^2.8.6", + "cross-spawn": "^7.0.6", + "dotenv-expand": "^13.0.0", + "es-module-lexer": "^2.3.1", + "esbuild": "^0.28.2", + "escape-html": "^1.0.3", + "estree-walker": "^3.0.3", + "etag": "^1.8.1", + "fresh-import": "^0.2.1", + "host-validation-middleware": "^0.1.4", + "http-proxy-3": "^1.23.3", + "launch-editor-middleware": "^2.14.1", + "magic-string": "^1.2.0", + "mlly": "^1.8.2", + "mrmime": "^2.0.1", + "nanoid": "^5.1.16", + "obug": "^1.0.2", + "open": "^10.2.0", + "parse5": "^8.0.1", + "pathe": "^2.0.3", + "periscopic": "^4.0.3", + "picocolors": "^1.1.1", + "postcss-import": "^16.2.0", + "postcss-load-config": "^6.0.1", + "postcss-modules": "^9.0.1", + "premove": "^4.0.0", + "resolve.exports": "^2.0.3", + "rolldown-plugin-dts": "^0.28.2", + "rollup": "^4.59.0", + "rollup-plugin-license": "^3.7.1", + "sass": "^1.102.0", + "sass-embedded": "^1.102.0", + "sirv": "^3.0.2", + "strip-literal": "^4.0.0", + "terser": "^5.50.0", + "ufo": "^1.6.4", + "ws": "^8.21.3" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-ai": { + "resolved": "../../node_modules/.pnpm/@earendil-works+pi-ai@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-ai", + "link": true + }, + "node_modules/@earendil-works/pi-coding-agent": { + "resolved": "../../node_modules/.pnpm/@earendil-works+pi-coding-agent@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-coding-agent", + "link": true + }, + "node_modules/@earendil-works/pi-server": { + "resolved": "../../node_modules/.pnpm/@earendil-works+pi-server@0.85.0_ws@8.21.3/node_modules/@earendil-works/pi-server", + "link": true + }, + "node_modules/@llm-ui/code": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@llm-ui/code/-/code-0.13.3.tgz", + "integrity": "sha512-bqHjHanYbNetuDJaAmVwIg4vRhH6Cth4Ljo2YxHZNEpDtEqLHonDaAcAq8//mibFGVwhspxIQYClnT8K4Zxlqg==", + "license": "MIT", + "dependencies": { + "@llm-ui/react": "0.13.3", + "@llm-ui/shared": "0.13.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "shiki": "^1.6.0" + } + }, + "node_modules/@llm-ui/markdown": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@llm-ui/markdown/-/markdown-0.13.3.tgz", + "integrity": "sha512-7wXte9RXQosSFKneYnVwnxUvwIgov7bE04nqA6PjMBDqxxeSI3FD5TFzrpBMj6+TN2Urraj5iJWaL4IdCQNtsA==", + "license": "MIT", + "dependencies": { + "@llm-ui/react": "0.13.3", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm": "^3.0.0", + "mdast-util-to-markdown": "^2.1.0", + "micromark-extension-gfm": "^3.0.0", + "remeda": "^1.57.0", + "unist-util-visit": "^5.0.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@llm-ui/react": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@llm-ui/react/-/react-0.13.3.tgz", + "integrity": "sha512-b5UtUl8KZp+P4VyAypNqGnSv8jZVJlyYrH8wvOQNIa9x/Y3H4zWiSNTV5BYk5rOJ7WPDCruvLJX/G87+HR1gog==", + "license": "MIT", + "dependencies": { + "remeda": "^1.57.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@llm-ui/shared": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@llm-ui/shared/-/shared-0.13.3.tgz", + "integrity": "sha512-+vt6uHnYXKqCtA+rNxCRwxdLFft3muMFA0JYMxf1b11gSMriO0Jgvwwx72dn1YWmX4gF1YHGRbMPWBe7V6CQCA==", + "license": "MIT", + "dependencies": { + "@llm-ui/react": "0.13.3" + } + }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tailwindcss/vite": { + "resolved": "../../node_modules/.pnpm/@tailwindcss+vite@4.3.3_vit_a9eca87458f3ee6e74129ebf668d736e/node_modules/@tailwindcss/vite", + "link": true + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "resolved": "../../node_modules/.pnpm/@types+node@26.4.1/node_modules/@types/node", + "link": true + }, + "node_modules/@types/react": { + "resolved": "../../node_modules/.pnpm/@types+react@19.2.18/node_modules/@types/react", + "link": true + }, + "node_modules/@types/react-dom": { + "resolved": "../../node_modules/.pnpm/@types+react-dom@19.2.7_@types+react@19.2.18/node_modules/@types/react-dom", + "link": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "resolved": "../../node_modules/.pnpm/@vitejs+plugin-react@6.1.1__53224dc688df1e7fe4782d8236195344/node_modules/@vitejs/plugin-react", + "link": true + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/class-variance-authority": { + "resolved": "../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority", + "link": true + }, + "node_modules/clsx": { + "resolved": "../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx", + "link": true + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron": { + "resolved": "../../node_modules/.pnpm/electron@44.2.0/node_modules/electron", + "link": true + }, + "node_modules/electron-vite": { + "resolved": "../../node_modules/.pnpm/electron-vite@5.0.0_vite@8._18387c8b1a9e0f53f139a78b83265a64/node_modules/electron-vite", + "link": true + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/gsap": { + "resolved": "../../node_modules/.pnpm/gsap@3.15.0/node_modules/gsap", + "link": true + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "resolved": "../../node_modules/.pnpm/react@19.2.8/node_modules/react", + "link": true + }, + "node_modules/react-dom": { + "resolved": "../../node_modules/.pnpm/react-dom@19.2.8_react@19.2.8/node_modules/react-dom", + "link": true + }, + "node_modules/react-icons": { + "resolved": "../../node_modules/.pnpm/react-icons@5.7.0_react@19.2.8/node_modules/react-icons", + "link": true + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remeda": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-1.61.0.tgz", + "integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==", + "license": "MIT" + }, + "node_modules/shadow-plugin": { + "resolved": "../../node_modules/.pnpm/shadow-plugin@2.1.0/node_modules/shadow-plugin", + "link": true + }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tailwind-merge": { + "resolved": "../../node_modules/.pnpm/tailwind-merge@3.6.0/node_modules/tailwind-merge", + "link": true + }, + "node_modules/tailwindcss": { + "resolved": "../../node_modules/.pnpm/tailwindcss@4.3.3/node_modules/tailwindcss", + "link": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/typescript": { + "resolved": "../../node_modules/.pnpm/typescript@7.0.2/node_modules/typescript", + "link": true + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "resolved": "../../node_modules/.pnpm/vite@8.2.2_@types+node@26.4_9aec1c9b231ec5c84774cec00150d533/node_modules/vite", + "link": true + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e2a95f2..0aceca9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "@aso/desktop", "private": true, "version": "0.5.0", - "description": "店铺运营 Agent 桌面端(Electron for pi agent)", + "description": "Inkreach Agent 桌面端(Electron for pi agent)", "type": "module", "main": "out/main/index.mjs", "scripts": { @@ -16,10 +16,20 @@ "@earendil-works/pi-ai": "0.85.0", "@earendil-works/pi-coding-agent": "0.85.0", "@earendil-works/pi-server": "0.85.0", + "@llm-ui/code": "^0.13.3", + "@llm-ui/markdown": "^0.13.3", + "@llm-ui/react": "^0.13.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "gsap": "3.15.0", - "lucide-react": "1.41.0", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "react-icons": "5.7.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "shadow-plugin": "2.1.0", + "shiki": "^1.29.2", + "tailwind-merge": "3.6.0" }, "devDependencies": { "@tailwindcss/vite": "4.3.3", diff --git a/apps/desktop/preview/index.html b/apps/desktop/preview/index.html new file mode 100644 index 0000000..38f5a77 --- /dev/null +++ b/apps/desktop/preview/index.html @@ -0,0 +1,12 @@ + + + + + + beautifului primitives preview + + +
+ + + diff --git a/apps/desktop/preview/main.tsx b/apps/desktop/preview/main.tsx new file mode 100644 index 0000000..e567358 --- /dev/null +++ b/apps/desktop/preview/main.tsx @@ -0,0 +1,210 @@ +// beautifului.dev 组件视觉验收页(静态构建,模拟聊天流内的真实用法) +import { useRef, useState } from 'react' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './preview.css' +import { Thinking } from '../src/renderer/src/components/Thinking' +import { StreamingText } from '../src/renderer/src/components/StreamingText' +import { ApprovalStack } from '../src/renderer/src/components/ApprovalCard' +import { ToolChips } from '../src/renderer/src/components/ToolChips' +import { SidebarNav } from '../src/renderer/src/components/SidebarNav' +import { Search } from '../src/renderer/src/components/Search' +import { CodeBlock } from '../src/renderer/src/components/CodeBlock' +import { SelectionActions } from '../src/renderer/src/components/SelectionActions' +import { Loading } from '../src/renderer/src/components/Loading' +import { Markdown } from '../src/renderer/src/components/Markdown' +import type { ApprovalRequest, SessionMeta } from '../src/shared/types' + +const thinkingRows = [ + { primary: '用户想了解今天的品类热点,我先查一下最近的趋势数据。' }, + { primary: '需要调用 query_trend 工具获取近 7 天的热点词,再按类目分组。' }, + { primary: '结果里先给 TOP3 品类,附上建议的标题关键词。' } +] + +const toolCalls = [ + { + id: 'call-1', + name: 'query_trend', + tool: { + toolName: 'query_trend', + args: { category: 'home', days: 7 }, + status: 'done' as const, + result: { content: [{ type: 'text', text: '{ "top": ["收纳", "香薰", "桌布"], "lift": [0.32, 0.21, 0.18] }' }] } + } + }, + { + id: 'call-2', + name: 'draft_title', + tool: { + toolName: 'draft_title', + args: { keywords: ['收纳', '灵感'] }, + status: 'error' as const, + result: { content: [{ type: 'text', text: 'Error: rate limited, retry after 30s' }] } + } + }, + { + id: 'call-3', + name: 'update_price', + pendingApproval: { args: { sku: 'SKU-1024', from: 39.9, to: 35.9 } } + } +] + +const approvalRequest: ApprovalRequest = { + id: 'approval-1', + toolName: 'update_price', + args: { sku: 'SKU-1024', from: 39.9, to: 35.9 } +} + +const sessions: SessionMeta[] = [ + { id: 's1', path: 'a.json', name: '供应商记录整理', firstMessage: '帮我整理供应商', created: '', modified: new Date().toISOString(), messageCount: 12 }, + { id: 's2', path: 'b.json', name: '', firstMessage: '根据热点词起草一个商品标题方案', created: '', modified: new Date(Date.now() - 86400000).toISOString(), messageCount: 5 }, + { id: 's3', path: 'c.json', name: '', firstMessage: '查询最近一次上架任务的执行结果', created: '', modified: new Date(Date.now() - 3 * 86400000).toISOString(), messageCount: 8 } +] + +const CODE_SNIPPET = `export async function churnBatch() { + const flavor = await getFlavor("pistachio"); + const base = await dairy.fetch({ flavor }); + await freezer.store(base, { temp: "-16C" }); + if (!base.approved) return null; + return base.gallons; +}` + +const DIFF_SNIPPET = `@@ -1,4 +1,5 @@ + export const hero = "mint-chip"; +-const windows = slots; ++const windows = slots.filter( ++ (s) => s.temp <= -12, ++); + schedule(windows);` + +const PARA = + '百香果风味在整个周末保持热销榜首。周六一早就要开始搅拌,让批次在下午高峰前有足够时间固化。同时需要预留两个晚间冷冻窗口,避免与常规生产冲突。' + +function PreviewCard({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +function App() { + const [searchOpen, setSearchOpen] = useState(false) + const [applied, setApplied] = useState('') + const selectionBoxRef = useRef(null) + + // 调试分区:URL hash 指定只渲染某一节(#thinking / #streaming / #tools / + // #approval / #code / #selection / #loading / #sidebar / #none),空 hash 全渲染 + const only = window.location.hash.slice(1) + const show = (id: string): boolean => only === '' || only === id + + return ( +
+

beautifului.dev · 8 组件直连验收(light)

+ +
+
+ {show('thinking') && ( + + +
+ +
+
+ )} + + {show('streaming') && ( + + setApplied(q)}> + + + + )} + + {show('tools') && ( + +
+ +
+
+ )} + + {show('approval') && ( + + setApplied(approved ? '已允许' : '已拒绝')} /> +
+ + )} + + {show('code') && ( + + +
+ +
+
+ )} + + {show('selection') && ( + +
+

{PARA}

+ setApplied(`${instruction}:${selection.slice(0, 18)}…`)} /> +
+ {applied &&

onApply → {applied}

} +
+ )} + + {show('loading') && ( + + + + )} +
+ + {show('sidebar') && ( +
+ +
+ undefined} + onOpenWorkspace={() => undefined} + onNewSession={() => setApplied('新会话')} + onOpenSession={() => setApplied('打开会话')} + onRenameSession={() => setApplied('重命名')} + onDeleteSession={() => setApplied('删除')} + onRefreshSessions={() => setApplied('刷新')} + onOpenSearch={() => setSearchOpen(true)} + onOpenSettings={() => setApplied('设置')} + /> +
+
+
+ )} +
+ + {show('sidebar') && ( + setSearchOpen(false)} + sessions={sessions} + onNewSession={() => setApplied('新会话')} + onRefreshSessions={() => setApplied('刷新')} + onOpenSettings={() => setApplied('设置')} + onOpenSession={() => setApplied('打开会话')} + /> + )} +
+ ) +} + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/apps/desktop/preview/preview.css b/apps/desktop/preview/preview.css new file mode 100644 index 0000000..815d52b --- /dev/null +++ b/apps/desktop/preview/preview.css @@ -0,0 +1,6 @@ +/* 预览页专用入口:复用应用全套样式(tailwind + beautifului foundation + 字体), + * 并显式声明组件源码目录参与工具类扫描(vite root 在 preview/ 下, + * 自动检测覆盖不到 ../src)。 */ +@import "../src/renderer/src/styles/globals.css"; + +@source "../src/renderer/src"; diff --git a/apps/desktop/scripts/capture-app.cjs b/apps/desktop/scripts/capture-app.cjs new file mode 100644 index 0000000..9c345ed --- /dev/null +++ b/apps/desktop/scripts/capture-app.cjs @@ -0,0 +1,263 @@ +// 真实 App 渲染层截图(离屏,注入假 IPC 数据): +// npx electron scripts/capture-app.cjs +// 产出 preview-shots/app-*.png:展开态 / 收起态 / markdown 渲染 +const { app, BrowserWindow, ipcMain } = require('electron') +const { mkdirSync, writeFileSync, existsSync } = require('node:fs') +const { join } = require('node:path') + +app.commandLine.appendSwitch('disable-gpu') + +const ROOT = join(__dirname, '..') +const OUT = join(ROOT, 'preview-shots') + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +const MARKDOWN_SAMPLE = [ + '## 1. "这是演示数据,仅用于验证链路"', + '', + '我配置的 `hotword_lookup` 工具目前连的是**演示实现(demo)**,[项目主页](https://example.com)。', + '', + '| 说明 | 结论 |', + '| :--- | ---: |', + '| **工具可用** | 工具这个"壳"是好的 —— 链路没有问题 |', + '| **数据源是空的** | 它背后的"仓库"是空的,查询返回"无匹配项" |', + '', + '- 列表项一,包含 `inline code`', + '- 列表项二,**加粗** 与 ~~删除线~~', + '', + '```python', + "print('Hello llm-ui!')", + 'for i in range(3):', + ' print(i)', + '```', + '', + '```rust', + 'fn main() {', + ' println!("Hello, World!");', + '}', + '```' +].join('\n') + +const FAKE_STATE = { + sessionId: 's1', + sessionName: 'hi', + streaming: false, + activeTools: [], + messageCount: 2, + workspace: 'C:\\fake\\workspace' +} + +const FAKE_SESSIONS = [ + { id: 's1', path: 'C:\\fake\\s1.jsonl', name: 'hi', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 2 }, + { id: 's2', path: 'C:\\fake\\s2.jsonl', name: 'hello world', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 1 } +] + +function registerFakeIpc() { + ipcMain.handle('agent:state', () => FAKE_STATE) + ipcMain.handle('sessions:list', () => FAKE_SESSIONS) + ipcMain.handle('workspaces:list', () => [{ name: 'Default', path: 'C:\\fake\\workspace' }]) + ipcMain.handle('models:catalog', () => ({ providers: [] })) + ipcMain.handle('settings:get', () => ({ systemPrompt: '' })) + ipcMain.handle('app:versions', () => ({ app: 'dev', electron: 'x', node: 'x', chrome: 'x', user: 'x' })) + ipcMain.handle('agent:send', () => undefined) + ipcMain.handle('chat:resources', () => ({ + commands: [{ name: 'daily-report', description: '生成每日经营日报', kind: 'prompt' }], + skills: [{ name: 'hotword-analysis', description: '分析近 7 天类目热点词' }] + })) + ipcMain.handle('files:list', () => [ + { path: 'data/products.csv', name: 'products.csv', isDir: false }, + { path: 'docs/sop.md', name: 'sop.md', isDir: false }, + { path: 'hello_world.py', name: 'hello_world.py', isDir: false }, + { path: 'assets', name: 'assets', isDir: true } + ]) +} + +async function capture(win, name) { + // 离屏窗口 capturePage 常给旧帧:连拍多张,取最后一张(大概率已提交新帧) + let image = null + for (let k = 0; k < 4; k += 1) { + win.webContents.invalidate() + await sleep(320) + image = await win.webContents.capturePage() + } + writeFileSync(join(OUT, name), image.toPNG()) + console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`) +} + +async function run() { + mkdirSync(OUT, { recursive: true }) + registerFakeIpc() + + const preload = join(ROOT, 'out', 'preload', 'index.cjs') + const win = new BrowserWindow({ + width: 1280, + height: 900, + show: false, + webPreferences: { + paintWhenInitiallyHidden: true, + backgroundThrottling: false, + preload: existsSync(preload) ? preload : undefined, + contextIsolation: true + } + }) + await win.loadFile(join(ROOT, 'out', 'renderer', 'index.html')) + // 清掉历史 localStorage(收起态持久化),再重载让侧栏以展开态启动 + await win.webContents.executeJavaScript('try { localStorage.clear() } catch {}') + await win.reload() + await sleep(1200) + + // 注入一条含 markdown 的会话(user + assistant) + win.webContents.send('agent:event', { + type: 'session_switched', + state: FAKE_STATE, + messages: [ + { role: 'user', content: '解释一下那两句话的意思' }, + { role: 'assistant', content: [{ type: 'text', text: MARKDOWN_SAMPLE }] } + ], + activeTools: {} + }) + await sleep(1200) + console.log( + 'hl-diag:', + await win.webContents.executeJavaScript( + `(() => { const codes = [...document.querySelectorAll('code')]; return JSON.stringify({ ready: window.__shikiReady, codes: codes.map((c) => c.querySelectorAll('span[style]').length) }) })()` + ) + ) + await capture(win, 'app-markdown.png') + + // 注入排队消息场景(streaming + 队列两条) + win.webContents.send('agent:event', { type: 'agent_start' }) + win.webContents.send('agent:event', { + type: 'queue_update', + steering: ['设置里面做成这样,分成基础设置、Agent 能力'], + followUp: ['随后帮我补上skills、命令和附件的支持,chatbox里面/调起命令或skill,@则是引用文件,加号是附件'] + }) + await sleep(900) + win.webContents.invalidate() + await sleep(300) + FAKE_STATE.permissionMode = 'confirm' + await capture(win, 'app-queue.png') + + // 权限模式菜单 + await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) + win.webContents.send('agent:event', { type: 'agent_end' }) + win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] }) + await sleep(500) + await win.webContents.executeJavaScript( + `[...document.querySelectorAll('button')].find((b) => b.title.startsWith('权限模式'))?.click()` + ) + await sleep(500) + win.webContents.invalidate() + await sleep(300) + console.log( + 'perm-diag:', + await win.webContents.executeJavaScript( + `(() => { const panels = [...document.querySelectorAll('div.fixed.z-40')]; const m = panels.find((d) => d.textContent.includes('减少确认次数')); if (!m) return 'closed'; const r = m.getBoundingClientRect(); const cs = getComputedStyle(m); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, opacity: cs.opacity, display: cs.display, zIndex: cs.zIndex, buttons: m.querySelectorAll('button').length }) })()` + ) + ) + await capture(win, 'app-permissions.png') + // 结束流式,恢复常规状态 + win.webContents.send('agent:event', { type: 'agent_end' }) + win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] }) + await sleep(400) + + // "/" 命令/技能面板 + const typeInPrompt = (text) => + win.webContents.executeJavaScript( + `(() => { const ta = document.querySelector('textarea[aria-label="Prompt"]'); if (!ta) return 'no-ta'; const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; set.call(ta, ${JSON.stringify(text)}); ta.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()` + ) + await typeInPrompt('/') + await sleep(600) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-slash-menu.png') + // "@ " 文件引用 + await typeInPrompt('帮我看看 @') + await sleep(600) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-at-menu.png') + await typeInPrompt('') + + // 注入审批卡 + win.webContents.send('agent:event', { + type: 'approval_request', + request: { + id: 'approval-1', + toolName: 'write', + args: { path: 'hello_world.py', content: 'def main():\n print("Hello world")\n\nmain()' } + } + }) + await sleep(800) + win.webContents.invalidate() + await sleep(400) + await capture(win, 'app-approval.png') + + // 行内操作浮层:悬停操作按钮点击后弹出(重命名 / 删除) + await win.webContents.executeJavaScript( + `[...document.querySelectorAll('button')].find((b) => b.title === '重命名会话')?.click()` + ) + await sleep(500) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-row-rename.png') + await win.webContents.executeJavaScript( + `[...document.querySelectorAll('button')].find((b) => b.title === '删除会话')?.click()` + ) + await sleep(500) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-row-delete.png') + await win.webContents.executeJavaScript(`document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))`) + + // 设置页:Ctrl+, 打开,截「模型设置」与「常规」 + await win.webContents.executeJavaScript( + `window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true, bubbles: true }))` + ) + await sleep(600) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-settings-model.png') + await win.webContents.executeJavaScript( + `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '常规')?.click()` + ) + await sleep(400) + win.webContents.invalidate() + await sleep(300) + await capture(win, 'app-settings-general.png') + await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) + + console.log( + 'diag:', + await win.webContents.executeJavaScript( + `(() => { const btn = document.querySelector('button[aria-label="拒绝并关闭"]'); if (!btn) return 'missing'; const card = btn.closest('div[class*="rounded-card"]') ?? btn.parentElement; const r = card.getBoundingClientRect(); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, winH: innerHeight, opacity: getComputedStyle(card).opacity }) })()` + ) + ) + + // 侧栏收起回归检查 + const collapsed = await win.webContents.executeJavaScript( + `(() => { const b = document.querySelector('[aria-label="折叠侧栏"]'); if (!b) return 'no-button'; b.click(); return 'clicked' })()` + ) + console.log('collapse:', collapsed) + await sleep(800) + const geo = await win.webContents.executeJavaScript( + `(() => { + const aside = document.querySelector('aside[aria-label="工作区导航"]') + const row = document.querySelector('.sidebar-row') + const icon = row ? row.querySelector('span') : null + if (!aside || !row || !icon) return 'missing' + const a = aside.getBoundingClientRect() + const i = icon.getBoundingClientRect() + return JSON.stringify({ rail: Math.round(a.width), iconCenter: Math.round(i.left - a.left + i.width / 2) }) + })()` + ) + console.log('collapsed-geo:', geo) + await capture(win, 'app-collapsed.png') + + app.exit(0) +} + +app.whenReady().then(run).catch((err) => { + console.error(err) + app.exit(1) +}) diff --git a/apps/desktop/scripts/capture-preview.cjs b/apps/desktop/scripts/capture-preview.cjs new file mode 100644 index 0000000..2353970 --- /dev/null +++ b/apps/desktop/scripts/capture-preview.cjs @@ -0,0 +1,95 @@ +// 预览页视觉验收截图(Electron 离屏捕获): +// npx electron scripts/capture-preview.cjs +// 产出 preview-shots/*.png:整页 / 侧栏折叠 / 搜索面板 / 选区工具条 +const { app, BrowserWindow } = require('electron') +const { mkdirSync, writeFileSync } = require('node:fs') +const { join } = require('node:path') + +// 离屏捕获在 GPU 合成异常时会得到空白帧,改用软件合成 +app.commandLine.appendSwitch('disable-gpu') + +const URL = 'http://localhost:4173/' +const OUT = join(__dirname, '..', 'preview-shots') + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +async function capture(win, name) { + try { + const image = await win.webContents.capturePage() + writeFileSync(join(OUT, name), image.toPNG()) + console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`) + } catch (e) { + console.log('capture failed', name, e.message) + } +} + +async function js(win, code) { + try { + return await win.webContents.executeJavaScript(code) + } catch (e) { + console.log('js failed:', e.message) + return undefined + } +} + +async function run() { + mkdirSync(OUT, { recursive: true }) + const win = new BrowserWindow({ + width: 1280, + height: 960, + show: false, + webPreferences: { paintWhenInitiallyHidden: true } + }) + await win.loadURL(URL) + await sleep(1500) + + // 1) 整页(上半) + await capture(win, '01-full.png') + + // 2) 折叠侧栏(等宽度过渡) + await js(win, `document.querySelector('[aria-label="折叠侧栏"]')?.click()`) + await sleep(700) + await capture(win, '02-sidebar-collapsed.png') + await js(win, `document.querySelector('[aria-label="展开侧栏"]')?.click()`) + await sleep(700) + + // 3) 搜索面板(侧栏「搜索」行)+ 输入过滤 + await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '搜索')?.click()`) + await sleep(600) + await js(win, `(() => { const i = document.querySelector('input[aria-label="搜索会话或命令"]'); if (!i) return 'no-input'; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; set.call(i, '供应'); i.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()`) + await sleep(400) + await capture(win, '03-search-filtered.png') + await js(win, `document.querySelector('input[aria-label="搜索会话或命令"]')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) + await sleep(400) + + // 4) 划选文本 → Selection Actions 工具条(展开「更多」) + await js(win, `(() => { + const p = [...document.querySelectorAll('p')].find((el) => el.textContent.includes('百香果')); + if (!p) return 'no-p'; + const textNode = p.firstChild; + const range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, 30); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + return 'ok'; + })()`) + await sleep(600) + await js(win, `document.querySelector('[aria-label="更多操作"]')?.click()`) + await sleep(500) + await capture(win, '04-selection-actions.png') + + // 5) 审批卡交互:选择「允许执行」(radio 自动前进 480ms 后提交) + await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '允许执行')?.click()`) + await sleep(900) + await capture(win, '05-approval-approved.png') + + app.exit(0) +} + +app.whenReady().then(run).catch((err) => { + console.error(err) + app.exit(1) +}) diff --git a/apps/desktop/scripts/debug-collapse.cjs b/apps/desktop/scripts/debug-collapse.cjs new file mode 100644 index 0000000..bfdb5b8 --- /dev/null +++ b/apps/desktop/scripts/debug-collapse.cjs @@ -0,0 +1,21 @@ +// 验证侧栏折叠交互 +const { app, BrowserWindow } = require('electron') +app.commandLine.appendSwitch('disable-gpu') +app.commandLine.appendSwitch('disable-software-rasterizer') + +app.whenReady().then(async () => { + const win = new BrowserWindow({ width: 1280, height: 900, show: false, webPreferences: { offscreen: true } }) + await win.loadURL('http://127.0.0.1:4173/') + await new Promise((r) => setTimeout(r, 1200)) + const found = await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]') ? 'found' : 'NOT-FOUND'`) + console.log('collapse btn:', found) + await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]').click()`) + await new Promise((r) => setTimeout(r, 600)) + console.log( + 'after click: collapsed=', + await win.webContents.executeJavaScript(`document.querySelector('aside')?.getAttribute('data-sidebar-collapsed')`), + 'width=', + await win.webContents.executeJavaScript(`document.querySelector('aside')?.style.width`) + ) + app.exit(0) +}) diff --git a/apps/desktop/scripts/debug-preview.cjs b/apps/desktop/scripts/debug-preview.cjs new file mode 100644 index 0000000..6d8d806 --- /dev/null +++ b/apps/desktop/scripts/debug-preview.cjs @@ -0,0 +1,41 @@ +// 预览页控制台诊断:node 调试某一分区 +// npx electron scripts/debug-preview.cjs "#thinking" +const { app, BrowserWindow } = require('electron') + +const hash = process.argv[2] ?? '' +const URL = `http://127.0.0.1:4173/${hash}` + +// 兜底退出(渲染进程卡死时 executeJavaScript 永不返回) +const guard = setTimeout(() => { + console.log('[guard] hard exit (page busy or crashed)') + app.exit(2) +}, 12000) + +app.whenReady().then(async () => { + const win = new BrowserWindow({ + width: 1280, + height: 900, + show: false, + webPreferences: { offscreen: true } + }) + win.webContents.on('console-message', (_e, level, message, line, source) => { + console.log(`[console:${level}]`, message, `(${source}:${line})`) + }) + win.webContents.on('did-fail-load', (_e, code, desc, url) => { + console.log(`[did-fail-load] ${code} ${desc} ${url}`) + }) + win.webContents.on('render-process-gone', (_e, details) => { + console.log('[crashed]', details.reason) + }) + await win.loadURL(URL) + setTimeout(async () => { + try { + const text = await win.webContents.executeJavaScript('document.body.innerText.slice(0, 120)') + console.log('[body]', JSON.stringify(text)) + } catch (e) { + console.log('[executeJavaScript failed]', e.message) + } + clearTimeout(guard) + app.exit(0) + }, 2500) +}) diff --git a/apps/desktop/scripts/inspect-bundle.cjs b/apps/desktop/scripts/inspect-bundle.cjs new file mode 100644 index 0000000..f281903 --- /dev/null +++ b/apps/desktop/scripts/inspect-bundle.cjs @@ -0,0 +1,17 @@ +// 对比 out/renderer 旧 bundle 与当前源码的 reducer 逻辑 +const fs = require('fs') +const t = fs.readFileSync('apps/desktop/out/renderer/assets/index-DJXyfP9T.js', 'utf8') + +console.log('has message_end:', t.includes('message_end')) +console.log('has optimistic-user:', t.includes('optimistic-user')) +console.log('has session_switched:', t.includes('session_switched')) +console.log('has agent_settled:', t.includes('agent_settled')) +console.log('has approval_request:', t.includes('approval_request')) + +// reducer 里 message_end 附近的代码 +let i = t.indexOf('"message_end"') +if (i < 0) i = t.indexOf('message_end') +if (i >= 0) { + console.log('\n=== around first message_end ===') + console.log(t.slice(i - 500, i + 900)) +} diff --git a/apps/desktop/scripts/make-icons.cjs b/apps/desktop/scripts/make-icons.cjs new file mode 100644 index 0000000..2122495 --- /dev/null +++ b/apps/desktop/scripts/make-icons.cjs @@ -0,0 +1,87 @@ +// 生成应用图标(build/icon.ico + build/icon.png): +// npx electron scripts/make-icons.cjs +// 用 Electron 离屏渲染 build/icon.svg(居中于方形画布、留 9% 边距), +// 输出多尺寸 PNG 并打包为 PNG 内嵌式 ICO(Vista+ 支持)。 +const { app, BrowserWindow } = require('electron') +const { readFileSync, writeFileSync } = require('node:fs') +const { join } = require('node:path') + +app.commandLine.appendSwitch('disable-gpu') + +const SIZES = [16, 24, 32, 48, 64, 128, 256] +const ROOT = join(__dirname, '..') + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +/** PNG 内嵌式 ICO 打包 */ +function buildIco(pngs) { + const count = pngs.length + const header = Buffer.alloc(6) + header.writeUInt16LE(0, 0) // reserved + header.writeUInt16LE(1, 2) // type: icon + header.writeUInt16LE(count, 4) + const entries = Buffer.alloc(16 * count) + const blobs = [] + let offset = 6 + 16 * count + pngs.forEach((png, i) => { + const size = png.size + entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 0) // width + entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 1) // height + entries.writeUInt8(0, i * 16 + 2) // palette + entries.writeUInt8(0, i * 16 + 3) // reserved + entries.writeUInt16LE(1, i * 16 + 4) // planes + entries.writeUInt16LE(32, i * 16 + 6) // bpp + entries.writeUInt32LE(png.data.length, i * 16 + 8) + entries.writeUInt32LE(offset, i * 16 + 12) + offset += png.data.length + blobs.push(png.data) + }) + return Buffer.concat([header, entries, ...blobs]) +} + +async function renderSize(win, svgDataUrl, size) { + await win.loadURL( + 'data:text/html,' + + encodeURIComponent( + `` + + `
` + + `` + + `
` + ) + ) + await sleep(350) + const image = await win.webContents.capturePage({ x: 0, y: 0, width: size, height: size }) + return image.toPNG() +} + +async function run() { + const svg = readFileSync(join(ROOT, 'build', 'icon.svg'), 'utf8') + const svgDataUrl = 'data:image/svg+xml;base64,' + Buffer.from(svg, 'utf8').toString('base64') + + const win = new BrowserWindow({ + width: 256, + height: 256, + show: false, + frame: false, + transparent: true, + webPreferences: { paintWhenInitiallyHidden: true, offscreen: true } + }) + + const pngs = [] + for (const size of SIZES) { + await win.setSize ? win.setSize(size, size) : null + const png = await renderSize(win, svgDataUrl, size) + pngs.push({ size, data: png }) + console.log('rendered', size, `${png.length}B`) + } + + writeFileSync(join(ROOT, 'build', 'icon.png'), pngs.find((p) => p.size === 256).data) + writeFileSync(join(ROOT, 'build', 'icon.ico'), buildIco(pngs)) + console.log('written build/icon.ico & build/icon.png') + app.exit(0) +} + +app.whenReady().then(run).catch((err) => { + console.error(err) + app.exit(1) +}) diff --git a/apps/desktop/scripts/read-dts.cjs b/apps/desktop/scripts/read-dts.cjs new file mode 100644 index 0000000..535f5ab --- /dev/null +++ b/apps/desktop/scripts/read-dts.cjs @@ -0,0 +1,15 @@ +// 打印 pi SDK 关键类型定义(skills / slash-commands / prompt 参数) +const fs = require('node:fs') +const files = [ + 'dist/core/skills.d.ts', + 'dist/core/slash-commands.d.ts', + 'dist/modes/interactive/components/skill-invocation-message.d.ts' +] +for (const f of files) { + console.log('=====', f) + try { + console.log(fs.readFileSync(f, 'utf8')) + } catch (e) { + console.log('READ FAIL', e.message) + } +} diff --git a/apps/desktop/scripts/read-dts2.cjs b/apps/desktop/scripts/read-dts2.cjs new file mode 100644 index 0000000..af57092 --- /dev/null +++ b/apps/desktop/scripts/read-dts2.cjs @@ -0,0 +1,22 @@ +// 打印 ImageContent / PromptTemplate / AgentSessionServices 类型 +const fs = require('node:fs') +const root = 'C:/Users/21976/Desktop/Develop/agent-store-operations/apps/desktop/node_modules' +const files = [ + [root + '/@earendil-works/pi-ai/dist/compat.d.ts', /export (interface|type) (ImageContent|TextContent)[\s\S]*?\n\}/], + [root + '/@earendil-works/pi-coding-agent/dist/core/prompt-templates.d.ts', null], + [root + '/@earendil-works/pi-coding-agent/dist/core/sdk.d.ts', /export interface AgentSessionServices[\s\S]*?\n\}/] +] +for (const [f, re] of files) { + console.log('=====', f.split('/').slice(-2).join('/')) + try { + const s = fs.readFileSync(f, 'utf8') + if (re) { + const m = s.match(re) + console.log(m ? m[0] : '(no match)') + } else { + console.log(s.slice(0, 2500)) + } + } catch (e) { + console.log('READ FAIL', e.message) + } +} diff --git a/apps/desktop/scripts/repro-empty-workspace.mjs b/apps/desktop/scripts/repro-empty-workspace.mjs new file mode 100644 index 0000000..6aabd83 --- /dev/null +++ b/apps/desktop/scripts/repro-empty-workspace.mjs @@ -0,0 +1,21 @@ +// 验证 pi SDK 对空目录(无会话的工作区)的行为: +// node scripts/repro-empty-workspace.mjs "C:\path\to\empty\dir" +import { SessionManager, getAgentDir } from '@earendil-works/pi-coding-agent' +import { mkdirSync, mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const target = process.argv[2] ?? mkdtempSync(join(tmpdir(), 'ws-empty-')) +mkdirSync(target, { recursive: true }) +console.log('target cwd:', target) + +try { + const sm = SessionManager.continueRecent(target) + console.log('continueRecent OK, sessionFile:', sm.sessionFile ?? '(none)') + const list = await SessionManager.list(target) + console.log('list count:', list.length) +} catch (err) { + console.error('continueRecent FAILED:', err instanceof Error ? err.message : err) + process.exit(1) +} +console.log('agentDir:', getAgentDir()) diff --git a/apps/desktop/scripts/repro-event-flow.mjs b/apps/desktop/scripts/repro-event-flow.mjs new file mode 100644 index 0000000..fc3a8b3 --- /dev/null +++ b/apps/desktop/scripts/repro-event-flow.mjs @@ -0,0 +1,103 @@ +// 复现渲染层"输出一闪而过":用与 agent-service.ts 相同的 SDK 调用链, +// 订阅全部事件并打印事件序列与消息对象结构。 +import os from 'node:os' +import { + createAgentSessionRuntime, + createAgentSessionServices, + createAgentSessionFromServices, + getAgentDir, + SessionManager +} from '@earendil-works/pi-coding-agent' + +const cwd = os.homedir() +const agentDir = getAgentDir() + +const services = await createAgentSessionServices({ cwd, agentDir }) +const factory = async (opts) => { + const result = await createAgentSessionFromServices({ + services, + sessionManager: opts.sessionManager, + sessionStartEvent: opts.sessionStartEvent + }) + return { ...result, services, diagnostics: services.diagnostics } +} + +const sessionManager = SessionManager.continueRecent(cwd) +const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager }) +const session = runtime.session + +let rebindCount = 0 +runtime.setRebindSession(async (s) => { + rebindCount++ + console.log(`\n>>> REBIND fired (#${rebindCount}) — 渲染层会收到 session_switched,messages.length=${s.messages.length}`) +}) + +session.subscribe((event) => { + switch (event.type) { + case 'message_start': + console.log('EVT message_start role=', JSON.stringify(event.message?.role)) + break + case 'message_update': { + const inner = event.assistantMessageEvent + if (inner) { + if (inner.type.endsWith('_delta')) { + process.stdout.write(`.`) + } else { + console.log(`EVT message_update ${inner.type}`) + } + } + break + } + case 'message_end': { + const m = event.message + console.log('\nEVT message_end role=', m?.role) + console.log(' message keys:', Object.keys(m ?? {})) + try { + JSON.stringify(m) + console.log(' JSON.stringify: OK, length=', JSON.stringify(m).length) + } catch (err) { + console.log(' JSON.stringify FAILED ->', err.message, '(toCloneable 会变成 null → 渲染层消息变空!)') + } + if (m?.role === 'assistant') { + console.log(' stopReason=', m.stopReason, ' errorMessage=', m.errorMessage) + console.log(' content types=', (m.content ?? []).map((b) => b.type).join(',')) + for (const b of m.content ?? []) { + if (b.type === 'text') console.log(' text block:', JSON.stringify(b.text).slice(0, 200)) + } + } + break + } + case 'agent_end': + console.log(`\nEVT agent_end willRetry=${event.willRetry} messages=${event.messages?.length}`) + break + default: + console.log('EVT', event.type) + } +}) + +console.log('--- session:', session.sessionId, 'messages:', session.messages.length) +console.log('--- model:', session.model?.provider + '/' + session.model?.id) +console.log('--- sending "hi" ...') + +try { + await session.prompt('hi') + console.log('\n--- prompt resolved') +} catch (err) { + console.log('\n--- prompt THREW:', err?.message) +} + +// 等待 agent_settled 之后的异步收尾 +await new Promise((r) => setTimeout(r, 1500)) + +console.log('\n=== final session.messages ===') +for (const [i, m] of session.messages.entries()) { + const blocks = Array.isArray(m.content) + ? m.content.map((b) => b.type).join(',') + : typeof m.content === 'string' + ? 'str:' + m.content.slice(0, 30) + : '??' + console.log(`#${i} ${m.role} [${blocks}]`) +} + +console.log('\nrebindCount=', rebindCount) +process.exit(0) diff --git a/apps/desktop/scripts/repro-markdown.mjs b/apps/desktop/scripts/repro-markdown.mjs new file mode 100644 index 0000000..d37c46a --- /dev/null +++ b/apps/desktop/scripts/repro-markdown.mjs @@ -0,0 +1,41 @@ +// 验证 Markdown.tsx 的 parseBlocks 是否产出空内容 +const INLINE = /^(\s*)([-*+]\s+|\d+[.)]\s+|>\s?|(#{1,3}\s+))?(.*)$/ + +function parseBlocks(text) { + const blocks = [] + const lines = text.split('\n') + let current = null + const flush = () => { + if (current) blocks.push(current) + current = null + } + for (const line of lines) { + const m = line.match(INLINE) + if (!m) continue + const [, indent, marker, content] = m + const isListItem = Boolean(marker && /^([-*+]\s+|\d+[.)]\s+)/.test(marker)) + const kind = marker + ? marker.startsWith('#') + ? 'h1' + : isListItem + ? /^\d/.test(marker) ? 'ol' : 'ul' + : marker.startsWith('>') ? 'quote' : null + : 'p' + if (kind === 'p') { + if (!current || current.kind !== 'p') { + flush() + current = { kind: 'p', lines: [] } + } + current.lines.push(content) + continue + } + flush() + } + return blocks +} + +for (const t of ['pong', 'Hello world', '# 标题', '- 列表项', '> 引用', '1. 有序']) { + const blocks = parseBlocks(t) + const rendered = blocks.map((b) => b.lines.join('\n')) + console.log(JSON.stringify(t), '->', JSON.stringify(rendered)) +} diff --git a/apps/desktop/scripts/repro-render-logic.mjs b/apps/desktop/scripts/repro-render-logic.mjs new file mode 100644 index 0000000..63834d1 --- /dev/null +++ b/apps/desktop/scripts/repro-render-logic.mjs @@ -0,0 +1,81 @@ +// 验证渲染层对 session_switched 历史消息的渲染逻辑: +// 用与主进程一致的 session.messages 源,跑 MessageItem 的 readBlocks + AssistantBody 判断。 +import os from 'node:os' +import { createAgentSessionServices, getAgentDir } from '@earendil-works/pi-coding-agent' + +const cwd = os.homedir() +const services = await createAgentSessionServices({ cwd, agentDir: getAgentDir() }) + +// SessionManager.list + 加载最近会话的等价做法:直接用 services 里加载当前会话 +const session = services.sessionManager + ? null + : null + +// 走 SessionManager 拿最近会话并加载 +const { SessionManager } = await import('@earendil-works/pi-coding-agent') +const sm = SessionManager.continueRecent(cwd) +const smAny = sm +// SessionManager.continueRecent 返回的 manager 需要挂到 session 上才有 messages; +// 更直接:读它指向的会话文件并解析 message 条目 +const entries = [] +if (typeof smAny.loadEntries === 'function') { + entries.push(...(await smAny.loadEntries())) +} +console.log('loadEntries available:', typeof smAny.loadEntries === 'function', 'entries:', entries.length) + +// 退路:直接解析磁盘上的最近 jsonl(与主进程 toCloneable 后的消息同构) +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { join } from 'node:path' +const dir = join(cwd, '.pi', 'agent', 'sessions', '--C--Users-21976--') +const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl')) +files.sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs) +const latest = files[0] +console.log('latest session file:', latest) + +const lines = readFileSync(join(dir, latest), 'utf8').trim().split('\n') +const messages = [] +for (const line of lines) { + let entry + try { + entry = JSON.parse(line) + } catch { + continue + } + if (entry.type === 'message' && entry.message) messages.push(entry.message) +} + +// ==== 渲染层逻辑复刻(与 MessageItem.tsx 一致)==== +function readBlocks(raw) { + if (!raw || typeof raw !== 'object') return [] + const content = raw.content + if (typeof content === 'string') return [{ type: 'text', text: content }] + if (Array.isArray(content)) { + return content.filter((c) => typeof c === 'object' && c !== null) + } + return [] +} + +for (const [i, m] of messages.entries()) { + console.log(`\n=== history msg #${i} role=${m.role} ===`) + const blocks = readBlocks(m) + let rendered = 0 + for (const block of blocks) { + if (block.type === 'thinking' && typeof block.text === 'string') { + console.log(' -> ThinkingBlock (text field)') + rendered++ + continue + } + if (block.type === 'text' && block.text) { + console.log(` -> Markdown("${String(block.text).slice(0, 60)}…")`) + rendered++ + continue + } + if (block.type === 'toolCall' && block.id) { + console.log(' -> ToolCard', block.name) + rendered++ + continue + } + console.log(' -> **被丢弃** block type =', block.type, 'keys =', Object.keys(block).join(',')) + } + console.log(` rendered ${rendered}/${blocks.length} blocks`) +} diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index f2fd79d..3b21028 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,14 +1,35 @@ -import { app, BrowserWindow, shell } from 'electron' +import { app, BrowserWindow, ipcMain, Menu, shell } from 'electron' +import { existsSync } from 'node:fs' import { join } from 'node:path' import { registerIpc } from './ipc' process.env.APP_ROOT = join(__dirname, '..') -export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL +export const VITE_DEV_SERVER_URL = process.env.ELECTRON_RENDERER_URL ?? process.env.VITE_DEV_SERVER_URL export const RENDERER_DIST = join(process.env.APP_ROOT, 'renderer') +/** 应用图标(build/icon.ico,由 scripts/make-icons.cjs 从 build/icon.svg 生成) */ +const APP_ICON = join(app.getAppPath(), 'build', 'icon.ico') + const agentService = registerIpc() +/** 窗口控制(自绘标题栏按钮走这三个 IPC;不使用系统 WCO, + * 否则 overlay 的原生白色背景会盖住标题栏分隔线) */ +function registerWindowControls(): void { + ipcMain.handle('win:minimize', (event) => { + BrowserWindow.fromWebContents(event.sender)?.minimize() + }) + ipcMain.handle('win:toggleMaximize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win) return + if (win.isMaximized()) win.unmaximize() + else win.maximize() + }) + ipcMain.handle('win:close', (event) => { + BrowserWindow.fromWebContents(event.sender)?.close() + }) +} + async function createWindow(): Promise { const win = new BrowserWindow({ width: 1280, @@ -16,8 +37,12 @@ async function createWindow(): Promise { minWidth: 960, minHeight: 640, show: false, - backgroundColor: '#09090b', - title: '店铺运营 Agent', + backgroundColor: '#ffffff', + title: 'Inkreach Agent', + // 窗口/任务栏图标(Windows/Linux;macOS 打包时由 build 配置提供 icns) + ...(existsSync(APP_ICON) ? { icon: APP_ICON } : {}), + // 无原生标题栏/菜单栏,标题栏与窗口控制按钮全部由渲染层 TitleBar 自绘 + titleBarStyle: 'hidden', webPreferences: { preload: join(__dirname, '../preload/index.cjs'), sandbox: false, @@ -26,6 +51,18 @@ async function createWindow(): Promise { } }) + // 最大化状态同步给渲染层(切换「最大化/还原」字形) + const sendWinState = (): void => win.webContents.send('win:state', win.isMaximized()) + win.on('maximize', sendWinState) + win.on('unmaximize', sendWinState) + + // 默认应用菜单已移除(File/Edit/View 不再显示),F12 补回 DevTools 开关 + win.webContents.on('before-input-event', (_event, input) => { + if (input.type === 'keyDown' && input.key === 'F12') { + win.webContents.toggleDevTools() + } + }) + win.on('ready-to-show', () => { win.show() // pi 会话初始化放在窗口就绪后,错误可以第一时间在 UI 上呈现 @@ -46,8 +83,8 @@ async function createWindow(): Promise { }) // 渲染层日志/加载失败转发到主进程 stdout,便于排查白屏/黑屏问题 - win.webContents.on('console-message', (_e, level, message, line, sourceId) => { - console.log(`[renderer:${level}] ${message} (${sourceId}:${line})`) + win.webContents.on('console-message', (details) => { + console.log(`[renderer:${details.level}] ${details.message} (${details.sourceId}:${details.lineNumber})`) }) win.webContents.on('did-fail-load', (_e, code, desc, url) => { console.log(`[renderer:load-fail] ${code} ${desc} ${url}`) @@ -79,6 +116,11 @@ if (!app.requestSingleInstanceLock()) { }) void app.whenReady().then(() => { + // Windows 任务栏归组的 AppUserModelID + if (process.platform === 'win32') { + app.setAppUserModelId('com.inkreach.agent') + } + Menu.setApplicationMenu(null) void createWindow() app.on('activate', () => { @@ -89,4 +131,6 @@ if (!app.requestSingleInstanceLock()) { app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) + + registerWindowControls() } diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index abf53df..eb3bc21 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -1,4 +1,5 @@ -import { BrowserWindow, ipcMain } from 'electron' +import { BrowserWindow, dialog, ipcMain } from 'electron' +import { userInfo } from 'node:os' import { AgentService } from './pi/agent-service' /** @@ -16,11 +17,20 @@ export function registerIpc(): AgentService { } }) - ipcMain.handle('agent:send', (_event, text: unknown) => { + ipcMain.handle('agent:send', (_event, text: unknown, images: unknown) => { if (typeof text !== 'string' || !text.trim()) { throw new Error('empty prompt') } - return service.send(text) + const sendImages = Array.isArray(images) + ? images.filter( + (img): img is { mimeType: string; data: string } => + typeof img === 'object' && + img !== null && + typeof (img as { mimeType?: unknown }).mimeType === 'string' && + typeof (img as { data?: unknown }).data === 'string' + ) + : undefined + return service.send(text, sendImages) }) ipcMain.handle('agent:steer', (_event, text: unknown) => { @@ -105,6 +115,39 @@ export function registerIpc(): AgentService { return pass(() => service.setSystemPrompt(text)) }) + // ---- 工作区 ---- + ipcMain.handle('workspaces:list', () => + service.listWorkspaces().catch((err: unknown) => { + throw new Error(err instanceof Error ? err.message : String(err)) + }) + ) + + ipcMain.handle('workspaces:create', (_event, name: unknown) => { + if (typeof name !== 'string' || !name.trim()) throw new Error('invalid workspace name') + return service.createWorkspace(name).catch((err: unknown) => { + throw new Error(err instanceof Error ? err.message : String(err)) + }) + }) + + ipcMain.handle('workspaces:open', (_event, path: unknown) => { + if (typeof path !== 'string' || !path.trim()) throw new Error('invalid workspace path') + return service.openWorkspace(path).catch((err: unknown) => { + throw new Error(err instanceof Error ? err.message : String(err)) + }) + }) + + // 调起系统目录选择器;取消返回 null + ipcMain.handle('workspaces:browse', async () => { + const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] + const result = await dialog.showOpenDialog(win, { + title: '选择工作区目录', + defaultPath: service.workspacesRoot, + properties: ['openDirectory', 'createDirectory'] + }) + if (result.canceled || result.filePaths.length === 0) return null + return result.filePaths[0] + }) + // ---- 工具审批 ---- ipcMain.handle('approval:resolve', (_event, id: unknown, approved: unknown) => { if (typeof id !== 'string' || typeof approved !== 'boolean') throw new Error('invalid approval') @@ -117,8 +160,39 @@ export function registerIpc(): AgentService { app: process.env.npm_package_version ?? '0.0.0', electron: process.versions.electron, node: process.versions.node, - chrome: process.versions.chrome + chrome: process.versions.chrome, + user: userInfo().username })) + // ---- Chatbox 资源(/ 命令 + 技能、@ 工作区文件、+ 附件)---- + ipcMain.handle('chat:resources', () => + service.listChatResources().catch((err: unknown) => { + throw new Error(err instanceof Error ? err.message : String(err)) + }) + ) + + ipcMain.handle('queue:set', (_event, steering: unknown, followUp: unknown) => { + const norm = (value: unknown): string[] => + Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [] + return service.setQueue(norm(steering), norm(followUp)) + }) + + ipcMain.handle('permissions:set', (_event, mode: unknown) => { + const valid: ReadonlySet = new Set(['confirm', 'autoEdit', 'plan', 'full']) + if (typeof mode !== 'string' || !valid.has(mode)) throw new Error('invalid permission mode') + service.setPermissionMode(mode as never) + }) + + ipcMain.handle('files:list', () => + service.listWorkspaceFiles().catch((err: unknown) => { + throw new Error(err instanceof Error ? err.message : String(err)) + }) + ) + + ipcMain.handle('attachments:pick', async (event) => { + const win = BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getAllWindows()[0] + return service.pickAttachments(win ?? undefined) + }) + return service } diff --git a/apps/desktop/src/main/pi/agent-service.ts b/apps/desktop/src/main/pi/agent-service.ts index b8ca928..f680b91 100644 --- a/apps/desktop/src/main/pi/agent-service.ts +++ b/apps/desktop/src/main/pi/agent-service.ts @@ -1,11 +1,12 @@ -import { app, shell } from 'electron' -import { readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' +import { app, dialog, shell } from 'electron' +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' import { createAgentSessionRuntime, createAgentSessionServices, createAgentSessionFromServices, getAgentDir, + loadSkills, SessionManager, type AgentSession, type AgentSessionEvent, @@ -18,11 +19,17 @@ import { demoTools, TOOLS_REQUIRING_APPROVAL } from '../tools/demo-tools' import type { AgentUiEvent, AppSettings, + AttachmentInfo, + ChatResources, ModelCatalog, ModelInfo, + PermissionMode, + SendImage, SessionMeta, SessionState, - SessionStatsInfo + SessionStatsInfo, + WorkspaceFile, + WorkspaceMeta } from '@shared/types' /** 增量事件合批的刷新间隔(ms),参考 pi-web 的 60ms 快照节流经验取略低值 */ @@ -43,10 +50,24 @@ export class AgentService { private attached: AgentSession | null = null private unsubscribeAttached: (() => void) | null = null private settings: AppSettings = { systemPrompt: '' } + /** 当前工作区目录(agent cwd),会话按目录隔离 */ + private workspaceDir = '' + private approvalsRegistered = false + /** 权限模式(Composer pill / 审批行为),持久化在应用设置里 */ + private permissionMode: PermissionMode = 'confirm' readonly approvals = new ApprovalService() + /** 工作区根目录:文档/Inkreach,每个子目录 = 一个工作区 */ + get workspacesRoot(): string { + return join(app.getPath('documents'), 'Inkreach') + } + + private get defaultWorkspaceDir(): string { + return join(this.workspacesRoot, 'Default') + } + private get cwd(): string { - return app.getPath('home') + return this.workspaceDir || this.defaultWorkspaceDir } private get settingsFile(): string { @@ -54,14 +75,103 @@ export class AgentService { } private loadSettings(): AppSettings { + const VALID_MODES: ReadonlySet = new Set(['confirm', 'autoEdit', 'plan', 'full']) try { const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as Partial - return { - systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : '' + this.settings = { + systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : '', + workspace: typeof raw.workspace === 'string' ? raw.workspace : undefined, + customWorkspaces: Array.isArray(raw.customWorkspaces) + ? raw.customWorkspaces.filter((p): p is string => typeof p === 'string') + : undefined, + permissionMode: + typeof raw.permissionMode === 'string' && VALID_MODES.has(raw.permissionMode) + ? (raw.permissionMode as PermissionMode) + : 'confirm' } } catch { - return { systemPrompt: '' } + this.settings = { systemPrompt: '', permissionMode: this.permissionMode } } + this.permissionMode = this.settings.permissionMode ?? 'confirm' + this.initWorkspaces() + return this.settings + } + + /** 确保根目录与默认工作区存在,并解析当前工作区(设置值失效时回退 Default) */ + private initWorkspaces(): void { + try { + mkdirSync(this.defaultWorkspaceDir, { recursive: true }) + } catch (err) { + console.error('[agent-service] init workspaces failed:', err) + } + const saved = this.settings.workspace + this.workspaceDir = saved && existsSync(saved) ? saved : this.defaultWorkspaceDir + } + + // ---- 工作区 ---- + + async listWorkspaces(): Promise { + try { + mkdirSync(this.defaultWorkspaceDir, { recursive: true }) + } catch { + // 目录已存在或创建失败时仍尝试枚举 + } + const entries: WorkspaceMeta[] = [] + const seen = new Set([this.workspacesRoot.toLowerCase()]) + try { + for (const d of readdirSync(this.workspacesRoot, { withFileTypes: true })) { + if (!d.isDirectory()) continue + const path = join(this.workspacesRoot, d.name) + entries.push({ name: d.name, path }) + seen.add(path.toLowerCase()) + } + } catch (err) { + console.error('[agent-service] list workspaces failed:', err) + } + // 目录选择器添加的根目录之外的工作区 + for (const p of this.settings.customWorkspaces ?? []) { + if (!existsSync(p)) continue + const normalized = join(p).replace(/[/\\]+$/, '') + if (seen.has(normalized.toLowerCase())) continue + seen.add(normalized.toLowerCase()) + entries.push({ name: dirname(normalized) === normalized ? normalized : normalized.split(/[\\/]/).pop() || normalized, path: normalized }) + } + if (!entries.some((w) => w.name === 'Default')) { + entries.unshift({ name: 'Default', path: this.defaultWorkspaceDir }) + } + return entries.sort((a, b) => (a.name === 'Default' ? -1 : b.name === 'Default' ? 1 : a.name.localeCompare(b.name))) + } + + async createWorkspace(name: string): Promise { + const trimmed = name.trim() + if (!trimmed || /[\\/:*?"<>|]/.test(trimmed)) { + throw new Error('工作区名称非法(不能包含 \\/:*?"<>|)') + } + const path = join(this.workspacesRoot, trimmed) + if (existsSync(path)) { + throw new Error(`工作区已存在: ${trimmed}`) + } + mkdirSync(path, { recursive: true }) + return { name: trimmed, path } + } + + /** 切换工作区:持久化设置后重建运行时(cwd 变化 → 会话列表随目录隔离)。 + * 允许任意已存在目录;根目录之外的目录记入自定义工作区以便下次列出。 */ + async openWorkspace(path: string): Promise { + const resolved = join(path).replace(/[/\\]+$/, '') + if (!existsSync(resolved)) { + throw new Error(`工作区目录不存在: ${path}`) + } + if (samePath(resolved, this.cwd)) return + if (dirname(resolved).toLowerCase() !== this.workspacesRoot.toLowerCase()) { + const custom = new Set(this.settings.customWorkspaces ?? []) + custom.add(resolved) + this.settings.customWorkspaces = [...custom] + } + this.settings.workspace = resolved + this.saveSettings() + this.workspaceDir = resolved + await this.rebuildRuntime() } private saveSettings(): void { @@ -118,20 +228,25 @@ export class AgentService { const cwd = this.cwd const agentDir = getAgentDir() this.settings = this.loadSettings() - // 审批拦截:TOOLS_REQUIRING_APPROVAL 名单内的工具调用需经渲染层 Approval Card 确认 - this.approvals.onRequest((request) => { - this.emit({ type: 'approval_request', request }) - }) - this.approvals.onSettle((id, approved, timedOut) => { - this.emit({ type: 'approval_settled', id, approved, timedOut }) - }) + // 审批拦截只需注册一次(create 会随工作区切换多次执行) + if (!this.approvalsRegistered) { + this.approvalsRegistered = true + this.approvals.onRequest((request) => { + this.emit({ type: 'approval_request', request }) + }) + this.approvals.onSettle((id, approved, timedOut) => { + this.emit({ type: 'approval_settled', id, approved, timedOut }) + }) + } const buildServices = (targetCwd: string, targetAgentDir: string) => createAgentSessionServices({ cwd: targetCwd, agentDir: targetAgentDir, resourceLoaderOptions: { ...(this.settings.systemPrompt.trim() ? { systemPrompt: this.settings.systemPrompt } : {}), - extensionFactories: [createApprovalExtension(this.approvals, TOOLS_REQUIRING_APPROVAL)] + extensionFactories: [ + createApprovalExtension(this.approvals, TOOLS_REQUIRING_APPROVAL, () => this.permissionMode) + ] } }) const initialServices = await buildServices(cwd, agentDir) @@ -164,6 +279,8 @@ export class AgentService { this.emitSwitched(session) }) this.attach(runtime.session) + // 此前 runtime 从未挂到实例上(rebuildRuntime 的 dispose 永远拿不到旧实例),补上 + this.runtime = runtime return runtime } @@ -306,7 +423,9 @@ export class AgentService { model: modelInfo, streaming: session.isStreaming, activeTools: session.getActiveToolNames(), - messageCount: session.messages.length + messageCount: session.messages.length, + workspace: this.cwd, + permissionMode: this.permissionMode } } @@ -331,13 +450,125 @@ export class AgentService { return this.buildState(runtime.session) } - async send(text: string): Promise { + async send(text: string, images?: SendImage[]): Promise { const runtime = await this.getRuntime() const session = runtime.session - if (session.isStreaming) { - await session.prompt(text, { streamingBehavior: 'followUp' }) - } else { - await session.prompt(text) + const imageContent = images?.length + ? images.map((img) => ({ type: 'image' as const, data: img.data, mimeType: img.mimeType })) + : undefined + const options = { + ...(session.isStreaming ? { streamingBehavior: 'followUp' as const } : {}), + ...(imageContent ? { images: imageContent } : {}) + } + await session.prompt(text, Object.keys(options).length > 0 ? options : undefined) + } + + // ---- Chatbox 资源(/ 命令、/skill: 技能)---- + + /** 重建排队消息(编辑/删除/排序/置顶都走这里:clear 后按新顺序重推)。 + * 空闲时队列会被 pi 立即消费,无需(也不能)重推。 */ + async setQueue(steering: string[], followUp: string[]): Promise { + const runtime = await this.getRuntime() + const session = runtime.session + if (!session.isStreaming) return + session.clearQueue() + for (const text of steering) void session.steer(text) + for (const text of followUp) void session.followUp(text) + } + + async listChatResources(): Promise { + const runtime = await this.getRuntime() + let skills: ChatResources['skills'] = [] + try { + const loaded = loadSkills({ + cwd: this.cwd, + agentDir: getAgentDir(), + skillPaths: [], + includeDefaults: true + }) + skills = loaded.skills.map((s) => ({ name: s.name, description: s.description ?? '' })) + } catch (err) { + console.error('[agent-service] load skills failed:', err) + } + const commands = runtime.session.promptTemplates.map((t) => ({ + name: t.name, + description: t.description ?? '', + kind: 'prompt' as const + })) + return { commands, skills } + } + + // ---- 工作区文件(@ 引用)---- + + async listWorkspaceFiles(): Promise { + const root = this.cwd + const out: WorkspaceFile[] = [] + const SKIP = new Set(['.git', 'node_modules', 'dist', 'out', 'build', '.pi', '.next', 'coverage']) + const walk = (dir: string, depth: number): void => { + if (depth > 4 || out.length >= 400) return + let entries: import('node:fs').Dirent[] + try { + entries = readdirSync(dir, { withFileTypes: true }) + } catch { + return + } + for (const entry of entries) { + if (out.length >= 400) return + if (entry.name.startsWith('.') && entry.name !== '.env') continue + const full = join(dir, entry.name) + const rel = full.slice(root.length + 1).replace(/\\/g, '/') + if (entry.isDirectory()) { + if (SKIP.has(entry.name)) continue + out.push({ path: rel, name: entry.name, isDir: true }) + walk(full, depth + 1) + } else if (entry.isFile()) { + out.push({ path: rel, name: entry.name, isDir: false }) + } + } + } + walk(root, 0) + return out + } + + // ---- 附件(+ 选择文件)---- + + async pickAttachments(parent?: Electron.BrowserWindow): Promise { + const options = { + title: '添加附件', + properties: ['openFile', 'multiSelections'] as Array<'openFile' | 'multiSelections'>, + filters: [ + { name: '常用文件', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'md', 'txt', 'csv', 'json', 'xlsx', 'docx'] }, + { name: '所有文件', extensions: ['*'] } + ] + } + const result = parent ? await dialog.showOpenDialog(parent, options) : await dialog.showOpenDialog(options) + if (result.canceled || result.filePaths.length === 0) return [] + return result.filePaths.map((path) => this.prepareAttachment(path)) + } + + prepareAttachment(path: string): AttachmentInfo { + const name = path.split(/[\\/]/).pop() ?? path + const ext = name.toLowerCase().split('.').pop() ?? '' + const IMAGE_MIME: Record = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp' + } + const mimeType = IMAGE_MIME[ext] ?? '' + if (!mimeType) { + return { name, path, mimeType: '', data: '' } + } + try { + const stat = statSync(path) + if (stat.size > 8 * 1024 * 1024) { + throw new Error(`图片过大(>8MB): ${name}`) + } + return { name, path, mimeType, data: readFileSync(path).toString('base64') } + } catch (err) { + console.error('[agent-service] read attachment failed:', err) + return { name, path, mimeType: '', data: '' } } } @@ -356,9 +587,25 @@ export class AgentService { async listSessions(): Promise { const infos = await SessionManager.list(this.cwd) - return infos + const metas = infos .sort((a, b) => b.modified.getTime() - a.modified.getTime()) .map((info) => toSessionMeta(info)) + // 新会话在首条消息写入前没有 .jsonl 文件,SessionManager.list 扫不到; + // 把当前活跃会话兜底合并进列表,避免侧栏“新建后不出现” + const session = this.runtime?.session + if (session && !metas.some((m) => samePath(m.path, session.sessionFile ?? ''))) { + const now = new Date().toISOString() + metas.unshift({ + id: session.sessionId, + path: session.sessionFile ?? '', + name: session.sessionName, + firstMessage: firstUserText(session.messages), + created: now, + modified: now, + messageCount: session.messages.length + }) + } + return metas } async newSession(): Promise { @@ -466,11 +713,22 @@ export class AgentService { /** 更新系统提示词后重建运行时(continueRecent 重新挂载当前最近会话,历史保留) */ async setSystemPrompt(text: string): Promise { - this.settings = { systemPrompt: text } + this.settings = { ...this.settings, systemPrompt: text } this.saveSettings() await this.rebuildRuntime() } + /** 切换权限模式:即时生效(审批扩展每笔工具调用实时读取),并广播给渲染层 */ + setPermissionMode(mode: PermissionMode): void { + this.permissionMode = mode + this.settings = { ...this.settings, permissionMode: mode } + this.saveSettings() + const session = this.runtime?.session ?? this.attached + if (session) { + this.emit({ type: 'state', state: this.buildState(session) }) + } + } + private async rebuildRuntime(): Promise { const old = this.runtime this.runtime = null @@ -518,3 +776,24 @@ function readRole(message: unknown): string { } return 'unknown' } + +/** 提取第一条用户消息的纯文本(新会话未落盘时用作列表标题) */ +function firstUserText(messages: unknown[]): string { + for (const message of messages) { + if (readRole(message) !== 'user') continue + const content = (message as { content?: unknown }).content + if (typeof content === 'string' && content.trim()) return content + if (Array.isArray(content)) { + const text = content + .map((block) => + block && typeof block === 'object' && typeof (block as { text?: unknown }).text === 'string' + ? (block as { text: string }).text + : '' + ) + .join('') + .trim() + if (text) return text + } + } + return '' +} diff --git a/apps/desktop/src/main/pi/approval.ts b/apps/desktop/src/main/pi/approval.ts index 245c555..e7568c7 100644 --- a/apps/desktop/src/main/pi/approval.ts +++ b/apps/desktop/src/main/pi/approval.ts @@ -1,14 +1,19 @@ import { randomUUID } from 'node:crypto' import type { ExtensionAPI, InlineExtension } from '@earendil-works/pi-coding-agent' +import type { PermissionMode } from '@shared/types' /** * 工具调用审批桥: * - extension 拦截 tool_call → 挂起等待渲染层审批 * - ApprovalService 维护 pending 请求,经 IPC 与 Approval Card 交互 * - 超时自动拒绝,避免 agent 永久阻塞 + * - 权限模式:full 全放行 / autoEdit 自动放行文件编辑 / plan 拦截修改类操作 */ const APPROVAL_TIMEOUT_MS = 120_000 +/** 文件编辑类工具(autoEdit 模式自动放行;plan 模式拦截) */ +const EDIT_TOOLS = new Set(['write', 'edit']) + export interface PendingApproval { id: string toolName: string @@ -67,17 +72,29 @@ function cloneable(value: unknown): unknown { /** * 生产 approval 拦截 extension: - * 匹配 NEEDS_APPROVAL 名单的工具调用先经用户审批,拒绝则 block。 + * 匹配 NEEDS_APPROVAL 名单的工具调用按权限模式处理 —— + * confirm:先经用户审批,拒绝则 block;full:全部放行; + * autoEdit:文件编辑自动放行(bash 等仍需审批);plan:修改类操作直接拦截。 */ export function createApprovalExtension( service: ApprovalService, - needsApproval: ReadonlySet + needsApproval: ReadonlySet, + getMode: () => PermissionMode ): InlineExtension { return { name: 'aso-approval', factory: (pi: ExtensionAPI) => { pi.on('tool_call', async (event) => { if (!needsApproval.has(event.toolName)) return + const mode = getMode() + if (mode === 'full') return + if (mode === 'autoEdit' && EDIT_TOOLS.has(event.toolName)) return + if (mode === 'plan' && (EDIT_TOOLS.has(event.toolName) || event.toolName === 'bash')) { + return { + block: true, + reason: '计划模式已开启:请先输出完整的执行计划(只读分析),待用户切换权限模式后再执行修改类操作。' + } + } const approved = await service.request(event.toolName, event.input) if (!approved) { return { block: true, reason: '用户拒绝了本次工具调用审批' } diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 53bbb8a..0ee0647 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -1,18 +1,51 @@ import { contextBridge, ipcRenderer } from 'electron' -import type { AgentUiEvent, AppSettings, ModelCatalog, SessionMeta, SessionState } from '@shared/types' +import type { + AgentUiEvent, + AppSettings, + AttachmentInfo, + ChatResources, + ModelCatalog, + PermissionMode, + SendImage, + SessionMeta, + SessionState, + WorkspaceFile, + WorkspaceMeta +} from '@shared/types' const api = { - send: (text: string): Promise => ipcRenderer.invoke('agent:send', text), + // 窗口控制(自绘标题栏按钮) + minimize: (): Promise => ipcRenderer.invoke('win:minimize'), + toggleMaximize: (): Promise => ipcRenderer.invoke('win:toggleMaximize'), + close: (): Promise => ipcRenderer.invoke('win:close'), + onWindowState: (callback: (maximized: boolean) => void): (() => void) => { + const listener = (_e: Electron.IpcRendererEvent, maximized: boolean): void => callback(maximized) + ipcRenderer.on('win:state', listener) + return () => { + ipcRenderer.removeListener('win:state', listener) + } + }, + send: (text: string, images?: SendImage[]): Promise => ipcRenderer.invoke('agent:send', text, images), steer: (text: string): Promise => ipcRenderer.invoke('agent:steer', text), stop: (): Promise => ipcRenderer.invoke('agent:stop'), getState: (): Promise => ipcRenderer.invoke('agent:state'), - versions: (): Promise<{ app: string; electron: string; node: string; chrome: string }> => + versions: (): Promise<{ app: string; electron: string; node: string; chrome: string; user: string }> => ipcRenderer.invoke('app:versions'), listSessions: (): Promise => ipcRenderer.invoke('sessions:list'), newSession: (): Promise => ipcRenderer.invoke('sessions:new'), openSession: (path: string): Promise => ipcRenderer.invoke('sessions:open', path), renameSession: (name: string): Promise => ipcRenderer.invoke('sessions:rename', name), deleteSession: (path: string): Promise => ipcRenderer.invoke('sessions:delete', path), + listWorkspaces: (): Promise => ipcRenderer.invoke('workspaces:list'), + createWorkspace: (name: string): Promise => ipcRenderer.invoke('workspaces:create', name), + openWorkspace: (path: string): Promise => ipcRenderer.invoke('workspaces:open', path), + browseWorkspace: (): Promise => ipcRenderer.invoke('workspaces:browse'), + listChatResources: (): Promise => ipcRenderer.invoke('chat:resources'), + setQueue: (steering: string[], followUp: string[]): Promise => + ipcRenderer.invoke('queue:set', steering, followUp), + setPermissionMode: (mode: PermissionMode): Promise => ipcRenderer.invoke('permissions:set', mode), + listWorkspaceFiles: (): Promise => ipcRenderer.invoke('files:list'), + pickAttachments: (): Promise => ipcRenderer.invoke('attachments:pick'), getModelCatalog: (): Promise => ipcRenderer.invoke('models:catalog'), setModel: (providerId: string, modelId: string): Promise => ipcRenderer.invoke('models:set', providerId, modelId), diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index fed5409..ff0c802 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -5,11 +5,11 @@ - 店铺运营 Agent + Inkreach Agent - +
diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 4aa769b..c6eadc7 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,16 +1,19 @@ import { useEffect, useRef, useState } from 'react' import type { JSX } from 'react' import { useAgent } from './hooks/useAgent' -import { Sidebar } from './components/Sidebar' +import { SidebarNav } from './components/SidebarNav' +import { TitleBar } from './components/TitleBar' import { ChatView } from './components/ChatView' +import { BottomSlot } from './components/BottomSlot' import { Composer } from './components/Composer' import { SettingsModal } from './components/SettingsModal' -import { ApprovalStack } from './components/ApprovalCard' +import { Search } from './components/Search' export default function App(): JSX.Element { const { state, sessions, + workspaces, send, stop, clearError, @@ -18,23 +21,31 @@ export default function App(): JSX.Element { openSession, renameSession, deleteSession, + setModel, resolveApproval, - refreshSessions + setPermissionMode, + refreshSessions, + openWorkspace } = useAgent() const prefillRef = useRef(null) const [settingsOpen, setSettingsOpen] = useState(false) + const [searchOpen, setSearchOpen] = useState(false) - // 全局快捷键:Ctrl+N 新会话 / Ctrl+, 设置 / Esc 关闭弹窗 + // 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Esc 关闭弹窗 useEffect(() => { const onKey = (e: KeyboardEvent): void => { if (e.ctrlKey && e.key.toLowerCase() === 'n') { e.preventDefault() newSession() + } else if (e.ctrlKey && e.key.toLowerCase() === 'k') { + e.preventDefault() + setSearchOpen(true) } else if (e.ctrlKey && e.key === ',') { e.preventDefault() setSettingsOpen(true) } else if (e.key === 'Escape') { setSettingsOpen(false) + setSearchOpen(false) } } window.addEventListener('keydown', onKey) @@ -45,23 +56,38 @@ export default function App(): JSX.Element { prefillRef.current = text } - const queuedCount = state.queue.followUp.length + state.queue.steering.length + // 选区操作(解释/改进/精简/自定义指令):把指令 + 引用发给 Agent + const handleApplySelection = (instruction: string, selection: string): void => { + send(`${instruction}:\n\n> ${selection.split('\n').join('\n> ')}`) + } + + // 标题栏会话名:优先当前会话名,其次会话列表里的名称/首条消息 + const activeMeta = state.sessionState?.sessionId + ? sessions.find((s) => s.id === state.sessionState?.sessionId) + : undefined + const sessionTitle = + state.sessionState?.sessionName?.trim() || + activeMeta?.name?.trim() || + activeMeta?.firstMessage?.trim() || + '' return ( -
- + setSearchOpen(true)} onOpenSettings={() => setSettingsOpen(true)} - onRefreshSessions={refreshSessions} + workspaces={workspaces} + activeWorkspace={workspaces.find((w) => w.path === state.sessionState?.workspace)} + onOpenWorkspace={openWorkspace} />
+ - setSettingsOpen(true)} + onPermissionMode={setPermissionMode} + prefillRef={prefillRef} + /> + } /> -
+ setSearchOpen(false)} + sessions={sessions} + onNewSession={newSession} + onRefreshSessions={refreshSessions} + onOpenSettings={() => setSettingsOpen(true)} + onOpenSession={openSession} + /> ('up') + + useEffect(() => { + if (prevRef.current === value) return + const from = prevRef.current + prevRef.current = value + const fromN = parseInt(from, 10) + const toN = parseInt(value, 10) + setDir(Number.isFinite(fromN) && Number.isFinite(toN) && toN < fromN ? 'down' : 'up') + setOldVal(from) + setChars(value) + setRolling(true) + setShifted(false) + + let raf2 = 0 + const raf1 = requestAnimationFrame(() => { + raf2 = requestAnimationFrame(() => setShifted(true)) + }) + const done = setTimeout(() => { + setRolling(false) + setOldVal(value) + setShifted(false) + }, ROLL_MS) + + return () => { + cancelAnimationFrame(raf1) + cancelAnimationFrame(raf2) + clearTimeout(done) + } + }, [value]) + + return ( + <> + {Array.from({ length: chars.length }, (_, i) => { + const o = oldVal[i] ?? '' + const n = chars[i] ?? '' + if (!rolling || o === n) return {n} + const top = dir === 'down' ? n : o + const bottom = dir === 'down' ? o : n + const restY = dir === 'down' ? '0' : '-1em' + const startY = dir === 'down' ? '-1em' : '0' + return ( + + + {top} + {bottom} + + + ) + })} + + ) +} + +function Ico({ path, size = 14, sw = 2 }: { path: ReactNode; size?: number; sw?: number }): ReactNode { + return ( + + {path} + + ) +} + +const OPTION_APPROVE = 0 + +export function ApprovalCard({ request, onResolve }: { request: ApprovalRequest - onResolve: (approved: boolean) => void -}): JSX.Element { - const ref = useRef(null) - const [remaining, setRemaining] = useState(APPROVAL_TIMEOUT_S) - - useEffect(() => { - const start = Date.now() - const timer = setInterval(() => { - const left = Math.max(0, APPROVAL_TIMEOUT_S - Math.floor((Date.now() - start) / 1000)) - setRemaining(left) - if (left === 0) clearInterval(timer) - }, 1000) - return () => clearInterval(timer) - }, [request.id]) - - useEffect(() => { - const el = ref.current - if (!el) return - let tween: { kill(): void } | null = null - let cancelled = false - void import('gsap').then((gsapModule) => { - if (cancelled) return - tween = gsapModule.gsap.fromTo( - el, - { opacity: 0, y: 16, scale: 0.97 }, - { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: 'back.out(1.6)' } - ) - }) - return () => { - cancelled = true - tween?.kill() - } - }, []) - - const argsText = (() => { - try { - return JSON.stringify(request.args, null, 2) - } catch { - return String(request.args) - } - })() - - return ( -
-
-
- -
-
-
敏感操作,需要你的确认
-
{request.toolName}
-
-
- - {Math.floor(remaining / 60)}:{String(remaining % 60).padStart(2, '0')} -
-
- {argsText && argsText !== 'null' && ( -
-          {argsText.length > 600 ? `${argsText.slice(0, 600)}…` : argsText}
-        
- )} -
- - -
-
- ) -} - -export function ApprovalStack({ - requests, - onResolve -}: { - requests: ApprovalRequest[] onResolve: (id: string, approved: boolean) => void -}): JSX.Element | null { - if (requests.length === 0) return null +}): ReactNode { + const [picked, setPicked] = useState(null) + const [sent, setSent] = useState(null) + const submitTimer = useRef | null>(null) + const settled = useRef(false) + + useEffect( + () => () => { + if (submitTimer.current) clearTimeout(submitTimer.current) + }, + [] + ) + + const settle = (approved: boolean, note: string): void => { + if (settled.current) return + settled.current = true + setSent(note) + if (submitTimer.current) clearTimeout(submitTimer.current) + onResolve(request.id, approved) + } + + const pick = (index: number): void => { + if (sent) return + setPicked(index) + // radio 自动前进:480ms 后提交(官方 single-choice 行为) + if (submitTimer.current) clearTimeout(submitTimer.current) + submitTimer.current = setTimeout(() => { + settle(index === OPTION_APPROVE, index === OPTION_APPROVE ? '已允许执行' : '已拒绝') + }, 480) + } + + if (sent) { + const ok = sent === '已允许执行' + return ( +
+ + + {ok ? ( + + + + ) : ( + + + + )} + + {sent} + +
+ ) + } + return ( -
- {requests.map((request) => ( - onResolve(request.id, approved)} /> - ))} +
+
+ + +
+ {/* 问题本体即标题(官方形态:14px medium) */} +
+ 「{request.toolName}」请求执行敏感操作 +
+ + + {(['允许执行', '拒绝执行'] as const).map((option, i) => { + const on = picked === i + return ( + + ) + })} + +
+ + {/* footer — step nav (rolling counter) + pill actions(官方底栏 1:1) */} +
+
+ + + + + +
+ +
+ + +
+
+
) } diff --git a/apps/desktop/src/renderer/src/components/BottomSlot.tsx b/apps/desktop/src/renderer/src/components/BottomSlot.tsx new file mode 100644 index 0000000..bde63ce --- /dev/null +++ b/apps/desktop/src/renderer/src/components/BottomSlot.tsx @@ -0,0 +1,93 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react' +import type { ApprovalRequest } from '@shared/types' +import { ApprovalCard } from './ApprovalCard' + +/** + * 底部插槽(抽屉):审批卡出现时从 Chatbox 位置升起替换输入框, + * 结算后输入框滑回。两行用 grid-template-rows 0fr↔1fr 过渡做真实高度动画, + * 交叉淡入淡出;Composer 始终挂载(草稿不丢)。已结算的卡片以结算胶囊态 + * 多停留约一秒再撤出,避免「啪」一下消失。 + */ + +const ROWS_TRANSITION = 'grid-template-rows 400ms cubic-bezier(0.22, 1, 0.36, 1)' +const FADE_TRANSITION = 'opacity 220ms ease, transform 400ms cubic-bezier(0.22, 1, 0.36, 1)' + +/** 跟踪从 requests 里消失的审批:缓存原件让卡片以结算态多停留一拍 */ +function useLeavingApprovals(requests: ApprovalRequest[]): ApprovalRequest[] { + const seenRef = useRef(new Map()) + for (const request of requests) seenRef.current.set(request.id, request) + const [leaving, setLeaving] = useState([]) + + useEffect(() => { + const gone: ApprovalRequest[] = [] + for (const [id, request] of seenRef.current) { + if (!requests.some((r) => r.id === id)) gone.push(request) + } + if (gone.length === 0) return + for (const g of gone) seenRef.current.delete(g.id) + setLeaving((cur) => [...cur.filter((l) => !gone.some((g) => g.id === l.id)), ...gone]) + const timers = gone.map((g) => + setTimeout(() => { + setLeaving((cur) => cur.filter((l) => l.id !== g.id)) + }, 1000) + ) + return () => timers.forEach(clearTimeout) + }, [requests]) + + return leaving +} + +export function BottomSlot({ + approvals, + onResolve, + composer +}: { + approvals: ApprovalRequest[] + onResolve: (id: string, approved: boolean) => void + composer: ReactNode +}): ReactNode { + const leaving = useLeavingApprovals(approvals) + const showApproval = approvals.length > 0 || leaving.length > 0 + + return ( +
+ {/* Chatbox 行:审批升起时收起(高度 0 + 下沉淡出) */} +
+
+
+ {composer} +
+
+
+ + {/* 审批抽屉行:从输入框位置升起(高度 1fr + 上浮淡入) */} +
+
+
+
+ {[...approvals, ...leaving.filter((l) => !approvals.some((a) => a.id === l.id))].map((request) => ( +
+ +
+ ))} +
+
+
+
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/components/ChatView.tsx b/apps/desktop/src/renderer/src/components/ChatView.tsx index a00e179..897d9a4 100644 --- a/apps/desktop/src/renderer/src/components/ChatView.tsx +++ b/apps/desktop/src/renderer/src/components/ChatView.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef } from 'react' -import type { JSX } from 'react' -import { AlertTriangle, RotateCw, Store } from 'lucide-react' +import type { ReactNode } from 'react' +import { LuRotateCw, LuTriangleAlert } from 'react-icons/lu' import type { ApprovalRequest, ToolState, UiMessage } from '../hooks/useAgent' -import { MessageItem } from './MessageItem' +import { AssistantTurn, MessageItem } from './MessageItem' +import { SelectionActions } from './SelectionActions' const SUGGESTIONS = [ '帮我分析一下今天该关注哪些品类热点', @@ -10,24 +11,26 @@ const SUGGESTIONS = [ '查询最近一次上架任务的执行结果' ] -function EmptyState({ onPick }: { onPick: (text: string) => void }): JSX.Element { +/** 空状态:大标题 + 建议词(beautifului Streaming Text 的 follow-ups 行样式) */ +function EmptyState({ onPick }: { onPick: (text: string) => void }): ReactNode { return (
-
- -
-

店铺运营 Agent

-

- 流程固定、编排灵活。描述你的目标,Agent 会按 SOP 调用工具完成;也可以随时切为手动逐步执行。 +

今天想做点什么?

+

+ 描述你的目标,Agent 会按 SOP 调用工具完成;也可以随时切为手动逐步执行。

-
+
{SUGGESTIONS.map((s) => ( ))} @@ -36,6 +39,47 @@ function EmptyState({ onPick }: { onPick: (text: string) => void }): JSX.Element ) } +/** 连续 assistant 消息(pi 每个工具轮次一条)合并为一个回合渲染, + * 整轮思考 + 工具调用共享一张 beautifului ToolChips 卡。 */ +function renderTurns( + messages: UiMessage[], + tools: Record, + streaming: boolean, + pendingApprovals: ApprovalRequest[] +): ReactNode[] { + const items: ReactNode[] = [] + let i = 0 + while (i < messages.length) { + const message = messages[i] + if (message.role === 'assistant') { + let j = i + 1 + while (j < messages.length && messages[j].role === 'assistant') j += 1 + items.push( + + ) + i = j + } else { + items.push( + + ) + i += 1 + } + } + return items +} + export function ChatView({ messages, tools, @@ -45,7 +89,8 @@ export function ChatView({ sessionError, pendingApprovals, onDismissError, - onPickSuggestion + onPickSuggestion, + onApplySelection }: { messages: UiMessage[] tools: Record @@ -56,7 +101,8 @@ export function ChatView({ pendingApprovals: ApprovalRequest[] onDismissError: () => void onPickSuggestion: (text: string) => void -}): JSX.Element { + onApplySelection: (instruction: string, selection: string) => void +}): ReactNode { const scrollRef = useRef(null) const pinnedRef = useRef(true) @@ -80,13 +126,13 @@ export function ChatView({ return (
{(error || sessionError) && ( -
- +
+
{sessionError ? ( <>
Agent 会话初始化失败
-
{sessionError}
+
{sessionError}
) : (
{error}
@@ -95,7 +141,7 @@ export function ChatView({ @@ -103,27 +149,19 @@ export function ChatView({ )} {retry && ( -
- +
+ 请求失败,自动重试 {retry.attempt}/{retry.maxAttempts} - {retry.errorMessage} + {retry.errorMessage}
)}
{hasContent ? (
- {messages.map((message) => ( - - ))} + {renderTurns(messages, tools, streaming, pendingApprovals)}
) : (
@@ -131,6 +169,9 @@ export function ChatView({
)}
+ + {/* 划选消息文本后浮出的操作条 */} +
) } diff --git a/apps/desktop/src/renderer/src/components/CodeBlock.tsx b/apps/desktop/src/renderer/src/components/CodeBlock.tsx new file mode 100644 index 0000000..374ca23 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/CodeBlock.tsx @@ -0,0 +1,196 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react' + +/** 行号代码块(beautifului.dev Code Block)。 + * 头部:代码图标 + 文件名/语言 + Copy;主体:20px 行号列 + hairline 分隔 + + * 语法着色(关键字 accent-ink、字符串 orange、行内标识符加粗 ink)。 + * 内容是 unified diff(```diff 或 +/- 行占多数)时整块渲染为红绿对照。 */ + +const KEYWORDS = new Set( + `export async function const let var await if else return import from class new for while try catch finally throw typeof interface type enum public private protected static extends implements null true false undefined this super yield of in as void delete instanceof default switch case break continue do`.split( + /\s+/ + ) +) + +type Token = { text: string; kind: 'kw' | 'str' | 'com' | 'id' | 'plain' } + +/** 单行词法着色:注释 → 字符串 → 词元。可选括号分支会匹配空串, + * 必须跳过零长度匹配前进一位,否则 lastIndex 不动造成死循环。 */ +function tokenizeLine(line: string, lang: string): Token[] { + const commentRe = lang === 'py' || lang === 'python' || lang === 'sh' || lang === 'bash' || lang === 'yaml' ? /#.*$/ : null + const re = /("(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?|`(?:[^`\\]|\\.)*`?|(\/\/.*$)?|(\b[A-Za-z_$][\w$]*\b)|(\b\d[\w.]*\b))/g + const tokens: Token[] = [] + let last = 0 + let m: RegExpExecArray | null + const commentMatch = line.match(commentRe ?? /$^/) + const commentStart = commentMatch?.index ?? Infinity + while ((m = re.exec(line))) { + if (m[0] === '') { + re.lastIndex += 1 + continue + } + if (m.index >= commentStart) break + if (m.index > last) tokens.push({ text: line.slice(last, m.index), kind: 'plain' }) + const [raw, lineComment, word, num] = m + if (lineComment) tokens.push({ text: raw, kind: 'com' }) + else if (raw && (raw[0] === '"' || raw[0] === "'" || raw[0] === '`')) tokens.push({ text: raw, kind: 'str' }) + else if (word) { + const after = line.slice(re.lastIndex).match(/^\s*\(/) + tokens.push({ text: raw, kind: KEYWORDS.has(word) ? 'kw' : after ? 'id' : 'plain' }) + } else if (num) tokens.push({ text: raw, kind: 'plain' }) + else tokens.push({ text: raw, kind: 'plain' }) + last = re.lastIndex + } + if (last < commentStart && last < line.length) tokens.push({ text: line.slice(last), kind: 'plain' }) + if (commentStart !== Infinity) tokens.push({ text: line.slice(commentStart), kind: 'com' }) + return tokens +} + +const TOKEN_CLASS: Record = { + kw: 'text-accent-ink', + str: 'text-orange', + com: 'text-ink-3', + id: 'font-medium text-ink', + plain: '' +} + +function looksLikeDiff(code: string, lang: string): boolean { + if (lang === 'diff' || lang === 'patch') return true + const lines = code.split('\n').filter((l) => l.trim() && !l.startsWith('@@')) + if (!lines.length) return false + const marked = lines.filter((l) => /^[+-]/.test(l)).length + return marked / lines.length > 0.6 +} + +function isDiffLine(line: string): 'add' | 'del' | 'ctx' | 'hunk' { + if (line.startsWith('@@')) return 'hunk' + if (line.startsWith('+')) return 'add' + if (line.startsWith('-')) return 'del' + return 'ctx' +} + +/** Shiki 词元行(Markdown.tsx 异步算好传入);无 token 时回退内置简易分词 */ +export type ShikiToken = { content: string; color?: string; fontStyle?: number } +export type ShikiLine = ShikiToken[] + +export function CodeBlock({ + code, + lang = '', + filename, + tokens +}: { + code: string + lang?: string + filename?: string + tokens?: ShikiLine[] | null +}): ReactNode { + const source = useMemo(() => code.replace(/\n$/, ''), [code]) + const language = lang.toLowerCase() + const diff = looksLikeDiff(source, language) + const [copied, setCopied] = useState(false) + + useEffect(() => { + if (!copied) return + const t = setTimeout(() => setCopied(false), 1600) + return () => clearTimeout(t) + }, [copied]) + + const copy = (): void => { + void navigator.clipboard.writeText(source).then(() => setCopied(true)) + } + + const title = filename ?? ((language && language !== 'diff' ? language : '') || (diff ? 'diff' : 'code')) + + return ( +
+ {/* 头部:文件名 + Copy */} +
+ + + + + {title} + + +
+ + {/* 主体 */} + {diff ? ( + + ) : ( +
+
+ + {source.split('\n').map((line, i) => { + const lineTokens = tokens?.[i] + return ( +
+ {i + 1} + + {lineTokens + ? lineTokens.map((token, j) => ( + + {token.content} + + )) + : tokenizeLine(line, language).map((token, j) => ( + + {token.text} + + ))} + +
+ ) + })} +
+
+ )} +
+ ) +} + +/** unified diff 视图:红删绿增、@@ 保持上下文行号 */ +function DiffBody({ code }: { code: string }): ReactNode { + return ( +
+ {code.split('\n').map((line, i) => { + const kind = isDiffLine(line) + return ( +
+ {kind === 'add' ? '+' : kind === 'del' ? '−' : ' '} + {kind === 'hunk' ? line : line.slice(kind === 'ctx' ? 0 : 1)} +
+ ) + })} +
+ ) +} diff --git a/apps/desktop/src/renderer/src/components/Composer.tsx b/apps/desktop/src/renderer/src/components/Composer.tsx index e795dcb..d9b58c5 100644 --- a/apps/desktop/src/renderer/src/components/Composer.tsx +++ b/apps/desktop/src/renderer/src/components/Composer.tsx @@ -1,25 +1,321 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { createPortal } from 'react-dom' import type { JSX, RefObject } from 'react' -import { ArrowUp, CornerDownLeft, Square } from 'lucide-react' +import { LuCheck, LuClipboardList, LuFilePen, LuFileText, LuGripVertical, LuHand, LuPencil, LuShieldCheck, LuTrash2, LuX } from 'react-icons/lu' +import type { AttachmentInfo, ChatResources, ModelCatalog, ModelInfo, PermissionMode, WorkspaceFile } from '@shared/types' + +/** + * Prompt Bar(beautifului.dev #prompt-bar): + * 14px 圆角容器 + 五列网格(+/输入/模型/语音/发送),focus 边框加深。 + * "/" 命令面板:应用内动作 + pi 文件命令模板 + /skill: 技能(发送时由 SDK 展开); + * "@" 引用工作区文件;"+" 添加附件(图片为真实 ImageContent,其他文件以 @path 引用)。 + */ + +interface LocalCommand { + id: string + label: string + hint: string + run: () => void + enabled?: boolean +} + +/** 语音识别(Chromium 内核;不支持/失败时静默降级为禁用态) */ +function useDictation(onText: (text: string) => void): { + supported: boolean + listening: boolean + toggle: () => void +} { + const [listening, setListening] = useState(false) + const recRef = useRef<{ start: () => void; stop: () => void } | null>(null) + const onTextRef = useRef(onText) + onTextRef.current = onText + + const SR = ( + globalThis as unknown as { webkitSpeechRecognition?: new () => never; SpeechRecognition?: new () => never } + ).SpeechRecognition + + const supported = Boolean(SR) + + const toggle = (): void => { + if (!SR) return + if (listening) { + recRef.current?.stop() + return + } + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rec = new (SR as any)() + rec.lang = 'zh-CN' + rec.interimResults = false + rec.continuous = false + rec.onresult = (event: { results: ArrayLike> }): void => { + let text = '' + for (let i = 0; i < event.results.length; i += 1) text += event.results[i][0].transcript + if (text.trim()) onTextRef.current(text.trim()) + } + rec.onend = (): void => setListening(false) + rec.onerror = (): void => setListening(false) + recRef.current = rec + rec.start() + setListening(true) + } catch { + setListening(false) + } + } + + return { supported, listening, toggle } +} + + +/** 输入框锚定菜单:portal 到 body 固定定位(BottomSlot 的 overflow-hidden 会裁剪普通弹层) */ +function ComposerMenu({ + anchor, + align, + onClose, + children +}: { + anchor: { top: number; left: number; right: number } + align: 'left' | 'right' + onClose: () => void + children: ReactNode +}): ReactNode { + return createPortal( + <> +
+
+ {children} +
+ , + document.body + ) +} + +/** 权限模式菜单(参考 ZCode:变更前确认 / 自动编辑 / 计划模式 / 完全访问) */ +const PERMISSION_MODES: Array<{ id: PermissionMode; label: string; desc: string; icon: typeof LuHand }> = [ + { id: 'confirm', label: '变更前确认', desc: '改文件前先问我。', icon: LuHand }, + { id: 'autoEdit', label: '自动编辑', desc: '自动编辑文件。', icon: LuFilePen }, + { id: 'plan', label: '计划模式', desc: '编辑前先出计划。', icon: LuClipboardList }, + { id: 'full', label: '完全访问', desc: '减少确认次数。', icon: LuShieldCheck } +] export function Composer({ streaming, - queuedCount, + queue, disabled, + model, + permissionMode = 'confirm', onSend, onStop, + onNewSession, + onSetModel, + onOpenSettings, + onPermissionMode, prefillRef }: { streaming: boolean - queuedCount: number + /** 排队消息(steering 先于 followUp 送达) */ + queue: { steering: string[]; followUp: string[] } disabled: boolean - onSend: (text: string) => void + model?: ModelInfo + permissionMode?: PermissionMode + onSend: (text: string, images?: { mimeType: string; data: string }[]) => void onStop: () => void + onNewSession: () => void + onSetModel: (providerId: string, modelId: string) => void + onOpenSettings: () => void + onPermissionMode: (mode: PermissionMode) => void /** 供空状态建议词点击后填入 */ prefillRef: RefObject }): JSX.Element { const [value, setValue] = useState('') + const [modelOpen, setModelOpen] = useState(false) + const [modelPos, setModelPos] = useState<{ top: number; left: number; right: number } | null>(null) + const [permOpen, setPermOpen] = useState(false) + const [permPos, setPermPos] = useState<{ top: number; left: number; right: number } | null>(null) + const [catalog, setCatalog] = useState(null) + const [catalogLoading, setCatalogLoading] = useState(false) + const [attachments, setAttachments] = useState([]) + const [picking, setPicking] = useState(false) + const [resources, setResources] = useState(null) + const [files, setFiles] = useState(null) + const [activeIdx, setActiveIdx] = useState(0) const textareaRef = useRef(null) + const catalogLoadedRef = useRef(false) + + const localCommands = useMemo( + () => [ + { id: 'new', label: '/new', hint: '新会话', run: onNewSession }, + { id: 'settings', label: '/settings', hint: '打开设置', run: onOpenSettings }, + { id: 'stop', label: '/stop', hint: '停止生成', run: onStop, enabled: streaming }, + { id: 'clear', label: '/clear', hint: '清空输入', run: () => setValue('') } + ], + [onNewSession, onOpenSettings, onStop, streaming] + ) + + // "/" 命令面板:输入以 / 开头且当前词无空格时触发 + const trimmed = value.trimStart() + const slashExec = trimmed === value ? /^\/(\S*)$/.exec(trimmed) : null + const slashQuery = slashExec ? (slashExec[1] ?? '').toLowerCase() : null + + // "@ " 引用文件:末个 @ 词令牌触发 + const atMatch = /@([^\s@]*)$/.exec(value) + const atQuery = atMatch ? (atMatch[1] ?? '') : null + + // 资源懒加载:每次打开菜单都刷新(文件/skills 变化即时可见),in-flight 防抖 + const filesLoadingRef = useRef(false) + const resourcesLoadingRef = useRef(false) + const atOpen = atQuery !== null + const slashOpen = slashQuery !== null + + useEffect(() => { + if (!slashOpen || resourcesLoadingRef.current) return + resourcesLoadingRef.current = true + window.api + .listChatResources() + .then(setResources) + .catch(() => undefined) + .finally(() => { + resourcesLoadingRef.current = false + }) + }, [slashOpen]) + + useEffect(() => { + if (!atOpen || filesLoadingRef.current) return + filesLoadingRef.current = true + window.api + .listWorkspaceFiles() + .then(setFiles) + .catch(() => undefined) + .finally(() => { + filesLoadingRef.current = false + }) + }, [atOpen]) + + // "/" 面板条目:本地命令 + pi 命令模板 + /skill: 技能 + const commandItems = useMemo(() => { + if (slashQuery === null) return [] + const q = slashQuery.slice(1).toLowerCase() + const match = (s: string): boolean => s.toLowerCase().includes(q) || q === '' + const items: Array<{ key: string; insert: string; label: string; hint: string; group: string; run?: () => void; enabled?: boolean }> = [] + for (const cmd of localCommands) { + if (match(cmd.label.slice(1))) items.push({ key: `local-${cmd.id}`, insert: '', label: cmd.label, hint: cmd.hint, group: '命令', run: cmd.run, enabled: cmd.enabled }) + } + for (const cmd of resources?.commands ?? []) { + if (match(cmd.name)) items.push({ key: `prompt-${cmd.name}`, insert: `/${cmd.name} `, label: `/${cmd.name}`, hint: cmd.description || '命令模板', group: '命令' }) + } + for (const skill of resources?.skills ?? []) { + if (match(`skill:${skill.name}`) || match(skill.name)) { + items.push({ key: `skill-${skill.name}`, insert: `/skill:${skill.name} `, label: `/skill:${skill.name}`, hint: skill.description || '技能', group: '技能' }) + } + } + return items.slice(0, 12) + }, [slashQuery, localCommands, resources]) + + // "@ " 文件条目 + const fileItems = useMemo(() => { + if (atQuery === null || !files) return [] + const q = atQuery.toLowerCase() + return files + .filter((f) => f.path.toLowerCase().includes(q)) + .sort((a, b) => { + const an = a.name.toLowerCase().startsWith(q) ? 0 : 1 + const bn = b.name.toLowerCase().startsWith(q) ? 0 : 1 + return an - bn || a.path.length - b.path.length || a.path.localeCompare(b.path) + }) + .slice(0, 8) + }, [atQuery, files]) + + const menuOpen = !modelOpen && (commandItems.length > 0 || fileItems.length > 0) + const menuCount = commandItems.length + fileItems.length + + // ---- 排队消息(steering 先于 followUp 送达;「立即」= 置顶优先送达)---- + const [queueOverride, setQueueOverride] = useState<{ steering: string[]; followUp: string[] } | null>(null) + useEffect(() => { + if (!queueOverride) return + if ( + queue.steering.join('\u0000') === queueOverride.steering.join('\u0000') && + queue.followUp.join('\u0000') === queueOverride.followUp.join('\u0000') + ) { + setQueueOverride(null) + } + }, [queue, queueOverride]) + const effectiveQueue = queueOverride ?? queue + + const queueItems = useMemo( + () => [ + ...effectiveQueue.steering.map((text) => ({ text, from: 'steering' as const })), + ...effectiveQueue.followUp.map((text) => ({ text, from: 'followUp' as const })) + ], + [effectiveQueue] + ) + + /** 应用本地新顺序并下发;600ms 内忽略回推的 queue_update,避免行闪烁 */ + const commitQueue = (items: Array<{ text: string; from: 'steering' | 'followUp' }>): void => { + const steering = items.filter((i) => i.from === 'steering').map((i) => i.text) + const followUp = items.filter((i) => i.from === 'followUp').map((i) => i.text) + setQueueOverride({ steering, followUp }) + window.setTimeout(() => setQueueOverride(null), 600) + void window.api.setQueue(steering, followUp).catch(() => undefined) + } + + const removeQueued = (index: number): void => { + const items = [...queueItems] + items.splice(index, 1) + commitQueue(items) + } + + const promoteQueued = (index: number): void => { + if (index === 0) return + const items = [...queueItems] + const [item] = items.splice(index, 1) + items.unshift(item) + commitQueue(items) + } + + const reorderQueued = (from: number, to: number): void => { + if (from === to) return + const items = [...queueItems] + const [moved] = items.splice(from, 1) + items.splice(to, 0, moved) + commitQueue(items) + } + + const saveQueuedEdit = (index: number, text: string): void => { + const trimmedText = text.trim() + if (!trimmedText) return + const items = [...queueItems] + items[index] = { ...items[index], text: trimmedText } + commitQueue(items) + } + + const [editingIdx, setEditingIdx] = useState(null) + const [editValue, setEditValue] = useState('') + const dragFromRef = useRef(null) + const [dragOverIdx, setDragOverIdx] = useState(null) + + // 命令/文件菜单:覆盖层(不占布局,避免聊天区上下跳动) + const barRef = useRef(null) + const [menuPos, setMenuPos] = useState<{ top: number; left: number; width: number } | null>(null) + useEffect(() => { + if (menuOpen) { + const rect = barRef.current?.getBoundingClientRect() + if (rect) setMenuPos({ top: rect.top, left: rect.left, width: rect.width }) + } else { + setMenuPos(null) + } + }, [menuOpen]) + const closeMenu = (): void => { + if (atMatch) setValue(value.replace(/@[^\s@]*$/, '')) + else if (slashQuery !== null) setValue('') + } + + useEffect(() => setActiveIdx(0), [value]) // 空状态建议词点击 → 填入输入框并聚焦 useEffect(() => { @@ -38,73 +334,550 @@ export function Composer({ el.style.height = `${Math.min(el.scrollHeight, 200)}px` }, [value]) + const modelBtnRef = useRef(null) + const permBtnRef = useRef(null) + + const anchorOf = (el: HTMLElement | null): { top: number; left: number; right: number } | null => { + if (!el) return null + const rect = el.getBoundingClientRect() + return { top: rect.top, left: rect.left, right: window.innerWidth - rect.right } + } + + const openModelMenu = (): void => { + if (modelOpen) { + setModelOpen(false) + return + } + setModelPos(anchorOf(modelBtnRef.current)) + setModelOpen((open) => { + if (!open && !catalogLoadedRef.current) { + catalogLoadedRef.current = true + setCatalogLoading(true) + window.api + .getModelCatalog() + .then(setCatalog) + .catch(() => undefined) + .finally(() => setCatalogLoading(false)) + } + return true + }) + } + + const togglePermMenu = (): void => { + if (permOpen) { + setPermOpen(false) + return + } + setPermPos(anchorOf(permBtnRef.current)) + setPermOpen(true) + } + + const pickMenuItem = (index: number): void => { + if (index < commandItems.length) { + const item = commandItems[index] + setValue('') + textareaRef.current?.focus() + if (item.insert) { + setValue(item.insert) + textareaRef.current?.focus() + return + } + if (item.enabled !== false) item.run?.() + return + } + const file = fileItems[index - commandItems.length] + if (!file) return + setValue((current) => current.replace(/@[^\s@]*$/, `@${file.path} `)) + textareaRef.current?.focus() + } + + const addAttachments = (): void => { + if (picking) return + setPicking(true) + window.api + .pickAttachments() + .then((picked) => { + setAttachments((cur) => { + const seen = new Set(cur.map((a) => a.path)) + return [...cur, ...picked.filter((p) => !seen.has(p.path))] + }) + }) + .catch(() => undefined) + .finally(() => setPicking(false)) + } + const submit = (): void => { + if (disabled) return const text = value.trim() - if (!text || disabled) return - onSend(text) + const images = attachments.filter((a) => a.data && a.mimeType.startsWith('image/')) + const fileRefs = attachments.filter((a) => !(a.data && a.mimeType.startsWith('image/'))) + if (!text && images.length === 0) return + const refLines = fileRefs.map((f) => `@${f.path}`) + const full = refLines.length > 0 ? `${text}${text ? '\n\n' : ''}${refLines.join('\n')}` : text + onSend(full, images.map((img) => ({ mimeType: img.mimeType, data: img.data }))) setValue('') + setAttachments([]) + } + + const onKeyDown = (e: React.KeyboardEvent): void => { + if (e.nativeEvent.isComposing) return + if (menuOpen && menuCount > 0) { + if (e.key === 'ArrowDown') { + e.preventDefault() + setActiveIdx((i) => (i + 1) % menuCount) + return + } + if (e.key === 'ArrowUp') { + e.preventDefault() + setActiveIdx((i) => (i - 1 + menuCount) % menuCount) + return + } + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault() + pickMenuItem(activeIdx) + return + } + if (e.key === 'Escape') { + e.preventDefault() + setValue(atMatch ? value.replace(/@[^\s@]*$/, '') : '') + return + } + } + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + submit() + } + } + + const dictation = useDictation((text) => setValue((current) => (current ? `${current} ${text}` : text))) + + const permMeta = PERMISSION_MODES.find((m) => m.id === permissionMode) ?? PERMISSION_MODES[0] + + // 快捷切换只列已配置凭据的服务商;全量目录(含未配置)去「管理模型与密钥」里看 + const providers = catalog?.providers.filter((p) => (p.hasApiKey || p.isOAuth) && p.models.length > 0) ?? [] + const canSend = !disabled && (value.trim().length > 0 || attachments.some((a) => a.data && a.mimeType.startsWith('image/'))) + + // 菜单分段偏移(键盘高亮跨段连续) + const commandEnd = commandItems.length + + const renderMenuItem = (item: (typeof commandItems)[number], index: number): ReactNode => { + return ( + + ) } return (
-
-