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 (
+
+
+
+
+ {renderContent()}
+
+
+
+
+ )
+}
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 (
+
+
+
+
+
+ {history.map((line, i) => (
+
+ {line.text}
+
+ ))}
+
+
+
+ {'>'}
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ autoFocus
+ />
+
+
+
+ {aiPanel && (
+
+
+
+
+ {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)
+}