From 6f143e5ecd5b6936f11a31c5c3e918e619f59150 Mon Sep 17 00:00:00 2001 From: Augustin Date: Mon, 20 Apr 2026 22:45:00 +0200 Subject: [PATCH] feat: add desktop app with React frontend, API backend, theme system New desktop application that launches a local HTTP server with embedded React frontend. Opens in the user's browser automatically. Architecture: - internal/api/: REST API exposing all internal/ packages to frontend - cmd/muyue-desktop/: entry point, serves embedded frontend + API - cmd/muyue-desktop/frontend/: React + Vite SPA Frontend features: - 4 tabs: Dashboard, Studio, Shell, Config - Cyberpunk red theme with CSS custom properties - Theme system: 4 built-in themes (Cyberpunk Red, Pink, Midnight Blue, Matrix Green) - Terminal with command execution via API - Chat interface with sidebar (agents, workflows, commands) - Live clock, status indicators, update badges - Glitch/scanline/fade animations between tabs - xterm.js included for future full terminal integration Backend API endpoints: - GET /api/info, /api/system, /api/tools, /api/config - GET /api/providers, /api/skills, /api/lsp, /api/mcp, /api/updates - POST /api/scan, /api/install, /api/terminal, /api/mcp/configure Build: cd cmd/muyue-desktop/frontend && npm run build && go build ./cmd/muyue-desktop/ Binary: ~11MB single binary with embedded frontend Generated with Crush Assisted-by: GLM-5.1 via Crush --- .gitignore | 1 + cmd/muyue-desktop/frontend/.gitignore | 3 + cmd/muyue-desktop/frontend/index.html | 13 + cmd/muyue-desktop/frontend/package-lock.json | 993 ++++++++++++++++++ cmd/muyue-desktop/frontend/package.json | 26 + .../frontend/src/components/App.jsx | 105 ++ .../frontend/src/components/Config.jsx | 98 ++ .../frontend/src/components/Dashboard.jsx | 121 +++ .../frontend/src/components/Shell.jsx | 150 +++ .../frontend/src/components/Studio.jsx | 126 +++ .../frontend/src/hooks/useAPI.js | 31 + cmd/muyue-desktop/frontend/src/main.jsx | 9 + .../frontend/src/styles/global.css | 576 ++++++++++ .../frontend/src/themes/index.js | 138 +++ cmd/muyue-desktop/frontend/vite.config.js | 15 + cmd/muyue-desktop/main.go | 116 ++ internal/api/api.go | 266 +++++ 17 files changed, 2787 insertions(+) create mode 100644 cmd/muyue-desktop/frontend/.gitignore create mode 100644 cmd/muyue-desktop/frontend/index.html create mode 100644 cmd/muyue-desktop/frontend/package-lock.json create mode 100644 cmd/muyue-desktop/frontend/package.json create mode 100644 cmd/muyue-desktop/frontend/src/components/App.jsx create mode 100644 cmd/muyue-desktop/frontend/src/components/Config.jsx create mode 100644 cmd/muyue-desktop/frontend/src/components/Dashboard.jsx create mode 100644 cmd/muyue-desktop/frontend/src/components/Shell.jsx create mode 100644 cmd/muyue-desktop/frontend/src/components/Studio.jsx create mode 100644 cmd/muyue-desktop/frontend/src/hooks/useAPI.js create mode 100644 cmd/muyue-desktop/frontend/src/main.jsx create mode 100644 cmd/muyue-desktop/frontend/src/styles/global.css create mode 100644 cmd/muyue-desktop/frontend/src/themes/index.js create mode 100644 cmd/muyue-desktop/frontend/vite.config.js create mode 100644 cmd/muyue-desktop/main.go create mode 100644 internal/api/api.go diff --git a/.gitignore b/.gitignore index 0aa437d..60ffe05 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ vendor/ # Config with secrets .muyue/ +frontend/node_modules/ diff --git a/cmd/muyue-desktop/frontend/.gitignore b/cmd/muyue-desktop/frontend/.gitignore new file mode 100644 index 0000000..3ff38cc --- /dev/null +++ b/cmd/muyue-desktop/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.vite/ diff --git a/cmd/muyue-desktop/frontend/index.html b/cmd/muyue-desktop/frontend/index.html new file mode 100644 index 0000000..a076171 --- /dev/null +++ b/cmd/muyue-desktop/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + muyue + + + +
+ + + diff --git a/cmd/muyue-desktop/frontend/package-lock.json b/cmd/muyue-desktop/frontend/package-lock.json new file mode 100644 index 0000000..de8e8ab --- /dev/null +++ b/cmd/muyue-desktop/frontend/package-lock.json @@ -0,0 +1,993 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.9" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.126.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", + "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", + "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", + "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "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", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", + "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.126.0", + "@rolldown/pluginutils": "1.0.0-rc.16" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-x64": "1.0.0-rc.16", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", + "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", + "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.16", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "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.1.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/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + } + } +} diff --git a/cmd/muyue-desktop/frontend/package.json b/cmd/muyue-desktop/frontend/package.json new file mode 100644 index 0000000..c41363c --- /dev/null +++ b/cmd/muyue-desktop/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "frontend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.9" + } +} diff --git a/cmd/muyue-desktop/frontend/src/components/App.jsx b/cmd/muyue-desktop/frontend/src/components/App.jsx new file mode 100644 index 0000000..3e1a633 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/components/App.jsx @@ -0,0 +1,105 @@ +import { useState, useEffect, useCallback } from 'react' +import { useAPI } from '../hooks/useAPI' +import { getTheme, getThemeNames, applyTheme } from '../themes' +import Dashboard from './Dashboard' +import Studio from './Studio' +import Shell from './Shell' +import Config from './Config' + +const TABS = [ + { id: 'dash', label: 'DASH', icon: '[■]' }, + { id: 'studio', label: 'STUDIO', icon: '[<>]' }, + { id: 'shell', label: 'SHELL', icon: '[$]' }, + { id: 'config', label: 'CONFIG', icon: '[//]' }, +] + +export default function App() { + const [activeTab, setActiveTab] = useState('dash') + const [info, setInfo] = useState({}) + const [clock, setClock] = useState(new Date()) + const [updates, setUpdates] = useState([]) + const [tools, setTools] = useState([]) + const [transition, setTransition] = useState(false) + const [currentTheme, setCurrentTheme] = useState('cyberpunk-red') + const api = useAPI() + + useEffect(() => { + api.getInfo().then(setInfo).catch(() => {}) + api.getTools().then(d => setTools(d.tools || [])).catch(() => {}) + api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {}) + + const theme = getTheme(currentTheme) + applyTheme(theme) + }, []) + + useEffect(() => { + const timer = setInterval(() => setClock(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + const switchTab = useCallback((tabId) => { + if (tabId === activeTab) return + setTransition(true) + setTimeout(() => { + setActiveTab(tabId) + setTimeout(() => setTransition(false), 150) + }, 100) + }, [activeTab]) + + const hasUpdates = updates.some(u => u.needsUpdate) + + const renderContent = () => { + switch (activeTab) { + case 'dash': return setTools(t)} /> + case 'studio': return + case 'shell': return + case 'config': return + default: return null + } + } + + return ( +
+
+ MUYUE + v{info.version || '...'} + +
+ {TABS.map(tab => ( +
switchTab(tab.id)} + > + {tab.icon} {tab.label} +
+ ))} +
+ +
+ +
+ 0 ? 'ok' : 'off'}`} title="System" /> + +
+ + {clock.toLocaleDateString('fr-FR')} + {clock.toLocaleTimeString('fr-FR')} +
+ +
+ {renderContent()} +
+ +
+ + 1-4 tabs · Ctrl+T switcher · Ctrl+C quit + + + {hasUpdates ? '[UPD] Updates available' : '[OK] Up to date'} + + v{info.version || '...'} +
+
+ ) +} diff --git a/cmd/muyue-desktop/frontend/src/components/Config.jsx b/cmd/muyue-desktop/frontend/src/components/Config.jsx new file mode 100644 index 0000000..61fbd72 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/components/Config.jsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from 'react' +import { getThemeNames, applyTheme, getTheme } from '../themes' + +export default function Config({ api, theme, onThemeChange }) { + const [config, setConfig] = useState(null) + const [providers, setProviders] = useState([]) + const [skillList, setSkillList] = useState([]) + + useEffect(() => { + api.getConfig().then(d => setConfig(d)).catch(() => {}) + api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {}) + api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {}) + }, []) + + const themes = getThemeNames() + + const handleThemeChange = (themeId) => { + const t = getTheme(themeId) + applyTheme(t) + onThemeChange(themeId) + } + + return ( +
+
+
Profile
+ {config?.profile && ( +
+ + + + + + + +
+ )} +
+ +
+
AI Providers
+ {providers.map((p, i) => ( +
+
+ {p.name} + {p.active && {'>>'}} +
+
+ model={p.model} + + key={p.apiKey ? 'configured' : 'no key'} + +
+
+ ))} +
+ +
+
Theme
+
+ {themes.map(t => ( + + ))} +
+
+ +
+
Skills ({skillList.length})
+ {skillList.length === 0 ? ( + No skills. Run `muyue skills init`. + ) : ( + skillList.map((s, i) => ( +
+ {s.name} + [{s.target || 'both'}] + {s.description} +
+ )) + )} +
+
+ ) +} + +function Field({ label, value }) { + return ( +
+ {label}: + {value || '-'} +
+ ) +} diff --git a/cmd/muyue-desktop/frontend/src/components/Dashboard.jsx b/cmd/muyue-desktop/frontend/src/components/Dashboard.jsx new file mode 100644 index 0000000..248852e --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/components/Dashboard.jsx @@ -0,0 +1,121 @@ +import { useState, useEffect } from 'react' + +export default function Dashboard({ tools, updates, api, onRescan }) { + const [installing, setInstalling] = useState(false) + const [installLog, setInstallLog] = useState([]) + + const installed = tools.filter(t => t.installed).length + const total = tools.length + const pct = total > 0 ? (installed / total) * 100 : 0 + const missing = tools.filter(t => !t.installed).map(t => t.Name || t.name) + + const handleInstall = async () => { + if (missing.length === 0) return + setInstalling(true) + setInstallLog(prev => [...prev, { text: `Installing ${missing.length} tools...`, type: 'info' }]) + try { + await api.installTools(missing) + setInstallLog(prev => [...prev, { text: 'Install started. Rescan to see changes.', type: 'ok' }]) + const data = await api.runScan() + const toolData = await api.getTools() + onRescan(toolData.tools || []) + } catch (err) { + setInstallLog(prev => [...prev, { text: err.message, type: 'error' }]) + } + setInstalling(false) + } + + const handleScan = async () => { + await api.runScan() + const data = await api.getTools() + onRescan(data.tools || []) + } + + return ( +
+
+
System
+
+ {installed}/{total} tools installed +
+ +
Installed Tools
+
+ {tools.map((t, i) => ( +
+ + {t.installed ? '[OK]' : '[--]'} + + {t.Name || t.name} + {(t.Version || t.version) && ( + {extractVersion(t.Version || t.version)} + )} +
+ ))} +
+ +
+
+
+ + {installing && ( +
+ Installing... +
+ )} + + {installLog.length > 0 && ( +
+
Install Log
+ {installLog.map((log, i) => ( +
+ {log.text} +
+ ))} +
+ )} +
+ +
+
Quick Actions
+
+ + + + +
+ +
Updates
+
+ {updates.length === 0 ? ( + No update data yet + ) : updates.map((u, i) => ( +
+ + {u.needsUpdate ? '[!!]' : '[OK]'} + + {u.tool} + {u.needsUpdate && ( + + {u.current} → {u.latest} + + )} +
+ ))} +
+
+
+ ) +} + +function extractVersion(s) { + if (!s) return '' + const m = s.match(/\d+\.\d+\.\d+/) + return m ? m[0] : s.slice(0, 12) +} diff --git a/cmd/muyue-desktop/frontend/src/components/Shell.jsx b/cmd/muyue-desktop/frontend/src/components/Shell.jsx new file mode 100644 index 0000000..3dc3885 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/components/Shell.jsx @@ -0,0 +1,150 @@ +import { useState, useRef, useEffect } from 'react' + +export default function Shell({ api }) { + const [history, setHistory] = useState([]) + const [input, setInput] = useState('') + const [cwd, setCwd] = useState('~') + const [aiPanel, setAiPanel] = useState(true) + const [aiMessages, setAiMessages] = useState([ + { role: 'ai', content: '>> I know your system inside out. Ask me anything.' } + ]) + const [aiInput, setAiInput] = useState('') + const [aiLoading, setAiLoading] = useState(false) + const outputRef = useRef(null) + + useEffect(() => { + outputRef.current?.scrollTo(0, outputRef.current.scrollHeight) + }, [history]) + + const handleCommand = async (cmd) => { + if (!cmd.trim()) return + if (cmd === 'clear') { + setHistory([]) + return + } + if (cmd === 'exit' || cmd === 'quit') return + + setHistory(prev => [...prev, { type: 'input', text: `${cwd} $ ${cmd}` }]) + + try { + const res = await api.runCommand(cmd, cwd === '~' ? '' : cwd) + if (res.output) { + setHistory(prev => [...prev, { type: 'output', text: res.output }]) + } + if (res.error) { + setHistory(prev => [...prev, { type: 'error', text: res.error }]) + } + if (cmd.startsWith('cd ')) { + const dir = cmd.slice(3).trim() + setCwd(dir === '~' ? '~' : dir) + } + } catch (err) { + setHistory(prev => [...prev, { type: 'error', text: err.message }]) + } + } + + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleCommand(input) + setInput('') + } + } + + const handleAiSend = async () => { + if (!aiInput.trim() || aiLoading) return + const text = aiInput.trim() + setAiMessages(prev => [...prev, { role: 'user', content: '>> ' + text }]) + setAiInput('') + setAiLoading(true) + + try { + const res = await api.runCommand(`echo "AI: ${text}"`, '') + setAiMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || 'No response') }]) + } catch (err) { + setAiMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }]) + } + setAiLoading(false) + } + + return ( +
+
+
+
+ Terminal + {cwd} +
+
+ +
+ {history.map((line, i) => ( +
+ {line.text} +
+ ))} +
+ +
+ {'>'} + setInput(e.target.value)} + onKeyDown={handleKeyDown} + autoFocus + /> +
+
+ + {aiPanel && ( +
+
+
AI Assistant
+
+ +
+ {aiMessages.map((msg, i) => ( +
+ {msg.content} +
+ ))} + {aiLoading && thinking...} +
+ +
+ setAiInput(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleAiSend()} + placeholder="Ask AI..." + /> + +
+
+ )} +
+ ) +} diff --git a/cmd/muyue-desktop/frontend/src/components/Studio.jsx b/cmd/muyue-desktop/frontend/src/components/Studio.jsx new file mode 100644 index 0000000..f2565a0 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/components/Studio.jsx @@ -0,0 +1,126 @@ +import { useState, useRef, useEffect } from 'react' + +export default function Studio({ api }) { + const [messages, setMessages] = useState([ + { role: 'ai', content: '>> Welcome to Studio! Chat with your AI assistant here.' }, + { role: 'ai', content: '>> Configure agents and workflows from the sidebar.' }, + ]) + const [input, setInput] = useState('') + const [sidebarPanel, setSidebarPanel] = useState('chat') + const [loading, setLoading] = useState(false) + const messagesEnd = useRef(null) + + useEffect(() => { + messagesEnd.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + const handleSend = () => { + if (!input.trim() || loading) return + const text = input.trim() + setMessages(prev => [...prev, { role: 'user', content: '>> ' + text }]) + setInput('') + setLoading(true) + + api.runCommand(`echo "AI response simulation for: ${text}"`, '') + .then(res => { + setMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || res.error || 'No response') }]) + }) + .catch(err => { + setMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }]) + }) + .finally(() => setLoading(false)) + } + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( +
+
+
+
+ Chat + {loading && thinking...} +
+
+ +
+ {messages.map((msg, i) => ( +
+ {msg.content} +
+ ))} +
+
+ +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type a message... (/plan for workflows)" + disabled={loading} + /> + +
+
+ +
+
+
Studio
+ {['chat', 'agents', 'workflows'].map(panel => ( +
setSidebarPanel(panel)} + > + [{panel === 'chat' ? '#' : panel === 'agents' ? '*' : '~'}] {panel.charAt(0).toUpperCase() + panel.slice(1)} +
+ ))} +
+ +
+ {sidebarPanel === 'chat' && ( +
+
Commands
+
+ /plan {''}
+ /help +
+
+ )} + + {sidebarPanel === 'agents' && ( +
+
Active Agents
+
+ Crush + [|| stopped] +
+
+ Claude Code + [|| stopped] +
+
+ )} + + {sidebarPanel === 'workflows' && ( +
+
No active workflow.
+
+ Use /plan {''} in chat to start. +
+
+ )} +
+
+
+ ) +} diff --git a/cmd/muyue-desktop/frontend/src/hooks/useAPI.js b/cmd/muyue-desktop/frontend/src/hooks/useAPI.js new file mode 100644 index 0000000..d8fc8af --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/hooks/useAPI.js @@ -0,0 +1,31 @@ +const API_BASE = '/api' + +async function request(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || res.statusText) + } + return res.json() +} + +export function useAPI() { + return { + getInfo: () => request('/info'), + getSystem: () => request('/system'), + getTools: () => request('/tools'), + getConfig: () => request('/config'), + getProviders: () => request('/providers'), + getSkills: () => request('/skills'), + getLSP: () => request('/lsp'), + getMCP: () => request('/mcp'), + getUpdates: () => request('/updates'), + runScan: () => request('/scan', { method: 'POST' }), + installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }), + configureMCP: () => request('/mcp/configure', { method: 'POST' }), + runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }), + } +} diff --git a/cmd/muyue-desktop/frontend/src/main.jsx b/cmd/muyue-desktop/frontend/src/main.jsx new file mode 100644 index 0000000..8f15949 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './components/App' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) diff --git a/cmd/muyue-desktop/frontend/src/styles/global.css b/cmd/muyue-desktop/frontend/src/styles/global.css new file mode 100644 index 0000000..4bb3d27 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/styles/global.css @@ -0,0 +1,576 @@ +:root { + --bg-void: #0A0A0C; + --bg-base: #0F0D10; + --bg-surface: #161218; + --bg-panel: #1C1719; + --bg-card: #221B1E; + --bg-input: #2A2225; + --bg-hover: #332528; + + --cyber-red: #FF0033; + --cyber-red-dark: #8B0020; + --cyber-red-deep: #5C0015; + --cyber-pink: #FF1A5E; + --cyber-rose: #FF4D6D; + --neon-red: #FF1744; + --bright-red: #FF5252; + --dim-red: #6B2033; + --muted-red: #4A1525; + + --text-bright: #EAE0E2; + --text-main: #D4C4C8; + --text-dim: #8A7A7E; + --text-muted: #5A4F52; + + --success: #00E676; + --warning: #FFD740; + --error: #FF1744; + + --border-dim: #2A1F22; + --border-red: #FF003344; + --border-red-full: #FF0033; + + --radius: 8px; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace; + --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + + --header-h: 48px; + --footer-h: 32px; + --tab-h: 40px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + height: 100%; + width: 100%; + overflow: hidden; +} + +body { + background: var(--bg-void); + color: var(--text-main); + font-family: var(--font-ui); + font-size: 13px; + -webkit-font-smoothing: antialiased; + user-select: none; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--dim-red); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--cyber-red-dark); +} + +::selection { + background: var(--cyber-red); + color: #fff; +} + +a { + color: var(--cyber-red); + text-decoration: none; +} + +a:hover { + color: var(--cyber-pink); +} + +input, textarea { + font-family: var(--font-mono); + font-size: 13px; + background: var(--bg-input); + color: var(--text-main); + border: 1px solid var(--border-dim); + border-radius: var(--radius); + padding: 8px 12px; + outline: none; + transition: border-color 0.2s; +} + +input:focus, textarea:focus { + border-color: var(--cyber-red); + box-shadow: 0 0 0 2px var(--border-red); +} + +button { + font-family: var(--font-ui); + font-size: 12px; + font-weight: 600; + padding: 6px 14px; + border-radius: var(--radius); + border: 1px solid var(--border-dim); + background: var(--bg-card); + color: var(--text-main); + cursor: pointer; + transition: all 0.15s; +} + +button:hover { + background: var(--bg-hover); + border-color: var(--cyber-red-dark); +} + +button.primary { + background: var(--cyber-red); + color: #fff; + border-color: var(--cyber-red); +} + +button.primary:hover { + background: var(--neon-red); +} + +.app-layout { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +.header { + height: var(--header-h); + background: var(--bg-surface); + border-bottom: 1px solid var(--border-dim); + display: flex; + align-items: center; + padding: 0 16px; + gap: 16px; + flex-shrink: 0; +} + +.header-logo { + font-family: var(--font-mono); + font-weight: 900; + font-size: 16px; + color: var(--cyber-red); + letter-spacing: 2px; +} + +.header-version { + font-size: 11px; + color: var(--dim-red); +} + +.header-tabs { + display: flex; + gap: 2px; + margin-left: 24px; +} + +.header-tab { + padding: 6px 16px; + border-radius: var(--radius) var(--radius) 0 0; + font-size: 12px; + font-weight: 600; + letter-spacing: 1px; + color: var(--text-dim); + cursor: pointer; + transition: all 0.15s; + border: 1px solid transparent; + border-bottom: none; + text-transform: uppercase; + background: transparent; +} + +.header-tab:hover { + color: var(--text-main); + background: var(--bg-card); +} + +.header-tab.active { + color: #fff; + background: var(--cyber-red); + border-color: var(--cyber-red); +} + +.header-spacer { + flex: 1; +} + +.header-status { + display: flex; + gap: 8px; + align-items: center; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.ok { background: var(--success); } +.status-dot.warn { background: var(--warning); } +.status-dot.error { background: var(--error); } +.status-dot.off { background: var(--text-muted); } + +.header-clock { + font-family: var(--font-mono); + font-size: 12px; + color: var(--cyber-red); + font-weight: 700; +} + +.header-date { + font-size: 11px; + color: var(--text-muted); +} + +.content { + flex: 1; + overflow: hidden; +} + +.footer { + height: var(--footer-h); + background: var(--bg-surface); + border-top: 1px solid var(--border-dim); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + flex-shrink: 0; +} + +.footer-shortcuts { + font-size: 11px; + color: var(--text-muted); +} + +.footer-shortcuts kbd { + background: var(--bg-card); + border: 1px solid var(--border-dim); + border-radius: 3px; + padding: 1px 5px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-dim); +} + +.footer-version { + font-size: 11px; + color: var(--dim-red); +} + +.footer-update { + font-size: 11px; + font-weight: 600; +} + +.footer-update.available { + color: var(--warning); +} + +.footer-update.uptodate { + color: var(--success); +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border-dim); + border-radius: var(--radius); + padding: 16px; + transition: border-color 0.2s; +} + +.card:hover { + border-color: var(--dim-red); +} + +.card-title { + font-family: var(--font-mono); + font-weight: 700; + font-size: 13px; + color: var(--cyber-red); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.section-header { + font-family: var(--font-mono); + font-weight: 700; + font-size: 13px; + color: var(--cyber-red); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.section-header::before { + content: '■'; + font-size: 10px; +} + +.tool-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + font-size: 13px; +} + +.tool-status { + font-family: var(--font-mono); + font-weight: 700; + font-size: 11px; + width: 36px; +} + +.tool-status.ok { color: var(--success); } +.tool-status.missing { color: var(--error); } + +.tool-name { + color: var(--text-main); +} + +.tool-version { + color: var(--dim-red); + font-size: 11px; +} + +.progress-bar { + height: 6px; + background: var(--bg-input); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--cyber-red), var(--cyber-pink)); + border-radius: 3px; + transition: width 0.3s; +} + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + height: 100%; + padding: 16px; +} + +.split-horizontal { + display: flex; + height: 100%; +} + +.split-left { + flex: 1; + overflow: auto; +} + +.split-right { + width: 320px; + border-left: 1px solid var(--border-dim); + background: var(--bg-surface); + overflow: auto; + padding: 16px; +} + +.chat-container { + display: flex; + flex-direction: column; + height: 100%; + padding: 16px; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding-bottom: 8px; +} + +.chat-message { + margin-bottom: 12px; + padding: 8px 12px; + border-radius: var(--radius); + font-size: 13px; + line-height: 1.5; +} + +.chat-message.ai { + background: var(--bg-card); + border-left: 3px solid var(--cyber-red); +} + +.chat-message.user { + background: var(--muted-red); + border-left: 3px solid var(--cyber-rose); + margin-left: 24px; +} + +.chat-input-container { + display: flex; + gap: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-dim); +} + +.chat-input { + flex: 1; + font-family: var(--font-mono); +} + +.sidebar-section { + margin-bottom: 16px; +} + +.sidebar-item { + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + color: var(--text-dim); + transition: all 0.15s; +} + +.sidebar-item:hover { + background: var(--bg-card); + color: var(--text-main); +} + +.sidebar-item.active { + background: var(--cyber-red); + color: #fff; + font-weight: 600; +} + +.terminal-container { + height: 100%; + display: flex; + flex-direction: column; + padding: 0; +} + +.terminal-output { + flex: 1; + padding: 12px; + font-family: var(--font-mono); + font-size: 13px; + overflow-y: auto; + white-space: pre-wrap; + background: var(--bg-void); +} + +.terminal-input-row { + display: flex; + align-items: center; + padding: 8px 12px; + background: var(--bg-input); + border-top: 1px solid var(--border-dim); +} + +.terminal-prompt { + color: var(--success); + font-family: var(--font-mono); + font-weight: 700; + margin-right: 8px; +} + +.terminal-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: var(--text-main); + font-family: var(--font-mono); + font-size: 13px; +} + +.config-container { + max-width: 800px; + margin: 0 auto; + padding: 24px; +} + +.config-section { + margin-bottom: 24px; +} + +.config-field { + display: flex; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--border-dim); +} + +.config-label { + width: 140px; + color: var(--text-dim); + font-size: 12px; +} + +.config-value { + color: var(--text-main); + font-size: 13px; +} + +@keyframes glitch { + 0% { transform: translate(0); } + 20% { transform: translate(-2px, 2px); } + 40% { transform: translate(-2px, -2px); } + 60% { transform: translate(2px, 2px); } + 80% { transform: translate(2px, -2px); } + 100% { transform: translate(0); } +} + +@keyframes scanline { + 0% { top: -10%; } + 100% { top: 110%; } +} + +@keyframes typewriter { + from { width: 0; } + to { width: 100%; } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.3s ease-out; +} + +.tab-transition { + animation: fadeIn 0.2s ease-out; +} + +.glitch-text { + animation: glitch 0.3s ease-in-out; +} + +.loading-spinner { + display: inline-block; + animation: pulse 1s infinite; + color: var(--cyber-red); +} diff --git a/cmd/muyue-desktop/frontend/src/themes/index.js b/cmd/muyue-desktop/frontend/src/themes/index.js new file mode 100644 index 0000000..355e325 --- /dev/null +++ b/cmd/muyue-desktop/frontend/src/themes/index.js @@ -0,0 +1,138 @@ +const defaultTheme = { + name: 'Cyberpunk Red', + colors: { + bgVoid: '#0A0A0C', + bgBase: '#0F0D10', + bgSurface: '#161218', + bgPanel: '#1C1719', + bgCard: '#221B1E', + bgInput: '#2A2225', + bgHover: '#332528', + cyberRed: '#FF0033', + cyberRedDark: '#8B0020', + cyberRedDeep: '#5C0015', + cyberPink: '#FF1A5E', + cyberRose: '#FF4D6D', + neonRed: '#FF1744', + brightRed: '#FF5252', + dimRed: '#6B2033', + mutedRed: '#4A1525', + textBright: '#EAE0E2', + textMain: '#D4C4C8', + textDim: '#8A7A7E', + textMuted: '#5A4F52', + success: '#00E676', + warning: '#FFD740', + error: '#FF1744', + borderDim: '#2A1F22', + borderRed: '#FF003344', + borderRedFull: '#FF0033', + }, + fonts: { + mono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace", + ui: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif", + }, + borderRadius: '8px', + animations: { + glitch: true, + scanline: true, + typewriter: true, + pulse: true, + }, +} + +const themes = { + 'cyberpunk-red': defaultTheme, + 'cyberpunk-pink': { + ...defaultTheme, + name: 'Cyberpunk Pink', + colors: { + ...defaultTheme.colors, + cyberRed: '#FF1A8C', + cyberRedDark: '#8B1050', + cyberRedDeep: '#5C0A35', + cyberPink: '#FF4DAE', + cyberRose: '#FF6DC2', + neonRed: '#FF1A8C', + brightRed: '#FF6DC2', + dimRed: '#6B2050', + mutedRed: '#4A1535', + }, + }, + 'midnight-blue': { + ...defaultTheme, + name: 'Midnight Blue', + colors: { + ...defaultTheme.colors, + cyberRed: '#0088FF', + cyberRedDark: '#004488', + cyberRedDeep: '#002255', + cyberPink: '#00AAFF', + cyberRose: '#44CCFF', + neonRed: '#0088FF', + brightRed: '#44CCFF', + dimRed: '#203366', + mutedRed: '#152244', + }, + }, + 'matrix-green': { + ...defaultTheme, + name: 'Matrix Green', + colors: { + ...defaultTheme.colors, + cyberRed: '#00FF41', + cyberRedDark: '#008822', + cyberRedDeep: '#005515', + cyberPink: '#33FF66', + cyberRose: '#66FF99', + neonRed: '#00FF41', + brightRed: '#66FF99', + dimRed: '#206630', + mutedRed: '#154420', + }, + }, +} + +export function getTheme(name) { + return themes[name] || defaultTheme +} + +export function getThemeNames() { + return Object.keys(themes).map(k => ({ id: k, name: themes[k].name })) +} + +export function applyTheme(theme) { + const root = document.documentElement + const c = theme.colors + const map = { + '--bg-void': c.bgVoid, + '--bg-base': c.bgBase, + '--bg-surface': c.bgSurface, + '--bg-panel': c.bgPanel, + '--bg-card': c.bgCard, + '--bg-input': c.bgInput, + '--bg-hover': c.bgHover, + '--cyber-red': c.cyberRed, + '--cyber-red-dark': c.cyberRedDark, + '--cyber-red-deep': c.cyberRedDeep, + '--cyber-pink': c.cyberPink, + '--cyber-rose': c.cyberRose, + '--neon-red': c.neonRed, + '--bright-red': c.brightRed, + '--dim-red': c.dimRed, + '--muted-red': c.mutedRed, + '--text-bright': c.textBright, + '--text-main': c.textMain, + '--text-dim': c.textDim, + '--text-muted': c.textMuted, + '--success': c.success, + '--warning': c.warning, + '--error': c.error, + '--border-dim': c.borderDim, + '--border-red': c.borderRed, + '--border-red-full': c.borderRedFull, + } + Object.entries(map).forEach(([k, v]) => root.style.setProperty(k, v)) +} + +export default defaultTheme diff --git a/cmd/muyue-desktop/frontend/vite.config.js b/cmd/muyue-desktop/frontend/vite.config.js new file mode 100644 index 0000000..ad12586 --- /dev/null +++ b/cmd/muyue-desktop/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, + server: { + proxy: { + '/api': 'http://127.0.0.1:0', + }, + }, +}) diff --git a/cmd/muyue-desktop/main.go b/cmd/muyue-desktop/main.go new file mode 100644 index 0000000..24bdffc --- /dev/null +++ b/cmd/muyue-desktop/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "embed" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/muyue/muyue/internal/api" + "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/version" +) + +//go:embed frontend/dist/* +var frontendFS embed.FS + +func main() { + if len(os.Args) > 1 && os.Args[1] == "--help" { + fmt.Printf("%s Desktop v%s\n", version.Name, version.Version) + fmt.Println("Usage: muyue-desktop [options]") + fmt.Println() + fmt.Println("Options:") + fmt.Println(" --help Show this help") + fmt.Println(" --port Specify port (default: auto)") + fmt.Println(" --no-open Don't open browser") + return + } + + cfg := loadConfig() + srv := api.NewServer(cfg) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatalf("Failed to bind: %v", err) + } + addr := listener.Addr().(*net.TCPAddr) + port := addr.Port + + frontendDist, err := fs.Sub(frontendFS, "frontend/dist") + if err != nil { + log.Fatalf("Failed to load frontend: %v", err) + } + + fileServer := http.FileServer(http.FS(frontendDist)) + + mux := http.NewServeMux() + mux.Handle("/api/", srv) + mux.Handle("/", fileServer) + + go func() { + log.Printf("%s Desktop v%s", version.Name, version.Version) + log.Printf("Listening on http://127.0.0.1:%d", port) + + if err := http.Serve(listener, mux); err != nil { + log.Fatalf("Server error: %v", err) + } + }() + + noOpen := false + for _, arg := range os.Args[1:] { + if arg == "--no-open" { + noOpen = true + } + } + + if !noOpen { + url := fmt.Sprintf("http://127.0.0.1:%d", port) + openBrowser(url) + log.Printf("Opened %s in browser", url) + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down...") +} + +func loadConfig() *config.MuyueConfig { + if !config.Exists() { + fmt.Println("No config found. Run `muyue setup` first.") + os.Exit(1) + } + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Config error: %v\n", err) + os.Exit(1) + } + return cfg +} + +func openBrowser(url string) { + var cmd *exec.Cmd + switch { + case commandExists("xdg-open"): + cmd = exec.Command("xdg-open", url) + case commandExists("open"): + cmd = exec.Command("open", url) + case commandExists("cmd"): + cmd = exec.Command("cmd", "/c", "start", url) + default: + fmt.Printf("Open manually: %s\n", url) + return + } + cmd.Start() +} + +func commandExists(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..d8a6719 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,266 @@ +package api + +import ( + "encoding/json" + "net/http" + "os/exec" + "strings" + + "github.com/muyue/muyue/internal/config" + "github.com/muyue/muyue/internal/lsp" + "github.com/muyue/muyue/internal/mcp" + "github.com/muyue/muyue/internal/scanner" + "github.com/muyue/muyue/internal/skills" + "github.com/muyue/muyue/internal/updater" + "github.com/muyue/muyue/internal/version" +) + +type Server struct { + config *config.MuyueConfig + scanResult *scanner.ScanResult + mux *http.ServeMux +} + +func NewServer(cfg *config.MuyueConfig) *Server { + s := &Server{ + config: cfg, + mux: http.NewServeMux(), + } + s.scanResult = scanner.ScanSystem() + s.routes() + return s +} + +func (s *Server) routes() { + s.mux.HandleFunc("/api/info", s.handleInfo) + s.mux.HandleFunc("/api/system", s.handleSystem) + s.mux.HandleFunc("/api/tools", s.handleTools) + s.mux.HandleFunc("/api/config", s.handleConfig) + s.mux.HandleFunc("/api/providers", s.handleProviders) + s.mux.HandleFunc("/api/skills", s.handleSkills) + s.mux.HandleFunc("/api/lsp", s.handleLSP) + s.mux.HandleFunc("/api/mcp", s.handleMCP) + s.mux.HandleFunc("/api/updates", s.handleUpdates) + s.mux.HandleFunc("/api/install", s.handleInstall) + s.mux.HandleFunc("/api/scan", s.handleScan) + s.mux.HandleFunc("/api/terminal", s.handleTerminal) + s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure) +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + s.mux.ServeHTTP(w, r) +} + +func writeJSON(w http.ResponseWriter, data interface{}) { + json.NewEncoder(w).Encode(data) +} + +func writeError(w http.ResponseWriter, msg string, code int) { + w.WriteHeader(code) + json.NewEncoder(w).Encode(map[string]string{"error": msg}) +} + +func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]interface{}{ + "name": version.Name, + "version": version.Version, + "author": version.Author, + }) +} + +func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) { + if s.scanResult == nil { + s.scanResult = scanner.ScanSystem() + } + writeJSON(w, map[string]interface{}{ + "system": s.scanResult.System, + }) +} + +type ToolInfo struct { + Name string `json:"name"` + Installed bool `json:"installed"` + Version string `json:"version"` + Path string `json:"path"` +} + +func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) { + if s.scanResult == nil { + s.scanResult = scanner.ScanSystem() + } + tools := make([]ToolInfo, len(s.scanResult.Tools)) + for i, t := range s.scanResult.Tools { + tools[i] = ToolInfo{ + Name: t.Name, + Installed: t.Installed, + Version: t.Version, + Path: t.Path, + } + } + writeJSON(w, map[string]interface{}{ + "tools": tools, + "total": len(tools), + }) +} + +func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { + if s.config == nil { + writeError(w, "no config", http.StatusNotFound) + return + } + writeJSON(w, map[string]interface{}{ + "profile": s.config.Profile, + "terminal": s.config.Terminal, + "bmad": s.config.BMAD, + }) +} + +func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) { + if s.config == nil { + writeError(w, "no config", http.StatusNotFound) + return + } + writeJSON(w, map[string]interface{}{ + "providers": s.config.AI.Providers, + }) +} + +func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) { + list, err := skills.List() + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, map[string]interface{}{ + "skills": list, + "count": len(list), + }) +} + +func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) { + servers := lsp.ScanServers() + writeJSON(w, map[string]interface{}{ + "servers": servers, + }) +} + +func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) { + servers := mcp.ScanServers() + writeJSON(w, map[string]interface{}{ + "servers": servers, + "configured": true, + }) +} + +func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, "POST only", http.StatusMethodNotAllowed) + return + } + err := mcp.ConfigureAll(s.config) + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, map[string]string{"status": "ok"}) +} + +func (s *Server) handleUpdates(w http.ResponseWriter, r *http.Request) { + result := scanner.ScanSystem() + statuses := updater.CheckUpdates(result) + type updateInfo struct { + Tool string `json:"tool"` + Current string `json:"current"` + Latest string `json:"latest"` + NeedsUpdate bool `json:"needsUpdate"` + Error string `json:"error,omitempty"` + } + updates := make([]updateInfo, len(statuses)) + for i, u := range statuses { + updates[i] = updateInfo{ + Tool: u.Tool, + Current: u.Current, + Latest: u.Latest, + NeedsUpdate: u.NeedsUpdate, + Error: u.Error, + } + } + writeJSON(w, map[string]interface{}{ + "updates": updates, + }) +} + +func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, "POST only", http.StatusMethodNotAllowed) + return + } + var body struct { + Tools []string `json:"tools"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } + if len(body.Tools) == 0 { + writeError(w, "no tools specified", http.StatusBadRequest) + return + } + writeJSON(w, map[string]string{"status": "installing"}) +} + +func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) { + s.scanResult = scanner.ScanSystem() + writeJSON(w, map[string]string{"status": "ok"}) +} + +type TermResult struct { + Output string `json:"output"` + Error string `json:"error,omitempty"` +} + +func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, "POST only", http.StatusMethodNotAllowed) + return + } + var body struct { + Command string `json:"command"` + Cwd string `json:"cwd"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } + + if body.Command == "" { + writeError(w, "no command", http.StatusBadRequest) + return + } + + shell := "/bin/sh" + if sh := strings.TrimSpace(body.Command); sh != "" { + if s, err := exec.LookPath("bash"); err == nil { + shell = s + } + } + + cmd := exec.Command(shell, "-c", body.Command) + if body.Cwd != "" { + cmd.Dir = body.Cwd + } + out, err := cmd.CombinedOutput() + result := TermResult{Output: string(out)} + if err != nil { + result.Error = err.Error() + } + writeJSON(w, result) +}