feat: integrate Tauri v2 with Android widget and voice assistant
- Add Tauri v2 shell (Cargo, tauri.conf.json, capabilities, plugins) - Migrate all fetch() calls to apiFetch() for Tauri-aware HTTP - Migrate WebSocket endpoints to resolveEndpoints() for dynamic URLs - Add ServerConfigDialog for remote server URL configuration - Add tauri.ts lib with isTauri detection, apiFetch wrapper, plugin helpers - Add server-config Pinia store with persistence via plugin-store - Conditional PWA (disabled in Tauri builds) - Android: home screen transcript widget (last 5 messages, 30s refresh) - Android: voice command / share activity (SpeechRecognizer + WebSocket) - Android: signed release APK with auto-copy to installers/ - Remove stale frontend/src-tauri directory
33
.gitignore
vendored
@@ -9,6 +9,39 @@ nul
|
|||||||
# Voice recordings (training data)
|
# Voice recordings (training data)
|
||||||
server/recordings/*.webm
|
server/recordings/*.webm
|
||||||
|
|
||||||
|
# Tauri build artifacts
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/installers/
|
||||||
|
|
||||||
|
# Tauri gen: ignore everything except our custom Android sources
|
||||||
|
src-tauri/gen/*
|
||||||
|
!src-tauri/gen/android/
|
||||||
|
src-tauri/gen/android/*
|
||||||
|
!src-tauri/gen/android/app/
|
||||||
|
src-tauri/gen/android/app/*
|
||||||
|
!src-tauri/gen/android/app/build.gradle.kts
|
||||||
|
!src-tauri/gen/android/app/src/
|
||||||
|
src-tauri/gen/android/app/src/*
|
||||||
|
!src-tauri/gen/android/app/src/main/
|
||||||
|
src-tauri/gen/android/app/src/main/*
|
||||||
|
!src-tauri/gen/android/app/src/main/AndroidManifest.xml
|
||||||
|
!src-tauri/gen/android/app/src/main/java/
|
||||||
|
!src-tauri/gen/android/app/src/main/res/
|
||||||
|
src-tauri/gen/android/app/src/main/res/*
|
||||||
|
!src-tauri/gen/android/app/src/main/res/layout/
|
||||||
|
src-tauri/gen/android/app/src/main/res/layout/*
|
||||||
|
!src-tauri/gen/android/app/src/main/res/layout/widget_transcript.xml
|
||||||
|
!src-tauri/gen/android/app/src/main/res/xml/
|
||||||
|
src-tauri/gen/android/app/src/main/res/xml/*
|
||||||
|
!src-tauri/gen/android/app/src/main/res/xml/transcript_widget_info.xml
|
||||||
|
!src-tauri/gen/android/app/src/main/res/values/
|
||||||
|
src-tauri/gen/android/app/src/main/res/values/*
|
||||||
|
!src-tauri/gen/android/app/src/main/res/values/strings.xml
|
||||||
|
src-tauri/gen/android/keystore.jks
|
||||||
|
|
||||||
|
# Old frontend Tauri location
|
||||||
|
frontend/src-tauri/
|
||||||
|
|
||||||
# Agent runtime data
|
# Agent runtime data
|
||||||
.claude-*/plugins/
|
.claude-*/plugins/
|
||||||
.claude-*/plans/
|
.claude-*/plans/
|
||||||
|
|||||||
279
frontend/package-lock.json
generated
@@ -9,6 +9,12 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
||||||
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.7",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.2",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
@@ -19,6 +25,7 @@
|
|||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.10.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.2",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
@@ -2415,6 +2422,278 @@
|
|||||||
"sourcemap-codec": "^1.4.8"
|
"sourcemap-codec": "^1.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "2.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||||
|
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"bin": {
|
||||||
|
"tauri": "tauri.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tauri-apps/cli-darwin-arm64": "2.10.0",
|
||||||
|
"@tauri-apps/cli-darwin-x64": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-arm64-gnu": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-arm64-musl": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-x64-gnu": "2.10.0",
|
||||||
|
"@tauri-apps/cli-linux-x64-musl": "2.10.0",
|
||||||
|
"@tauri-apps/cli-win32-arm64-msvc": "2.10.0",
|
||||||
|
"@tauri-apps/cli-win32-ia32-msvc": "2.10.0",
|
||||||
|
"@tauri-apps/cli-win32-x64-msvc": "2.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-clipboard-manager": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-http": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-store": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -8,10 +8,25 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-icons": "node scripts/generate-icons.js"
|
"generate-icons": "node scripts/generate-icons.js",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"tauri:dev": "tauri dev",
|
||||||
|
"tauri:build": "tauri build",
|
||||||
|
"tauri:android:init": "tauri android init",
|
||||||
|
"tauri:android:dev": "tauri android dev",
|
||||||
|
"tauri:android:build": "tauri android build",
|
||||||
|
"tauri:ios:init": "tauri ios init",
|
||||||
|
"tauri:ios:dev": "tauri ios dev",
|
||||||
|
"tauri:ios:build": "tauri ios build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
||||||
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.7",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.2",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
@@ -22,6 +37,7 @@
|
|||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.10.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.2",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
|||||||
@@ -10,19 +10,27 @@ import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
|
|||||||
import TerminalFabStack from './components/transcript-debug/TerminalFabStack.vue'
|
import TerminalFabStack from './components/transcript-debug/TerminalFabStack.vue'
|
||||||
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
||||||
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
||||||
|
import ServerConfigDialog from './components/ServerConfigDialog.vue'
|
||||||
import { useGlobalApproval } from './composables/useGlobalApproval'
|
import { useGlobalApproval } from './composables/useGlobalApproval'
|
||||||
import { initWebMCP, getWebMCP } from './services/webmcp'
|
import { initWebMCP, getWebMCP } from './services/webmcp'
|
||||||
import { initTorch, destroyTorch } from './services/torch'
|
import { initTorch, destroyTorch } from './services/torch'
|
||||||
import { initSessionStateWS, destroySessionStateWS } from './services/session-state-ws'
|
import { initSessionStateWS, destroySessionStateWS } from './services/session-state-ws'
|
||||||
import { endpoints } from './config/endpoints'
|
|
||||||
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||||
import { setResponseControls } from './services/tools/handlers/responseHandlers'
|
import { setResponseControls } from './services/tools/handlers/responseHandlers'
|
||||||
import { useCanvasStore } from './stores/canvas'
|
import { useCanvasStore } from './stores/canvas'
|
||||||
import { useProjectCanvasStore } from './stores/projectCanvas'
|
import { useProjectCanvasStore } from './stores/projectCanvas'
|
||||||
import { useSessionState } from './stores/session-state'
|
import { useSessionState } from './stores/session-state'
|
||||||
|
import { isTauri } from './lib/tauri'
|
||||||
|
import { useServerConfig } from './stores/server-config'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Tauri server config
|
||||||
|
const serverConfig = isTauri ? useServerConfig() : null
|
||||||
|
const showServerConfig = ref(false)
|
||||||
|
const needsServerConfig = computed(() => isTauri && serverConfig && !serverConfig.isConfigured)
|
||||||
|
|
||||||
const showVoice = ref(false)
|
const showVoice = ref(false)
|
||||||
const showTranscriptDebug = ref(false)
|
const showTranscriptDebug = ref(false)
|
||||||
const showDebugConsole = ref(false)
|
const showDebugConsole = ref(false)
|
||||||
@@ -293,6 +301,20 @@ watch(() => route.name, (newPage) => {
|
|||||||
activatePageTools(newPage as PageName)
|
activatePageTools(newPage as PageName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch for Tauri server config changes — re-init services when server is configured
|
||||||
|
if (serverConfig) {
|
||||||
|
watch(() => serverConfig!.isConfigured, async (configured) => {
|
||||||
|
if (configured) {
|
||||||
|
showServerConfig.value = false
|
||||||
|
// Re-initialize all services with the new server URL
|
||||||
|
initSessionStateWS()
|
||||||
|
initWhisperSocket()
|
||||||
|
await initWebMCP()
|
||||||
|
await initTorch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -325,7 +347,12 @@ watch(() => route.name, (newPage) => {
|
|||||||
</svg>
|
</svg>
|
||||||
<span v-if="debugLogs.length" class="log-count">{{ debugLogs.length }}</span>
|
<span v-if="debugLogs.length" class="log-count">{{ debugLogs.length }}</span>
|
||||||
</button>
|
</button>
|
||||||
<PwaInstallBanner />
|
<PwaInstallBanner v-if="!isTauri" />
|
||||||
|
<button v-if="isTauri" class="server-config-btn" @click="showServerConfig = true" title="Server settings">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button
|
<button
|
||||||
@@ -427,6 +454,9 @@ watch(() => route.name, (newPage) => {
|
|||||||
<!-- Global Hooks Approval Modal -->
|
<!-- Global Hooks Approval Modal -->
|
||||||
<HooksApprovalModal />
|
<HooksApprovalModal />
|
||||||
|
|
||||||
|
<!-- Tauri Server Config Dialog -->
|
||||||
|
<ServerConfigDialog v-if="needsServerConfig || showServerConfig" />
|
||||||
|
|
||||||
<!-- Debug Console Panel -->
|
<!-- Debug Console Panel -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="debug-slide">
|
<Transition name="debug-slide">
|
||||||
@@ -1031,6 +1061,28 @@ watch(() => route.name, (newPage) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Server Config Button (Tauri) */
|
||||||
|
.server-config-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-config-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--accent, #6366f1);
|
||||||
|
border-color: var(--accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Debug Console Button */
|
/* Debug Console Button */
|
||||||
.debug-btn {
|
.debug-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
import { endpoints } from '../config/endpoints'
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
import { resolveEndpoints } from '../config/endpoints'
|
||||||
|
|
||||||
// Web Speech API types (not in default TS lib)
|
// Web Speech API types (not in default TS lib)
|
||||||
interface SpeechRecognitionEvent extends Event {
|
interface SpeechRecognitionEvent extends Event {
|
||||||
@@ -64,7 +65,7 @@ const containerRef = ref<HTMLElement | null>(null)
|
|||||||
let recognition: SpeechRecognition | null = null as SpeechRecognition | null
|
let recognition: SpeechRecognition | null = null as SpeechRecognition | null
|
||||||
|
|
||||||
// WebSocket connection to terminal
|
// WebSocket connection to terminal
|
||||||
const WS_URL = endpoints.terminal
|
const WS_URL = resolveEndpoints().terminal
|
||||||
let socket: WebSocket | null = null
|
let socket: WebSocket | null = null
|
||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ let pendingWhisperSend = false // Flag to send transcript when Whisper responds
|
|||||||
const useWhisper = ref(false)
|
const useWhisper = ref(false)
|
||||||
const whisperReady = ref(false)
|
const whisperReady = ref(false)
|
||||||
const whisperLoading = ref(false)
|
const whisperLoading = ref(false)
|
||||||
const WHISPER_WS_URL = endpoints.whisper
|
const WHISPER_WS_URL = resolveEndpoints().whisper
|
||||||
let whisperSocket: WebSocket | null = null
|
let whisperSocket: WebSocket | null = null
|
||||||
let mediaRecorder: MediaRecorder | null = null
|
let mediaRecorder: MediaRecorder | null = null
|
||||||
let audioChunks: Blob[] = []
|
let audioChunks: Blob[] = []
|
||||||
@@ -172,7 +173,7 @@ async function saveRecordingToBackend(blob: Blob) {
|
|||||||
reader.onloadend = async () => {
|
reader.onloadend = async () => {
|
||||||
const base64 = (reader.result as string).split(',')[1]
|
const base64 = (reader.result as string).split(',')[1]
|
||||||
|
|
||||||
const response = await fetch('/api/recordings', {
|
const response = await apiFetch('/api/recordings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -331,7 +332,7 @@ function initRecognition() {
|
|||||||
|
|
||||||
async function checkWhisperStatus(updateLoading = true) {
|
async function checkWhisperStatus(updateLoading = true) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/status')
|
const res = await apiFetch('/api/whisper/status')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
useWhisper.value = data.enabled
|
useWhisper.value = data.enabled
|
||||||
whisperReady.value = data.running
|
whisperReady.value = data.running
|
||||||
@@ -365,7 +366,7 @@ async function toggleWhisperMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/toggle', {
|
const res = await apiFetch('/api/whisper/toggle', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|||||||
282
frontend/src/components/ServerConfigDialog.vue
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useServerConfig } from '@/stores/server-config'
|
||||||
|
|
||||||
|
const serverConfig = useServerConfig()
|
||||||
|
|
||||||
|
const urlInput = ref('')
|
||||||
|
const testing = ref(false)
|
||||||
|
const testResult = ref<'idle' | 'success' | 'error'>('idle')
|
||||||
|
|
||||||
|
async function handleTest() {
|
||||||
|
if (!urlInput.value.trim()) return
|
||||||
|
testing.value = true
|
||||||
|
testResult.value = 'idle'
|
||||||
|
|
||||||
|
const ok = await serverConfig.testConnection(urlInput.value.trim())
|
||||||
|
testResult.value = ok ? 'success' : 'error'
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConnect() {
|
||||||
|
if (!urlInput.value.trim()) return
|
||||||
|
const ok = await serverConfig.setServer(urlInput.value.trim())
|
||||||
|
if (ok) {
|
||||||
|
testResult.value = 'success'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRecent(url: string) {
|
||||||
|
urlInput.value = url
|
||||||
|
testResult.value = 'idle'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (serverConfig.serverUrl) {
|
||||||
|
urlInput.value = serverConfig.serverUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="server-config-overlay">
|
||||||
|
<div class="server-config-dialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h2>Connect to Server</h2>
|
||||||
|
<p class="dialog-subtitle">Enter the URL of your Agent UI backend</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="server-url">Server URL</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input
|
||||||
|
id="server-url"
|
||||||
|
v-model="urlInput"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://your-server.com or http://192.168.1.100:4101"
|
||||||
|
@keydown.enter="handleConnect"
|
||||||
|
:disabled="serverConfig.loading"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-test"
|
||||||
|
@click="handleTest"
|
||||||
|
:disabled="!urlInput.trim() || testing"
|
||||||
|
>
|
||||||
|
{{ testing ? 'Testing...' : 'Test' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="testResult === 'success'" class="status-msg success">
|
||||||
|
Connected successfully
|
||||||
|
</div>
|
||||||
|
<div v-if="testResult === 'error'" class="status-msg error">
|
||||||
|
Could not connect to server
|
||||||
|
</div>
|
||||||
|
<div v-if="serverConfig.error" class="status-msg error">
|
||||||
|
{{ serverConfig.error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="serverConfig.recentUrls.length > 0" class="recent-urls">
|
||||||
|
<label>Recent</label>
|
||||||
|
<div class="recent-list">
|
||||||
|
<button
|
||||||
|
v-for="url in serverConfig.recentUrls"
|
||||||
|
:key="url"
|
||||||
|
class="recent-item"
|
||||||
|
@click="selectRecent(url)"
|
||||||
|
>
|
||||||
|
{{ url }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="handleConnect"
|
||||||
|
:disabled="!urlInput.trim() || serverConfig.loading"
|
||||||
|
>
|
||||||
|
{{ serverConfig.loading ? 'Connecting...' : 'Connect' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.server-config-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-config-dialog {
|
||||||
|
background: var(--bg-primary, #0f0f14);
|
||||||
|
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 480px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 1.5rem 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #e4e4e7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-subtitle {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary, #a1a1aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary, #a1a1aa);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
background: var(--bg-secondary, #1a1a24);
|
||||||
|
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary, #e4e4e7);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row input:focus {
|
||||||
|
border-color: var(--accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row input::placeholder {
|
||||||
|
color: var(--text-muted, #52525b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary, #1a1a24);
|
||||||
|
color: var(--text-primary, #e4e4e7);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
background: var(--bg-hover, #252530);
|
||||||
|
border-color: var(--accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent, #6366f1);
|
||||||
|
border-color: var(--accent, #6366f1);
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #4f46e5;
|
||||||
|
border-color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-msg {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-msg.success {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-msg.error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-urls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-urls label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary, #a1a1aa);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.06));
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary, #a1a1aa);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item:hover {
|
||||||
|
background: var(--bg-hover, #252530);
|
||||||
|
color: var(--text-primary, #e4e4e7);
|
||||||
|
border-color: var(--accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
padding: 0 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { AgentName, SessionInfo } from '@/types/transcript-debug'
|
import type { AgentName, SessionInfo } from '@/types/transcript-debug'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -51,7 +52,7 @@ async function fetchAllSessions() {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
props.agents.map(async (a) => {
|
props.agents.map(async (a) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/transcript-debug/sessions?agent=${a.id}`)
|
const res = await apiFetch(`/api/transcript-debug/sessions?agent=${a.id}`)
|
||||||
if (res.ok) map[a.id] = await res.json()
|
if (res.ok) map[a.id] = await res.json()
|
||||||
else map[a.id] = []
|
else map[a.id] = []
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { TableInfo, TableSchema, DbStats } from '@/types/database'
|
import type { TableInfo, TableSchema, DbStats } from '@/types/database'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
@@ -17,7 +18,7 @@ export function useDatabaseApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/tables`)
|
const res = await apiFetch(`${API_BASE}/tables`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch tables')
|
if (!res.ok) throw new Error('Failed to fetch tables')
|
||||||
tables.value = await res.json()
|
tables.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -29,7 +30,7 @@ export function useDatabaseApi() {
|
|||||||
|
|
||||||
async function fetchDbStats() {
|
async function fetchDbStats() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/stats`)
|
const res = await apiFetch(`${API_BASE}/stats`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch stats')
|
if (!res.ok) throw new Error('Failed to fetch stats')
|
||||||
dbStats.value = await res.json()
|
dbStats.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -39,7 +40,7 @@ export function useDatabaseApi() {
|
|||||||
|
|
||||||
async function fetchTableSchema(tableName: string) {
|
async function fetchTableSchema(tableName: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/tables/${tableName}/schema`)
|
const res = await apiFetch(`${API_BASE}/tables/${tableName}/schema`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch schema')
|
if (!res.ok) throw new Error('Failed to fetch schema')
|
||||||
tableSchema.value = await res.json()
|
tableSchema.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -52,7 +53,7 @@ export function useDatabaseApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const offset = (page - 1) * pageSize
|
const offset = (page - 1) * pageSize
|
||||||
const res = await fetch(`${API_BASE}/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`)
|
const res = await apiFetch(`${API_BASE}/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch data')
|
if (!res.ok) throw new Error('Failed to fetch data')
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
tableData.value = result.rows
|
tableData.value = result.rows
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_BASE = '/api/database'
|
const API_BASE = '/api/database'
|
||||||
@@ -17,7 +18,7 @@ export function useQueryExecutor() {
|
|||||||
queryResult.value = null
|
queryResult.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/query`, {
|
const res = await apiFetch(`${API_BASE}/query`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ query: queryText.value })
|
body: JSON.stringify({ query: queryText.value })
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { GitStatus, CommitInfo, FileDiff, BranchInfo, CompareResult, DiffResult, TreeNode, FileContent } from '@/types/git'
|
import type { GitStatus, CommitInfo, FileDiff, BranchInfo, CompareResult, DiffResult, TreeNode, FileContent } from '@/types/git'
|
||||||
|
|
||||||
const API_BASE = '/api/git'
|
const API_BASE = '/api/git'
|
||||||
@@ -20,7 +21,7 @@ export function useGitApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/status`)
|
const res = await apiFetch(`${API_BASE}/status`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch status')
|
if (!res.ok) throw new Error('Failed to fetch status')
|
||||||
status.value = await res.json()
|
status.value = await res.json()
|
||||||
currentBranch.value = status.value?.branch || ''
|
currentBranch.value = status.value?.branch || ''
|
||||||
@@ -39,7 +40,7 @@ export function useGitApi() {
|
|||||||
if (options?.staged) params.set('staged', 'true')
|
if (options?.staged) params.set('staged', 'true')
|
||||||
if (options?.file) params.set('file', options.file)
|
if (options?.file) params.set('file', options.file)
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/diff?${params}`)
|
const res = await apiFetch(`${API_BASE}/diff?${params}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch diff')
|
if (!res.ok) throw new Error('Failed to fetch diff')
|
||||||
diff.value = await res.json()
|
diff.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -53,7 +54,7 @@ export function useGitApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/log?limit=${limit}&offset=${offset}`)
|
const res = await apiFetch(`${API_BASE}/log?limit=${limit}&offset=${offset}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch log')
|
if (!res.ok) throw new Error('Failed to fetch log')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
commits.value = append ? [...commits.value, ...data] : data
|
commits.value = append ? [...commits.value, ...data] : data
|
||||||
@@ -68,7 +69,7 @@ export function useGitApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/log/${sha}`)
|
const res = await apiFetch(`${API_BASE}/log/${sha}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch commit')
|
if (!res.ok) throw new Error('Failed to fetch commit')
|
||||||
selectedCommit.value = await res.json()
|
selectedCommit.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -81,7 +82,7 @@ export function useGitApi() {
|
|||||||
async function fetchBranches() {
|
async function fetchBranches() {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/branches`)
|
const res = await apiFetch(`${API_BASE}/branches`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch branches')
|
if (!res.ok) throw new Error('Failed to fetch branches')
|
||||||
branches.value = await res.json()
|
branches.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -93,7 +94,7 @@ export function useGitApi() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/compare`, {
|
const res = await apiFetch(`${API_BASE}/compare`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ base, head })
|
body: JSON.stringify({ base, head })
|
||||||
@@ -125,7 +126,7 @@ export function useGitApi() {
|
|||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (path) params.set('path', path)
|
if (path) params.set('path', path)
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/tree?${params}`)
|
const res = await apiFetch(`${API_BASE}/tree?${params}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch file tree')
|
if (!res.ok) throw new Error('Failed to fetch file tree')
|
||||||
fileTree.value = await res.json()
|
fileTree.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -140,7 +141,7 @@ export function useGitApi() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ path })
|
const params = new URLSearchParams({ path })
|
||||||
const res = await fetch(`${API_BASE}/file?${params}`)
|
const res = await apiFetch(`${API_BASE}/file?${params}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch file content')
|
if (!res.ok) throw new Error('Failed to fetch file content')
|
||||||
fileContent.value = await res.json()
|
fileContent.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
||||||
|
|
||||||
export function useHooksApproval() {
|
export function useHooksApproval() {
|
||||||
@@ -33,7 +34,7 @@ export function useHooksApproval() {
|
|||||||
|
|
||||||
async function respondPermission(requestId: string, decision: 'allow' | 'deny') {
|
async function respondPermission(requestId: string, decision: 'allow' | 'deny') {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/hooks-approval/respond', {
|
await apiFetch('/api/hooks-approval/respond', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision })
|
body: JSON.stringify({ requestId, decision })
|
||||||
@@ -48,7 +49,7 @@ export function useHooksApproval() {
|
|||||||
|
|
||||||
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/hooks-approval/respond-plan', {
|
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision, reason })
|
body: JSON.stringify({ requestId, decision, reason })
|
||||||
@@ -63,7 +64,7 @@ export function useHooksApproval() {
|
|||||||
|
|
||||||
async function fetchPending() {
|
async function fetchPending() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/hooks-approval')
|
const res = await apiFetch('/api/hooks-approval')
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, shallowRef, computed, onUnmounted } from 'vue'
|
import { ref, shallowRef, computed, onUnmounted } from 'vue'
|
||||||
import { endpoints, terminalApiUrl } from '@/config/endpoints'
|
import { resolveEndpoints, terminalApiUrl } from '@/config/endpoints'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import { useEphemeralTerminal, type EphemeralTerminal } from '../useEphemeralTerminal'
|
import { useEphemeralTerminal, type EphemeralTerminal } from '../useEphemeralTerminal'
|
||||||
import { useSessionState } from '@/stores/session-state'
|
import { useSessionState } from '@/stores/session-state'
|
||||||
import type {
|
import type {
|
||||||
@@ -102,7 +103,7 @@ export function useTranscriptDebug() {
|
|||||||
updates: { transcriptSessionId?: string; label?: string }
|
updates: { transcriptSessionId?: string; label?: string }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await fetch(terminalApiUrl('/update-terminal'), {
|
await apiFetch(terminalApiUrl('/update-terminal'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ephemeralSessionId, ...updates })
|
body: JSON.stringify({ ephemeralSessionId, ...updates })
|
||||||
@@ -112,7 +113,7 @@ export function useTranscriptDebug() {
|
|||||||
|
|
||||||
async function unregisterTerminalOnServer(ephemeralSessionId: string) {
|
async function unregisterTerminalOnServer(ephemeralSessionId: string) {
|
||||||
try {
|
try {
|
||||||
await fetch(terminalApiUrl('/unregister-terminal'), {
|
await apiFetch(terminalApiUrl('/unregister-terminal'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ephemeralSessionId })
|
body: JSON.stringify({ ephemeralSessionId })
|
||||||
@@ -141,7 +142,7 @@ export function useTranscriptDebug() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Server creates PTY, runs command, registers in registry, broadcasts
|
// Server creates PTY, runs command, registers in registry, broadcasts
|
||||||
const res = await fetch(terminalApiUrl('/create-terminal'), {
|
const res = await apiFetch(terminalApiUrl('/create-terminal'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -215,7 +216,7 @@ export function useTranscriptDebug() {
|
|||||||
} else if (ephSid) {
|
} else if (ephSid) {
|
||||||
// No local terminal — kill the PTY directly on server
|
// No local terminal — kill the PTY directly on server
|
||||||
try {
|
try {
|
||||||
await fetch(terminalApiUrl('/kill-session'), {
|
await apiFetch(terminalApiUrl('/kill-session'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ sessionId: ephSid })
|
body: JSON.stringify({ sessionId: ephSid })
|
||||||
@@ -324,7 +325,7 @@ export function useTranscriptDebug() {
|
|||||||
if (socket?.readyState === WebSocket.OPEN) return
|
if (socket?.readyState === WebSocket.OPEN) return
|
||||||
|
|
||||||
// Same sync server as git (port 4105)
|
// Same sync server as git (port 4105)
|
||||||
socket = new WebSocket(endpoints.git)
|
socket = new WebSocket(resolveEndpoints().git)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
isRealtime.value = true
|
isRealtime.value = true
|
||||||
@@ -436,7 +437,7 @@ export function useTranscriptDebug() {
|
|||||||
if (!selectedSessionId.value) return
|
if (!selectedSessionId.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/transcript-debug/${selectedSessionId.value}/raw?agent=${selectedAgent.value}`)
|
const res = await apiFetch(`/api/transcript-debug/${selectedSessionId.value}/raw?agent=${selectedAgent.value}`)
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
rawContent.value = await res.text()
|
rawContent.value = await res.text()
|
||||||
const parsed = parseJsonl(rawContent.value, selectedSessionId.value)
|
const parsed = parseJsonl(rawContent.value, selectedSessionId.value)
|
||||||
@@ -544,7 +545,7 @@ export function useTranscriptDebug() {
|
|||||||
|
|
||||||
async function fetchSessions() {
|
async function fetchSessions() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/transcript-debug/sessions?agent=${selectedAgent.value}`)
|
const res = await apiFetch(`/api/transcript-debug/sessions?agent=${selectedAgent.value}`)
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
sessions.value = await res.json()
|
sessions.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -557,7 +558,7 @@ export function useTranscriptDebug() {
|
|||||||
// Determine agent from registry if different from selected
|
// Determine agent from registry if different from selected
|
||||||
const entry = serverRegistry.value.find(e => e.transcriptSessionId === sessionId)
|
const entry = serverRegistry.value.find(e => e.transcriptSessionId === sessionId)
|
||||||
const agent = entry?.agent || selectedAgent.value
|
const agent = entry?.agent || selectedAgent.value
|
||||||
const res = await fetch(`/api/transcript-debug/${sessionId}/raw?agent=${agent}`)
|
const res = await apiFetch(`/api/transcript-debug/${sessionId}/raw?agent=${agent}`)
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
rawContent.value = await res.text()
|
rawContent.value = await res.text()
|
||||||
conversation.value = parseJsonl(rawContent.value, sessionId)
|
conversation.value = parseJsonl(rawContent.value, sessionId)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
import { ref, computed, type Ref } from 'vue'
|
import { ref, computed, type Ref } from 'vue'
|
||||||
import { useTerminalRenderer, type TerminalRenderer } from './useTerminalRenderer'
|
import { useTerminalRenderer, type TerminalRenderer } from './useTerminalRenderer'
|
||||||
import { endpoints, terminalApiUrl } from '../config/endpoints'
|
import { resolveEndpoints, terminalApiUrl } from '../config/endpoints'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
export type EphemeralState = 'off' | 'connecting' | 'shell-ready' | 'running' | 'exited'
|
export type EphemeralState = 'off' | 'connecting' | 'shell-ready' | 'running' | 'exited'
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ export function useEphemeralTerminal(
|
|||||||
if (state.value !== 'off') return
|
if (state.value !== 'off') return
|
||||||
state.value = 'connecting'
|
state.value = 'connecting'
|
||||||
|
|
||||||
const wsBase = endpoints.terminal
|
const wsBase = resolveEndpoints().terminal
|
||||||
const sep = wsBase.includes('?') ? '&' : '?'
|
const sep = wsBase.includes('?') ? '&' : '?'
|
||||||
const wsUrl = `${wsBase}${sep}session=${ephemeralSessionId}`
|
const wsUrl = `${wsBase}${sep}session=${ephemeralSessionId}`
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ export function useEphemeralTerminal(
|
|||||||
|
|
||||||
// Force-kill via HTTP as safety net
|
// Force-kill via HTTP as safety net
|
||||||
try {
|
try {
|
||||||
await fetch(terminalApiUrl('/kill-session'), {
|
await apiFetch(terminalApiUrl('/kill-session'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ sessionId: ephemeralSessionId })
|
body: JSON.stringify({ sessionId: ephemeralSessionId })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useSessionState } from '@/stores/session-state'
|
import { useSessionState } from '@/stores/session-state'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
||||||
|
|
||||||
export interface ApprovalSessionGroup {
|
export interface ApprovalSessionGroup {
|
||||||
@@ -79,7 +80,7 @@ export function useGlobalApproval() {
|
|||||||
async function respondPermission(requestId: string, decision: string, reason?: string) {
|
async function respondPermission(requestId: string, decision: string, reason?: string) {
|
||||||
console.log(`[GlobalApproval] Responding permission ${requestId}: ${decision}${reason ? ' reason=' + reason : ''}`)
|
console.log(`[GlobalApproval] Responding permission ${requestId}: ${decision}${reason ? ' reason=' + reason : ''}`)
|
||||||
try {
|
try {
|
||||||
await fetch('/api/hooks-approval/respond', {
|
await apiFetch('/api/hooks-approval/respond', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision, reason })
|
body: JSON.stringify({ requestId, decision, reason })
|
||||||
@@ -93,7 +94,7 @@ export function useGlobalApproval() {
|
|||||||
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
||||||
console.log(`[GlobalApproval] Responding plan ${requestId}: ${decision}`)
|
console.log(`[GlobalApproval] Responding plan ${requestId}: ${decision}`)
|
||||||
try {
|
try {
|
||||||
await fetch('/api/hooks-approval/respond-plan', {
|
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision, reason })
|
body: JSON.stringify({ requestId, decision, reason })
|
||||||
@@ -106,7 +107,7 @@ export function useGlobalApproval() {
|
|||||||
async function ignoreApproval(requestId: string) {
|
async function ignoreApproval(requestId: string) {
|
||||||
console.log(`[GlobalApproval] Ignoring ${requestId} (UI-only removal)`)
|
console.log(`[GlobalApproval] Ignoring ${requestId} (UI-only removal)`)
|
||||||
try {
|
try {
|
||||||
await fetch('/api/hooks-approval/ignore', {
|
await apiFetch('/api/hooks-approval/ignore', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId })
|
body: JSON.stringify({ requestId })
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Centralized endpoint configuration for Agent UI
|
* Centralized endpoint configuration for Agent UI
|
||||||
* Automatically detects HTTPS/Traefik and uses appropriate protocols
|
* Automatically detects HTTPS/Traefik and uses appropriate protocols
|
||||||
|
* Supports Tauri mode where all URLs resolve against a configured server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isTauri, getServerUrl } from '@/lib/tauri'
|
||||||
|
|
||||||
// Detect if running over HTTPS (behind Traefik)
|
// Detect if running over HTTPS (behind Traefik)
|
||||||
export const isSecure = window.location.protocol === 'https:'
|
export const isSecure = window.location.protocol === 'https:'
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ function buildHttpUrl(securePath: string, devPort: number): string {
|
|||||||
return `http://${hostname}:${devPort}`
|
return `http://${hostname}:${devPort}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint configuration
|
// Static endpoint configuration (used in web mode)
|
||||||
export const endpoints = {
|
export const endpoints = {
|
||||||
// Terminal WebSocket
|
// Terminal WebSocket
|
||||||
terminal: buildWsUrl('/ws/terminal', 4103),
|
terminal: buildWsUrl('/ws/terminal', 4103),
|
||||||
@@ -57,17 +60,85 @@ export const endpoints = {
|
|||||||
api: '/api'
|
api: '/api'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve endpoints dynamically.
|
||||||
|
* In Tauri mode, builds WebSocket/HTTP URLs against the configured server.
|
||||||
|
* In web mode, returns the static endpoints.
|
||||||
|
*/
|
||||||
|
export function resolveEndpoints() {
|
||||||
|
if (!isTauri) return endpoints
|
||||||
|
|
||||||
|
const serverUrl = getServerUrl()
|
||||||
|
if (!serverUrl) return endpoints
|
||||||
|
|
||||||
|
// Parse server URL to get host and protocol
|
||||||
|
let url: URL
|
||||||
|
try {
|
||||||
|
url = new URL(serverUrl)
|
||||||
|
} catch {
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
const secure = url.protocol === 'https:'
|
||||||
|
const wsProto = secure ? 'wss:' : 'ws:'
|
||||||
|
const host = url.host // includes port if non-standard
|
||||||
|
|
||||||
|
function tauriBuildWs(securePath: string, devPort: number): string {
|
||||||
|
if (secure) {
|
||||||
|
return `${wsProto}//${host}${securePath}`
|
||||||
|
}
|
||||||
|
// In Tauri dev, use the server host with specific ports
|
||||||
|
const baseHost = url.hostname
|
||||||
|
return `${wsProto}//${baseHost}:${devPort}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function tauriBuildHttp(securePath: string, devPort: number): string {
|
||||||
|
if (secure) {
|
||||||
|
return `${url.protocol}//${host}${securePath}`
|
||||||
|
}
|
||||||
|
const baseHost = url.hostname
|
||||||
|
return `http://${baseHost}:${devPort}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
terminal: tauriBuildWs('/ws/terminal', 4103),
|
||||||
|
git: tauriBuildWs('/ws/git', 4105),
|
||||||
|
claudeStatus: tauriBuildWs('/ws/status', 4103),
|
||||||
|
whisper: tauriBuildWs('/ws/whisper', 4104),
|
||||||
|
webmcp: tauriBuildWs('/ws/mcp', 4102),
|
||||||
|
webmcpHttp: tauriBuildHttp('/mcp', 4102),
|
||||||
|
torch: tauriBuildWs('/ws/git', 4105),
|
||||||
|
api: `${serverUrl}/api`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the API base URL (for use in Tauri and web) */
|
||||||
|
export function getApiBase(): string {
|
||||||
|
if (!isTauri) return '/api'
|
||||||
|
const serverUrl = getServerUrl()
|
||||||
|
return serverUrl ? `${serverUrl}/api` : '/api'
|
||||||
|
}
|
||||||
|
|
||||||
// Agent terminal helpers
|
// Agent terminal helpers
|
||||||
export function agentTerminalUrl(agentId: string): string {
|
export function agentTerminalUrl(agentId: string): string {
|
||||||
const base = endpoints.terminal
|
const base = resolveEndpoints().terminal
|
||||||
const sep = base.includes('?') ? '&' : '?'
|
const sep = base.includes('?') ? '&' : '?'
|
||||||
return `${base}${sep}session=agent-${agentId}`
|
return `${base}${sep}session=agent-${agentId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function terminalApiUrl(path: string): string {
|
export function terminalApiUrl(path: string): string {
|
||||||
|
if (isTauri) {
|
||||||
|
const serverUrl = getServerUrl()
|
||||||
|
if (serverUrl) {
|
||||||
|
const url = new URL(serverUrl)
|
||||||
|
const secure = url.protocol === 'https:'
|
||||||
|
if (secure) return `https://${url.host}/ws/terminal${path}`
|
||||||
|
return `http://${url.hostname}:4103${path}`
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isSecure) return `https://${hostname}/ws/terminal${path}`
|
if (isSecure) return `https://${hostname}/ws/terminal${path}`
|
||||||
return `http://${hostname}:4103${path}`
|
return `http://${hostname}:4103${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
console.log('[Endpoints]', isSecure ? 'HTTPS/Traefik mode' : 'Development mode', endpoints)
|
console.log('[Endpoints]', isTauri ? 'Tauri mode' : (isSecure ? 'HTTPS/Traefik mode' : 'Development mode'), endpoints)
|
||||||
|
|||||||
85
frontend/src/lib/tauri.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Tauri detection and fetch wrapper for Agent UI.
|
||||||
|
* In Tauri mode, relative /api/... paths are resolved against the configured server URL.
|
||||||
|
* In web mode, this is a transparent pass-through to native fetch.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Detect if running inside a Tauri webview
|
||||||
|
export const isTauri = '__TAURI_INTERNALS__' in window
|
||||||
|
|
||||||
|
// Detect mobile Tauri (Android/iOS)
|
||||||
|
export function isMobileTauri(): boolean {
|
||||||
|
if (!isTauri) return false
|
||||||
|
const ua = navigator.userAgent.toLowerCase()
|
||||||
|
return /android|iphone|ipad|ipod/.test(ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server URL storage (in-memory, loaded from Tauri store on init)
|
||||||
|
let _serverUrl = ''
|
||||||
|
|
||||||
|
export function getServerUrl(): string {
|
||||||
|
return _serverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setServerUrl(url: string) {
|
||||||
|
// Normalize: remove trailing slash
|
||||||
|
_serverUrl = url.replace(/\/+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a path to a full URL.
|
||||||
|
* - In web mode: returns the path as-is (relative URLs work with Vite proxy / reverse proxy)
|
||||||
|
* - In Tauri mode: prepends the configured server URL
|
||||||
|
*/
|
||||||
|
export function resolveUrl(path: string): string {
|
||||||
|
if (!isTauri) return path
|
||||||
|
if (!_serverUrl) {
|
||||||
|
console.warn('[Tauri] No server URL configured, using path as-is:', path)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
// If already absolute, return as-is
|
||||||
|
if (path.startsWith('http://') || path.startsWith('https://')) return path
|
||||||
|
return `${_serverUrl}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch wrapper that resolves relative API paths in Tauri mode.
|
||||||
|
* In Tauri, uses @tauri-apps/plugin-http for proper CORS-free requests.
|
||||||
|
* In web, delegates to native fetch.
|
||||||
|
*/
|
||||||
|
export async function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
|
||||||
|
if (!isTauri) {
|
||||||
|
return fetch(input, init)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve URL
|
||||||
|
const url = typeof input === 'string' ? resolveUrl(input) : input
|
||||||
|
|
||||||
|
// Use Tauri HTTP plugin for cross-origin requests
|
||||||
|
try {
|
||||||
|
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
|
||||||
|
return tauriFetch(url, init)
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to native fetch if plugin fails
|
||||||
|
console.warn('[Tauri] HTTP plugin failed, falling back to fetch:', e)
|
||||||
|
return fetch(url, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic plugin imports (only used behind isTauri checks)
|
||||||
|
export async function getTauriStore() {
|
||||||
|
const { LazyStore } = await import('@tauri-apps/plugin-store')
|
||||||
|
return new LazyStore('settings.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTauriNotification() {
|
||||||
|
return import('@tauri-apps/plugin-notification')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTauriClipboard() {
|
||||||
|
return import('@tauri-apps/plugin-clipboard-manager')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTauriDialog() {
|
||||||
|
return import('@tauri-apps/plugin-dialog')
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import './styles/main.css'
|
import './styles/main.css'
|
||||||
|
import { isTauri } from './lib/tauri'
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@@ -14,9 +15,23 @@ app.use(router)
|
|||||||
;(window as any).__vueApp = app
|
;(window as any).__vueApp = app
|
||||||
;(window as any).__pinia = pinia
|
;(window as any).__pinia = pinia
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
// In Tauri mode, load server config before anything else
|
||||||
|
if (isTauri) {
|
||||||
|
const { useServerConfig } = await import('./stores/server-config')
|
||||||
|
const serverConfig = useServerConfig(pinia)
|
||||||
|
await serverConfig.loadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
// Inicializar tema antes de montar la app
|
// Inicializar tema antes de montar la app
|
||||||
import { useThemeStore } from './stores/theme'
|
const { useThemeStore } = await import('./stores/theme')
|
||||||
const themeStore = useThemeStore(pinia)
|
const themeStore = useThemeStore(pinia)
|
||||||
themeStore.fetchThemes().then(() => {
|
await themeStore.fetchThemes().catch(() => {
|
||||||
app.mount('#app')
|
// In Tauri without server configured, themes will fail — mount anyway
|
||||||
|
console.warn('[Main] Failed to fetch themes, mounting app anyway')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useGitApi } from '@/composables/git'
|
import { useGitApi } from '@/composables/git'
|
||||||
import { DiffViewer, FileTree, CommitList, BranchSelector, ProjectTree, FileViewer, StatusTree } from '@/components/git'
|
import { DiffViewer, FileTree, CommitList, BranchSelector, ProjectTree, FileViewer, StatusTree } from '@/components/git'
|
||||||
import { endpoints } from '@/config/endpoints'
|
import { resolveEndpoints } from '@/config/endpoints'
|
||||||
|
|
||||||
type TabName = 'status' | 'history' | 'compare' | 'files'
|
type TabName = 'status' | 'history' | 'compare' | 'files'
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const isRealtime = ref(false)
|
|||||||
function connectGitWatcher() {
|
function connectGitWatcher() {
|
||||||
if (gitSocket?.readyState === WebSocket.OPEN) return
|
if (gitSocket?.readyState === WebSocket.OPEN) return
|
||||||
|
|
||||||
gitSocket = new WebSocket(endpoints.git)
|
gitSocket = new WebSocket(resolveEndpoints().git)
|
||||||
|
|
||||||
gitSocket.onopen = () => {
|
gitSocket.onopen = () => {
|
||||||
isRealtime.value = true
|
isRealtime.value = true
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
name: string
|
name: string
|
||||||
@@ -82,7 +83,7 @@ async function connect() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Test connection and get repo info
|
// Test connection and get repo info
|
||||||
const res = await fetch('/api/gitea/repo', {
|
const res = await apiFetch('/api/gitea/repo', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -119,7 +120,7 @@ async function connect() {
|
|||||||
|
|
||||||
async function loadFileTree(path: string = '') {
|
async function loadFileTree(path: string = '') {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/gitea/tree', {
|
const res = await apiFetch('/api/gitea/tree', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -184,7 +185,7 @@ async function selectFile(node: FileNode) {
|
|||||||
fileContent.value = ''
|
fileContent.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/gitea/file', {
|
const res = await apiFetch('/api/gitea/file', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useCanvasStore } from '../stores/canvas'
|
|||||||
import { useThemeStore } from '../stores/theme'
|
import { useThemeStore } from '../stores/theme'
|
||||||
import { useWindowsStore } from '../stores/windows'
|
import { useWindowsStore } from '../stores/windows'
|
||||||
import WindowContainer from '../components/WindowContainer.vue'
|
import WindowContainer from '../components/WindowContainer.vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_URL = ''
|
const API_URL = ''
|
||||||
@@ -128,7 +129,7 @@ export const componentsApi = {
|
|||||||
if (opts?.includeArchived) params.set('include_archived', 'true')
|
if (opts?.includeArchived) params.set('include_archived', 'true')
|
||||||
if (opts?.limit) params.set('limit', String(opts.limit))
|
if (opts?.limit) params.set('limit', String(opts.limit))
|
||||||
const qs = params.toString()
|
const qs = params.toString()
|
||||||
const res = await fetch(`${API_URL}/api/components${qs ? '?' + qs : ''}`)
|
const res = await apiFetch(`${API_URL}/api/components${qs ? '?' + qs : ''}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.map((row: any) => ({
|
return data.map((row: any) => ({
|
||||||
...row,
|
...row,
|
||||||
@@ -139,7 +140,7 @@ export const componentsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getById(id: string): Promise<VueComponentDefinition | null> {
|
async getById(id: string): Promise<VueComponentDefinition | null> {
|
||||||
const res = await fetch(`${API_URL}/api/components/${id}`)
|
const res = await apiFetch(`${API_URL}/api/components/${id}`)
|
||||||
if (!res.ok) return null
|
if (!res.ok) return null
|
||||||
const row = await res.json()
|
const row = await res.json()
|
||||||
return {
|
return {
|
||||||
@@ -151,7 +152,7 @@ export const componentsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(component: VueComponentDefinition): Promise<{ success: boolean; id: string }> {
|
async save(component: VueComponentDefinition): Promise<{ success: boolean; id: string }> {
|
||||||
const res = await fetch(`${API_URL}/api/components`, {
|
const res = await apiFetch(`${API_URL}/api/components`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(component)
|
body: JSON.stringify(component)
|
||||||
@@ -160,7 +161,7 @@ export const componentsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async update(id: string, data: Partial<VueComponentDefinition>): Promise<{ success: boolean }> {
|
async update(id: string, data: Partial<VueComponentDefinition>): Promise<{ success: boolean }> {
|
||||||
const res = await fetch(`${API_URL}/api/components/${id}`, {
|
const res = await apiFetch(`${API_URL}/api/components/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -173,12 +174,12 @@ export const componentsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async delete(id: string): Promise<{ success: boolean; warning?: string }> {
|
async delete(id: string): Promise<{ success: boolean; warning?: string }> {
|
||||||
const res = await fetch(`${API_URL}/api/components/${id}`, { method: 'DELETE' })
|
const res = await apiFetch(`${API_URL}/api/components/${id}`, { method: 'DELETE' })
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async archiveAll(): Promise<{ success: boolean }> {
|
async archiveAll(): Promise<{ success: boolean }> {
|
||||||
const res = await fetch(`${API_URL}/api/components`, { method: 'DELETE' })
|
const res = await apiFetch(`${API_URL}/api/components`, { method: 'DELETE' })
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { endpoints } from '@/config/endpoints'
|
import { resolveEndpoints } from '@/config/endpoints'
|
||||||
import { useSessionState } from '@/stores/session-state'
|
import { useSessionState } from '@/stores/session-state'
|
||||||
|
|
||||||
let ws: WebSocket | null = null
|
let ws: WebSocket | null = null
|
||||||
@@ -10,7 +10,7 @@ function connect() {
|
|||||||
const store = useSessionState()
|
const store = useSessionState()
|
||||||
|
|
||||||
// Connect to terminal server (4103) — same base as terminal WS
|
// Connect to terminal server (4103) — same base as terminal WS
|
||||||
const url = endpoints.terminal
|
const url = resolveEndpoints().terminal
|
||||||
console.log('[SessionStateWS] Connecting to', url)
|
console.log('[SessionStateWS] Connecting to', url)
|
||||||
|
|
||||||
ws = new WebSocket(url)
|
ws = new WebSocket(url)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ThemeVariables, Theme, DesignTokens } from '../stores/theme'
|
import type { ThemeVariables, Theme, DesignTokens } from '../stores/theme'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_URL = ''
|
const API_URL = ''
|
||||||
@@ -9,24 +10,24 @@ const API_URL = ''
|
|||||||
|
|
||||||
export const themesApi = {
|
export const themesApi = {
|
||||||
async getAll(): Promise<Theme[]> {
|
async getAll(): Promise<Theme[]> {
|
||||||
const res = await fetch(`${API_URL}/api/themes`)
|
const res = await apiFetch(`${API_URL}/api/themes`)
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async getById(id: string): Promise<Theme | null> {
|
async getById(id: string): Promise<Theme | null> {
|
||||||
const res = await fetch(`${API_URL}/api/themes/${id}`)
|
const res = await apiFetch(`${API_URL}/api/themes/${id}`)
|
||||||
if (!res.ok) return null
|
if (!res.ok) return null
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActive(): Promise<Theme | null> {
|
async getActive(): Promise<Theme | null> {
|
||||||
const res = await fetch(`${API_URL}/api/themes/active`)
|
const res = await apiFetch(`${API_URL}/api/themes/active`)
|
||||||
if (!res.ok) return null
|
if (!res.ok) return null
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(theme: Partial<Theme>): Promise<{ success: boolean; id: string }> {
|
async save(theme: Partial<Theme>): Promise<{ success: boolean; id: string }> {
|
||||||
const res = await fetch(`${API_URL}/api/themes`, {
|
const res = await apiFetch(`${API_URL}/api/themes`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(theme)
|
body: JSON.stringify(theme)
|
||||||
@@ -35,17 +36,17 @@ export const themesApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async delete(id: string): Promise<{ success: boolean; error?: string }> {
|
async delete(id: string): Promise<{ success: boolean; error?: string }> {
|
||||||
const res = await fetch(`${API_URL}/api/themes/${id}`, { method: 'DELETE' })
|
const res = await apiFetch(`${API_URL}/api/themes/${id}`, { method: 'DELETE' })
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async setDefault(id: string): Promise<{ success: boolean }> {
|
async setDefault(id: string): Promise<{ success: boolean }> {
|
||||||
const res = await fetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
const res = await apiFetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDesignTokens(): Promise<DesignTokens> {
|
async getDesignTokens(): Promise<DesignTokens> {
|
||||||
const res = await fetch(`${API_URL}/api/design-tokens`)
|
const res = await apiFetch(`${API_URL}/api/design-tokens`)
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ToolConfig } from './index'
|
import type { ToolConfig } from './index'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_BASE = ''
|
const API_BASE = ''
|
||||||
@@ -12,7 +13,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
|||||||
schema: { type: 'object', properties: {} },
|
schema: { type: 'object', properties: {} },
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/database/tables`)
|
const res = await apiFetch(`${API_BASE}/api/database/tables`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch tables')
|
if (!res.ok) throw new Error('Failed to fetch tables')
|
||||||
const tables = await res.json()
|
const tables = await res.json()
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
handler: async (args: { table: string }) => {
|
handler: async (args: { table: string }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/database/tables/${args.table}/schema`)
|
const res = await apiFetch(`${API_BASE}/api/database/tables/${args.table}/schema`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404) return `Tabla "${args.table}" no encontrada`
|
if (res.status === 404) return `Tabla "${args.table}" no encontrada`
|
||||||
throw new Error('Failed to fetch schema')
|
throw new Error('Failed to fetch schema')
|
||||||
@@ -81,7 +82,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
|||||||
const limit = Math.min(args.limit || 20, 100)
|
const limit = Math.min(args.limit || 20, 100)
|
||||||
const offset = args.offset || 0
|
const offset = args.offset || 0
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await apiFetch(
|
||||||
`${API_BASE}/api/database/tables/${args.table}/data?limit=${limit}&offset=${offset}`
|
`${API_BASE}/api/database/tables/${args.table}/data?limit=${limit}&offset=${offset}`
|
||||||
)
|
)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -118,7 +119,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
|||||||
schema: { type: 'object', properties: {} },
|
schema: { type: 'object', properties: {} },
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/database/stats`)
|
const res = await apiFetch(`${API_BASE}/api/database/stats`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch stats')
|
if (!res.ok) throw new Error('Failed to fetch stats')
|
||||||
const stats = await res.json()
|
const stats = await res.json()
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
handler: async (args: { query: string }) => {
|
handler: async (args: { query: string }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/database/query`, {
|
const res = await apiFetch(`${API_BASE}/api/database/query`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ query: args.query })
|
body: JSON.stringify({ query: args.query })
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ import {
|
|||||||
type VueComponentDefinition
|
type VueComponentDefinition
|
||||||
} from '../../dynamicComponents'
|
} from '../../dynamicComponents'
|
||||||
import { getWindowDefinitions } from './canvasHandlers'
|
import { getWindowDefinitions } from './canvasHandlers'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
const API_URL = ''
|
const API_URL = ''
|
||||||
|
|
||||||
export const fsComponentsApi = {
|
export const fsComponentsApi = {
|
||||||
async list(): Promise<VueComponentDefinition[]> {
|
async list(): Promise<VueComponentDefinition[]> {
|
||||||
const res = await fetch(`${API_URL}/api/fs-components`)
|
const res = await apiFetch(`${API_URL}/api/fs-components`)
|
||||||
if (!res.ok) throw new Error(`Failed to list fs-components: ${res.statusText}`)
|
if (!res.ok) throw new Error(`Failed to list fs-components: ${res.statusText}`)
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
async getByFolder(folder: string): Promise<VueComponentDefinition> {
|
async getByFolder(folder: string): Promise<VueComponentDefinition> {
|
||||||
const res = await fetch(`${API_URL}/api/fs-components/${encodeURIComponent(folder)}`)
|
const res = await apiFetch(`${API_URL}/api/fs-components/${encodeURIComponent(folder)}`)
|
||||||
if (!res.ok) throw new Error(`Component "${folder}" not found`)
|
if (!res.ok) throw new Error(`Component "${folder}" not found`)
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ToolConfig } from './index'
|
import type { ToolConfig } from './index'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
const API_BASE = ''
|
const API_BASE = ''
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ export function createGitHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/git/status`)
|
const res = await apiFetch(`${API_BASE}/api/git/status`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
return `Error: ${err.error}`
|
return `Error: ${err.error}`
|
||||||
@@ -83,7 +84,7 @@ export function createGitHandlers(): ToolConfig[] {
|
|||||||
if (args.staged) params.set('staged', 'true')
|
if (args.staged) params.set('staged', 'true')
|
||||||
if (args.file) params.set('file', args.file)
|
if (args.file) params.set('file', args.file)
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/api/git/diff?${params}`)
|
const res = await apiFetch(`${API_BASE}/api/git/diff?${params}`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
return `Error: ${err.error}`
|
return `Error: ${err.error}`
|
||||||
@@ -131,7 +132,7 @@ export function createGitHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
handler: async (args: { base: string; head: string }) => {
|
handler: async (args: { base: string; head: string }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/git/compare`, {
|
const res = await apiFetch(`${API_BASE}/api/git/compare`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(args)
|
body: JSON.stringify(args)
|
||||||
@@ -197,7 +198,7 @@ export function createGitHandlers(): ToolConfig[] {
|
|||||||
if (args.author) params.set('author', args.author)
|
if (args.author) params.set('author', args.author)
|
||||||
if (args.since) params.set('since', args.since)
|
if (args.since) params.set('since', args.since)
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/api/git/log?${params}`)
|
const res = await apiFetch(`${API_BASE}/api/git/log?${params}`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
return `Error: ${err.error}`
|
return `Error: ${err.error}`
|
||||||
@@ -235,7 +236,7 @@ export function createGitHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/git/branches`)
|
const res = await apiFetch(`${API_BASE}/api/git/branches`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
return `Error: ${err.error}`
|
return `Error: ${err.error}`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ToolConfig } from './index'
|
import type { ToolConfig } from './index'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
let routerInstance: any = null
|
let routerInstance: any = null
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/status')
|
const res = await apiFetch('/api/whisper/status')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return `Whisper GPU Status:\n` +
|
return `Whisper GPU Status:\n` +
|
||||||
` Enabled: ${data.enabled ? 'Yes' : 'No'}\n` +
|
` Enabled: ${data.enabled ? 'Yes' : 'No'}\n` +
|
||||||
@@ -257,7 +258,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/toggle', {
|
const res = await apiFetch('/api/whisper/toggle', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -287,7 +288,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/start', {
|
const res = await apiFetch('/api/whisper/start', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -315,7 +316,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
|||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/stop', {
|
const res = await apiFetch('/api/whisper/stop', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ToolConfig } from './index'
|
import type { ToolConfig } from './index'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_BASE = ''
|
const API_BASE = ''
|
||||||
@@ -34,7 +35,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/gitea/repo`, {
|
const res = await apiFetch(`${API_BASE}/api/gitea/repo`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(giteaCredentials)
|
body: JSON.stringify(giteaCredentials)
|
||||||
@@ -75,7 +76,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/gitea/tree`, {
|
const res = await apiFetch(`${API_BASE}/api/gitea/tree`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
||||||
@@ -131,7 +132,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/gitea/file`, {
|
const res = await apiFetch(`${API_BASE}/api/gitea/file`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...giteaCredentials, path: args.path })
|
body: JSON.stringify({ ...giteaCredentials, path: args.path })
|
||||||
@@ -177,7 +178,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treeRes = await fetch(`${API_BASE}/api/gitea/tree`, {
|
const treeRes = await apiFetch(`${API_BASE}/api/gitea/tree`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
||||||
@@ -199,7 +200,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
|||||||
|
|
||||||
for (const file of files.slice(0, maxFiles)) {
|
for (const file of files.slice(0, maxFiles)) {
|
||||||
try {
|
try {
|
||||||
const fileRes = await fetch(`${API_BASE}/api/gitea/file`, {
|
const fileRes = await apiFetch(`${API_BASE}/api/gitea/file`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...giteaCredentials, path: file.path })
|
body: JSON.stringify({ ...giteaCredentials, path: file.path })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTorchStore } from '../stores/torch'
|
import { useTorchStore } from '../stores/torch'
|
||||||
import { autoConnect, disconnectWebMCP } from './webmcp'
|
import { autoConnect, disconnectWebMCP } from './webmcp'
|
||||||
import { onTorchConnected, onTorchDisconnected } from './toolRegistry'
|
import { onTorchConnected, onTorchDisconnected } from './toolRegistry'
|
||||||
import { endpoints } from '../config/endpoints'
|
import { resolveEndpoints } from '../config/endpoints'
|
||||||
|
|
||||||
let torchWs: WebSocket | null = null
|
let torchWs: WebSocket | null = null
|
||||||
let clientId: string | null = null
|
let clientId: string | null = null
|
||||||
@@ -18,7 +18,7 @@ function connectToTorchServer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Torch] Connecting to server...')
|
console.log('[Torch] Connecting to server...')
|
||||||
torchWs = new WebSocket(endpoints.torch)
|
torchWs = new WebSocket(resolveEndpoints().torch)
|
||||||
|
|
||||||
torchWs.onopen = () => {
|
torchWs.onopen = () => {
|
||||||
console.log('[Torch] Connected to server')
|
console.log('[Torch] Connected to server')
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints'
|
import { endpoints, isSecure, wsProtocol, hostname, resolveEndpoints } from '../config/endpoints'
|
||||||
|
import { isTauri, getServerUrl } from '@/lib/tauri'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// WebMCP HTTP API base for direct token requests
|
// WebMCP HTTP API base for direct token requests (resolved dynamically for Tauri)
|
||||||
const WEBMCP_HTTP = endpoints.webmcpHttp
|
function getWebmcpHttp() { return resolveEndpoints().webmcpHttp }
|
||||||
|
|
||||||
let webmcpInstance: any = null
|
let webmcpInstance: any = null
|
||||||
const registeredTools = new Set<string>()
|
const registeredTools = new Set<string>()
|
||||||
const eventUnsubscribers: Array<() => void> = []
|
const eventUnsubscribers: Array<() => void> = []
|
||||||
|
|
||||||
const API_BASE = endpoints.api
|
function getApiBase() { return resolveEndpoints().api }
|
||||||
let tokenPollingInterval: number | null = null
|
let tokenPollingInterval: number | null = null
|
||||||
|
|
||||||
export async function initWebMCP() {
|
export async function initWebMCP() {
|
||||||
@@ -258,7 +260,7 @@ export function isToolRegistered(name: string): boolean {
|
|||||||
// Token polling functions
|
// Token polling functions
|
||||||
export async function checkForToken(): Promise<string | null> {
|
export async function checkForToken(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/webmcp-token`)
|
const res = await apiFetch(`${getApiBase()}/webmcp-token`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.token || null
|
return data.token || null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -268,7 +270,7 @@ export async function checkForToken(): Promise<string | null> {
|
|||||||
|
|
||||||
export async function clearToken(): Promise<void> {
|
export async function clearToken(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE}/webmcp-token`, { method: 'DELETE' })
|
await apiFetch(`${getApiBase()}/webmcp-token`, { method: 'DELETE' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -315,11 +317,11 @@ export async function requestToken(): Promise<string | null> {
|
|||||||
try {
|
try {
|
||||||
console.log('[WebMCP] Requesting token from server...')
|
console.log('[WebMCP] Requesting token from server...')
|
||||||
|
|
||||||
// In HTTPS mode, use Agent UI API as proxy (Traefik can't reach WebMCP directly)
|
// In HTTPS or Tauri mode, use Agent UI API as proxy
|
||||||
// In development, call WebMCP directly
|
// In development, call WebMCP directly
|
||||||
const url = isSecure ? `${API_BASE}/webmcp-request-token` : `${WEBMCP_HTTP}/token`
|
const url = (isSecure || isTauri) ? `${getApiBase()}/webmcp-request-token` : `${getWebmcpHttp()}/token`
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await apiFetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
@@ -392,9 +394,18 @@ export async function connectWithToken(token: string): Promise<boolean> {
|
|||||||
// Clear the pending token from server
|
// Clear the pending token from server
|
||||||
await clearToken()
|
await clearToken()
|
||||||
|
|
||||||
// If behind HTTPS/Traefik, modify token to use secure WebSocket
|
// Modify token for correct WebSocket URL based on environment
|
||||||
let finalToken = token
|
let finalToken = token
|
||||||
if (isSecure) {
|
if (isTauri) {
|
||||||
|
// In Tauri, rewrite WebSocket URL to match configured server
|
||||||
|
const parsed = parseToken(token)
|
||||||
|
if (parsed) {
|
||||||
|
const wsUrl = resolveEndpoints().webmcp
|
||||||
|
const modifiedToken = { server: wsUrl, token: parsed.token }
|
||||||
|
finalToken = btoa(JSON.stringify(modifiedToken))
|
||||||
|
console.log('[WebMCP] Modified token for Tauri:', wsUrl)
|
||||||
|
}
|
||||||
|
} else if (isSecure) {
|
||||||
const parsed = parseToken(token)
|
const parsed = parseToken(token)
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
// Replace ws://localhost:4102 with wss://hostname/ws/mcp
|
// Replace ws://localhost:4102 with wss://hostname/ws/mcp
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { endpoints } from '../config/endpoints'
|
import { resolveEndpoints } from '../config/endpoints'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
export type WhisperStatus = 'offline' | 'loading' | 'ready'
|
export type WhisperStatus = 'offline' | 'loading' | 'ready'
|
||||||
|
|
||||||
@@ -28,8 +29,8 @@ const listeners = new Set<TranscriptionCallback>()
|
|||||||
function connect() {
|
function connect() {
|
||||||
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return
|
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return
|
||||||
|
|
||||||
console.log('[WhisperSocket] Connecting to', endpoints.whisper)
|
console.log('[WhisperSocket] Connecting to', resolveEndpoints().whisper)
|
||||||
socket = new WebSocket(endpoints.whisper)
|
socket = new WebSocket(resolveEndpoints().whisper)
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (socket && socket.readyState !== WebSocket.OPEN) {
|
if (socket && socket.readyState !== WebSocket.OPEN) {
|
||||||
@@ -86,7 +87,7 @@ function scheduleReconnect() {
|
|||||||
|
|
||||||
async function checkStatusAndConnect() {
|
async function checkStatusAndConnect() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/status')
|
const res = await apiFetch('/api/whisper/status')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.running) {
|
if (data.running) {
|
||||||
connect()
|
connect()
|
||||||
@@ -148,7 +149,7 @@ export async function reconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/whisper/toggle', { method: 'POST' })
|
const res = await apiFetch('/api/whisper/toggle', { method: 'POST' })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.running) {
|
if (data.running) {
|
||||||
connect()
|
connect()
|
||||||
@@ -158,7 +159,7 @@ export async function reconnect() {
|
|||||||
for (let i = 0; i < 60; i++) {
|
for (let i = 0; i < 60; i++) {
|
||||||
await new Promise(r => setTimeout(r, 2000))
|
await new Promise(r => setTimeout(r, 2000))
|
||||||
try {
|
try {
|
||||||
const s = await fetch('/api/whisper/status')
|
const s = await apiFetch('/api/whisper/status')
|
||||||
const d = await s.json()
|
const d = await s.json()
|
||||||
if (d.running) {
|
if (d.running) {
|
||||||
connect()
|
connect()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// ── Existing types ──
|
// ── Existing types ──
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/agents')
|
const res = await apiFetch('/api/agents')
|
||||||
if (!res.ok) throw new Error('Failed to fetch agents')
|
if (!res.ok) throw new Error('Failed to fetch agents')
|
||||||
agents.value = await res.json()
|
agents.value = await res.json()
|
||||||
if (agents.value.length && !selectedAgentId.value) {
|
if (agents.value.length && !selectedAgentId.value) {
|
||||||
@@ -263,7 +264,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
async function loadFile(agentId: string, file: AgentFile) {
|
async function loadFile(agentId: string, file: AgentFile) {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/agents/file?path=${encodeURIComponent(file.path)}`)
|
const res = await apiFetch(`/api/agents/file?path=${encodeURIComponent(file.path)}`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
throw new Error(data.error || 'Failed to load file')
|
throw new Error(data.error || 'Failed to load file')
|
||||||
@@ -288,7 +289,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/agents/file', {
|
const res = await apiFetch('/api/agents/file', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -339,8 +340,8 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
try {
|
try {
|
||||||
const agentId = selectedAgentId.value || 'main'
|
const agentId = selectedAgentId.value || 'main'
|
||||||
const [configRes, knownRes] = await Promise.all([
|
const [configRes, knownRes] = await Promise.all([
|
||||||
fetch(`/api/agents/config?agentId=${encodeURIComponent(agentId)}`),
|
apiFetch(`/api/agents/config?agentId=${encodeURIComponent(agentId)}`),
|
||||||
fetch('/api/agents/known-tools')
|
apiFetch('/api/agents/known-tools')
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!configRes.ok) throw new Error('Failed to fetch config')
|
if (!configRes.ok) throw new Error('Failed to fetch config')
|
||||||
@@ -533,7 +534,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/api/agents/config/permissions', {
|
const res = await apiFetch('/api/agents/config/permissions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -558,8 +559,8 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const [mcpRes, configRes] = await Promise.all([
|
const [mcpRes, configRes] = await Promise.all([
|
||||||
fetch('/api/agents/mcp-json'),
|
apiFetch('/api/agents/mcp-json'),
|
||||||
fetch(`/api/agents/config?agentId=${encodeURIComponent(selectedAgentId.value || 'main')}`)
|
apiFetch(`/api/agents/config?agentId=${encodeURIComponent(selectedAgentId.value || 'main')}`)
|
||||||
])
|
])
|
||||||
if (!mcpRes.ok) throw new Error('Failed to fetch MCP config')
|
if (!mcpRes.ok) throw new Error('Failed to fetch MCP config')
|
||||||
|
|
||||||
@@ -604,7 +605,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
mcpServersObj[s.name] = entry
|
mcpServersObj[s.name] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/api/agents/config/mcp', {
|
const res = await apiFetch('/api/agents/config/mcp', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ mcpServers: mcpServersObj })
|
body: JSON.stringify({ mcpServers: mcpServersObj })
|
||||||
@@ -636,7 +637,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/agents/config/hooks', {
|
const res = await apiFetch('/api/agents/config/hooks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -692,7 +693,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const agentId = selectedAgentId.value || 'main'
|
const agentId = selectedAgentId.value || 'main'
|
||||||
const res = await fetch(`/api/agents/skills?agentId=${encodeURIComponent(agentId)}`)
|
const res = await apiFetch(`/api/agents/skills?agentId=${encodeURIComponent(agentId)}`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch skills')
|
if (!res.ok) throw new Error('Failed to fetch skills')
|
||||||
skills.value = await res.json()
|
skills.value = await res.json()
|
||||||
if (skills.value.length && !selectedSkill.value) {
|
if (skills.value.length && !selectedSkill.value) {
|
||||||
@@ -711,7 +712,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|||||||
pluginsLoading.value = true
|
pluginsLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/agents/plugins')
|
const res = await apiFetch('/api/agents/plugins')
|
||||||
if (!res.ok) throw new Error('Failed to fetch plugins')
|
if (!res.ok) throw new Error('Failed to fetch plugins')
|
||||||
plugins.value = await res.json()
|
plugins.value = await res.json()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
export interface HookNotification {
|
export interface HookNotification {
|
||||||
id: string
|
id: string
|
||||||
@@ -172,7 +173,7 @@ export const useClaudeHooksStore = defineStore('claude-hooks', () => {
|
|||||||
|
|
||||||
async function respondPermission(notifId: string, requestId: string, decision: 'allow' | 'deny') {
|
async function respondPermission(notifId: string, requestId: string, decision: 'allow' | 'deny') {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/claude-permission-respond', {
|
await apiFetch('/api/claude-permission-respond', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision })
|
body: JSON.stringify({ requestId, decision })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas'
|
import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
@@ -51,7 +52,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const url = includeArchived ? `${API_URL}/api/canvas?include_archived=true` : `${API_URL}/api/canvas`
|
const url = includeArchived ? `${API_URL}/api/canvas?include_archived=true` : `${API_URL}/api/canvas`
|
||||||
const res = await fetch(url)
|
const res = await apiFetch(url)
|
||||||
if (!res.ok) throw new Error('Failed to fetch canvases')
|
if (!res.ok) throw new Error('Failed to fetch canvases')
|
||||||
canvases.value = await res.json()
|
canvases.value = await res.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -64,7 +65,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
|
|
||||||
async function fetchToolbarCanvases() {
|
async function fetchToolbarCanvases() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/toolbar`)
|
const res = await apiFetch(`${API_URL}/api/canvas/toolbar`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch toolbar canvases')
|
if (!res.ok) throw new Error('Failed to fetch toolbar canvases')
|
||||||
toolbarCanvases.value = await res.json()
|
toolbarCanvases.value = await res.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -75,7 +76,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
|
|
||||||
async function fetchDefaultCanvas(): Promise<ProjectCanvas | null> {
|
async function fetchDefaultCanvas(): Promise<ProjectCanvas | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/default`)
|
const res = await apiFetch(`${API_URL}/api/canvas/default`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch default canvas')
|
if (!res.ok) throw new Error('Failed to fetch default canvas')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.hasDefault) {
|
if (data.hasDefault) {
|
||||||
@@ -93,7 +94,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
|
|
||||||
async function fetchCanvasById(id: string): Promise<ProjectCanvas | null> {
|
async function fetchCanvasById(id: string): Promise<ProjectCanvas | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${id}`)
|
const res = await apiFetch(`${API_URL}/api/canvas/${id}`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404) return null
|
if (res.status === 404) return null
|
||||||
throw new Error('Failed to fetch canvas')
|
throw new Error('Failed to fetch canvas')
|
||||||
@@ -109,7 +110,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas`, {
|
const res = await apiFetch(`${API_URL}/api/canvas`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -131,7 +132,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${id}`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -158,7 +159,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
async function deleteCanvas(id: string): Promise<boolean> {
|
async function deleteCanvas(id: string): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${id}`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${id}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -185,7 +186,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${id}/clone`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${id}/clone`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: newName })
|
body: JSON.stringify({ name: newName })
|
||||||
@@ -207,7 +208,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
async function fetchCanvasComponents(canvasId: string) {
|
async function fetchCanvasComponents(canvasId: string) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components`)
|
const res = await apiFetch(`${API_URL}/api/canvas/${canvasId}/components`)
|
||||||
if (!res.ok) throw new Error('Failed to fetch canvas components')
|
if (!res.ok) throw new Error('Failed to fetch canvas components')
|
||||||
activeCanvasComponents.value = await res.json()
|
activeCanvasComponents.value = await res.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -225,7 +226,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
position?: number
|
position?: number
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${canvasId}/components`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ component_id: componentId, props, position })
|
body: JSON.stringify({ component_id: componentId, props, position })
|
||||||
@@ -241,7 +242,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
|
|
||||||
async function removeComponentFromCanvas(canvasId: string, componentId: string): Promise<boolean> {
|
async function removeComponentFromCanvas(canvasId: string, componentId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error('Failed to remove component from canvas')
|
if (!res.ok) throw new Error('Failed to remove component from canvas')
|
||||||
@@ -259,7 +260,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
data: { position?: number; props?: Record<string, any>; layout?: any; is_visible?: boolean }
|
data: { position?: number; props?: Record<string, any>; layout?: any; is_visible?: boolean }
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, {
|
const res = await apiFetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -276,7 +277,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
|||||||
// Component Usage
|
// Component Usage
|
||||||
async function getComponentUsage(componentId: string): Promise<ComponentUsage | null> {
|
async function getComponentUsage(componentId: string): Promise<ComponentUsage | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/components/${componentId}/usage`)
|
const res = await apiFetch(`${API_URL}/api/components/${componentId}/usage`)
|
||||||
if (!res.ok) throw new Error('Failed to get component usage')
|
if (!res.ok) throw new Error('Failed to get component usage')
|
||||||
return await res.json()
|
return await res.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
109
frontend/src/stores/server-config.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { isTauri, getTauriStore, setServerUrl } from '@/lib/tauri'
|
||||||
|
|
||||||
|
export const useServerConfig = defineStore('server-config', () => {
|
||||||
|
const serverUrl = ref('')
|
||||||
|
const isConfigured = computed(() => !!serverUrl.value)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const recentUrls = ref<string[]>([])
|
||||||
|
|
||||||
|
/** Load saved config from Tauri store */
|
||||||
|
async function loadConfig() {
|
||||||
|
if (!isTauri) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const store = await getTauriStore()
|
||||||
|
const saved = await store.get<string>('serverUrl')
|
||||||
|
if (saved) {
|
||||||
|
serverUrl.value = saved
|
||||||
|
setServerUrl(saved)
|
||||||
|
}
|
||||||
|
const savedRecent = await store.get<string[]>('recentUrls')
|
||||||
|
if (savedRecent) {
|
||||||
|
recentUrls.value = savedRecent
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ServerConfig] Failed to load config:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test connection to a server URL */
|
||||||
|
async function testConnection(url: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const normalizedUrl = url.replace(/\/+$/, '')
|
||||||
|
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
|
||||||
|
const res = await tauriFetch(`${normalizedUrl}/api/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
return res.ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set and persist the server URL (validates connectivity first) */
|
||||||
|
async function setServer(url: string): Promise<boolean> {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const normalizedUrl = url.replace(/\/+$/, '')
|
||||||
|
const ok = await testConnection(normalizedUrl)
|
||||||
|
if (!ok) {
|
||||||
|
error.value = 'Could not connect to server'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
serverUrl.value = normalizedUrl
|
||||||
|
setServerUrl(normalizedUrl)
|
||||||
|
|
||||||
|
// Persist to Tauri store
|
||||||
|
const store = await getTauriStore()
|
||||||
|
await store.set('serverUrl', normalizedUrl)
|
||||||
|
|
||||||
|
// Update recent URLs
|
||||||
|
const filtered = recentUrls.value.filter(u => u !== normalizedUrl)
|
||||||
|
recentUrls.value = [normalizedUrl, ...filtered].slice(0, 5)
|
||||||
|
await store.set('recentUrls', recentUrls.value)
|
||||||
|
await store.save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || 'Unknown error'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the saved configuration */
|
||||||
|
async function clearConfig() {
|
||||||
|
serverUrl.value = ''
|
||||||
|
setServerUrl('')
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
if (isTauri) {
|
||||||
|
try {
|
||||||
|
const store = await getTauriStore()
|
||||||
|
await store.delete('serverUrl')
|
||||||
|
await store.save()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ServerConfig] Failed to clear config:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverUrl,
|
||||||
|
isConfigured,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
recentUrls,
|
||||||
|
loadConfig,
|
||||||
|
testConnection,
|
||||||
|
setServer,
|
||||||
|
clearConfig,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import { terminalApiUrl } from '../config/endpoints'
|
import { terminalApiUrl } from '../config/endpoints'
|
||||||
|
|
||||||
// ── Types (mirror server/services/session-state.ts) ──
|
// ── Types (mirror server/services/session-state.ts) ──
|
||||||
@@ -196,7 +197,7 @@ export const useSessionState = defineStore('session-state', () => {
|
|||||||
// ── Actions ──
|
// ── Actions ──
|
||||||
|
|
||||||
async function respondApproval(requestId: string, decision: string, reason?: string) {
|
async function respondApproval(requestId: string, decision: string, reason?: string) {
|
||||||
await fetch('/api/hooks-approval/respond', {
|
await apiFetch('/api/hooks-approval/respond', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision, reason })
|
body: JSON.stringify({ requestId, decision, reason })
|
||||||
@@ -204,7 +205,7 @@ export const useSessionState = defineStore('session-state', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function respondPlanApproval(requestId: string, decision: string, reason?: string) {
|
async function respondPlanApproval(requestId: string, decision: string, reason?: string) {
|
||||||
await fetch('/api/hooks-approval/respond-plan', {
|
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ requestId, decision, reason })
|
body: JSON.stringify({ requestId, decision, reason })
|
||||||
@@ -213,7 +214,7 @@ export const useSessionState = defineStore('session-state', () => {
|
|||||||
|
|
||||||
async function refreshAgentState(agent: string) {
|
async function refreshAgentState(agent: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(terminalApiUrl(`/session-state/${agent}`))
|
const res = await apiFetch(terminalApiUrl(`/session-state/${agent}`))
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
const state = await res.json() as AgentSessionState
|
const state = await res.json() as AgentSessionState
|
||||||
agents.value = { ...agents.value, [agent]: state }
|
agents.value = { ...agents.value, [agent]: state }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
import { useWindowsStore } from './windows'
|
import { useWindowsStore } from './windows'
|
||||||
import {
|
import {
|
||||||
getScriptLog,
|
getScriptLog,
|
||||||
@@ -142,7 +143,7 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
|||||||
async function save(name: string): Promise<string> {
|
async function save(name: string): Promise<string> {
|
||||||
const snapshot = captureState(name)
|
const snapshot = captureState(name)
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/api/snapshots`, {
|
const res = await apiFetch(`${API_URL}/api/snapshots`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -159,7 +160,7 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
|||||||
|
|
||||||
// ---- List snapshots ----
|
// ---- List snapshots ----
|
||||||
async function list(): Promise<SnapshotSummary[]> {
|
async function list(): Promise<SnapshotSummary[]> {
|
||||||
const res = await fetch(`${API_URL}/api/snapshots`)
|
const res = await apiFetch(`${API_URL}/api/snapshots`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
snapshots.value = data
|
snapshots.value = data
|
||||||
return data
|
return data
|
||||||
@@ -167,14 +168,14 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
|||||||
|
|
||||||
// ---- Load full snapshot ----
|
// ---- Load full snapshot ----
|
||||||
async function load(id: string): Promise<CanvasSnapshot> {
|
async function load(id: string): Promise<CanvasSnapshot> {
|
||||||
const res = await fetch(`${API_URL}/api/snapshots/${id}`)
|
const res = await apiFetch(`${API_URL}/api/snapshots/${id}`)
|
||||||
const row = await res.json()
|
const row = await res.json()
|
||||||
return row.data
|
return row.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Remove snapshot ----
|
// ---- Remove snapshot ----
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
await fetch(`${API_URL}/api/snapshots/${id}`, { method: 'DELETE' })
|
await apiFetch(`${API_URL}/api/snapshots/${id}`, { method: 'DELETE' })
|
||||||
await list()
|
await list()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiFetch } from '@/lib/tauri'
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Types
|
// Types
|
||||||
@@ -95,7 +96,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/themes`)
|
const res = await apiFetch(`${API_URL}/api/themes`)
|
||||||
themes.value = await res.json()
|
themes.value = await res.json()
|
||||||
|
|
||||||
const defaultTheme = themes.value.find(t => t.is_default)
|
const defaultTheme = themes.value.find(t => t.is_default)
|
||||||
@@ -113,7 +114,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
|
|
||||||
async function fetchDesignTokens() {
|
async function fetchDesignTokens() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/design-tokens`)
|
const res = await apiFetch(`${API_URL}/api/design-tokens`)
|
||||||
designTokens.value = await res.json()
|
designTokens.value = await res.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching design tokens:', e)
|
console.error('Error fetching design tokens:', e)
|
||||||
@@ -123,7 +124,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
async function saveTheme(theme: Partial<Theme> & { name: string; variables: ThemeVariables }) {
|
async function saveTheme(theme: Partial<Theme> & { name: string; variables: ThemeVariables }) {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/themes`, {
|
const res = await apiFetch(`${API_URL}/api/themes`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(theme)
|
body: JSON.stringify(theme)
|
||||||
@@ -142,7 +143,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
async function updateTheme(id: string, data: { name?: string; description?: string; variables?: ThemeVariables; metadata?: ThemeMetadata }) {
|
async function updateTheme(id: string, data: { name?: string; description?: string; variables?: ThemeVariables; metadata?: ThemeMetadata }) {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/themes/${id}`, {
|
const res = await apiFetch(`${API_URL}/api/themes/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -164,7 +165,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
|
|
||||||
async function deleteTheme(id: string) {
|
async function deleteTheme(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/themes/${id}`, { method: 'DELETE' })
|
const res = await apiFetch(`${API_URL}/api/themes/${id}`, { method: 'DELETE' })
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
@@ -180,7 +181,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
|
|
||||||
async function setDefaultTheme(id: string) {
|
async function setDefaultTheme(id: string) {
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
await apiFetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
||||||
await fetchThemes()
|
await fetchThemes()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = 'Error setting default theme'
|
error.value = 'Error setting default theme'
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import path from 'path'
|
|||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
const isTauri = !!process.env.TAURI_ENV_PLATFORM
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
envPrefix: ['VITE_', 'TAURI_ENV_'],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
@@ -16,7 +19,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
VitePWA({
|
// PWA only in web builds (not Tauri)
|
||||||
|
...(!isTauri ? [VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
includeAssets: ['favicon.svg', 'icons/*.svg', 'icons/*.png'],
|
includeAssets: ['favicon.svg', 'icons/*.svg', 'icons/*.png'],
|
||||||
devOptions: {
|
devOptions: {
|
||||||
@@ -67,7 +71,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})] : [])
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
port: 4100,
|
port: 4100,
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
"start": "bun run kill-ports && concurrently -n api,terminal,frontend -c blue,yellow,green \"cd server && bun --watch run index.ts\" \"cd server && bun run terminal.ts\" \"cd frontend && bun run dev --host\"",
|
"start": "bun run kill-ports && concurrently -n api,terminal,frontend -c blue,yellow,green \"cd server && bun --watch run index.ts\" \"cd server && bun run terminal.ts\" \"cd frontend && bun run dev --host\"",
|
||||||
"start:api": "cd server && bun --watch run index.ts",
|
"start:api": "cd server && bun --watch run index.ts",
|
||||||
"start:terminal": "cd server && bun run terminal.ts",
|
"start:terminal": "cd server && bun run terminal.ts",
|
||||||
"start:frontend": "cd frontend && bun run dev --host"
|
"start:frontend": "cd frontend && bun run dev --host",
|
||||||
|
"tauri": "npx --prefix frontend tauri",
|
||||||
|
"tauri:dev": "npx --prefix frontend tauri dev",
|
||||||
|
"tauri:build": "npx --prefix frontend tauri build",
|
||||||
|
"tauri:android:init": "npx --prefix frontend tauri android init",
|
||||||
|
"tauri:android:build": "npx --prefix frontend tauri android build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.2.1"
|
"concurrently": "^9.2.1"
|
||||||
|
|||||||
6235
src-tauri/Cargo.lock
generated
Normal file
23
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent-ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Agent UI - Desktop & Mobile App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "agent_ui_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-http = "2"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
tauri-plugin-clipboard-manager = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
20
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default permissions for Agent UI",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "http://**" },
|
||||||
|
{ "url": "https://**" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"store:default",
|
||||||
|
"notification:default",
|
||||||
|
"clipboard-manager:default",
|
||||||
|
"dialog:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
97
src-tauri/gen/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("rust")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tauriProperties = Properties().apply {
|
||||||
|
val propFile = file("tauri.properties")
|
||||||
|
if (propFile.exists()) {
|
||||||
|
propFile.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file("../keystore.jks")
|
||||||
|
storePassword = "agentui123"
|
||||||
|
keyAlias = "agentui"
|
||||||
|
keyPassword = "agentui123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileSdk = 36
|
||||||
|
namespace = "com.agentui.desktop"
|
||||||
|
defaultConfig {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
|
applicationId = "com.agentui.desktop"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
|
isDebuggable = true
|
||||||
|
isJniDebuggable = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
*fileTree(".") { include("**/*.pro") }
|
||||||
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rust {
|
||||||
|
rootDirRel = "../../../../"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.webkit:webkit:1.14.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "tauri.build.gradle.kts")
|
||||||
|
|
||||||
|
// Copy APK to src-tauri/installers after build
|
||||||
|
android.applicationVariants.all {
|
||||||
|
val variant = this
|
||||||
|
variant.outputs.all {
|
||||||
|
val output = this
|
||||||
|
variant.assembleProvider.get().doLast {
|
||||||
|
val src = output.outputFile
|
||||||
|
if (src.exists()) {
|
||||||
|
val dest = file("../../../../installers/AgentUI-${variant.versionName}-${variant.name}.apk")
|
||||||
|
src.copyTo(dest, overwrite = true)
|
||||||
|
println(">> Copied APK to ${dest.absolutePath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.agent_ui"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Transcript Widget -->
|
||||||
|
<receiver
|
||||||
|
android:name=".TranscriptWidgetProvider"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/transcript_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Voice Command / Share Activity -->
|
||||||
|
<activity
|
||||||
|
android:name=".VoiceCommandActivity"
|
||||||
|
android:label="Agent UI Voice"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.agentui.desktop
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class MainActivity : TauriActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
syncServerUrlToPrefs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
syncServerUrlToPrefs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the Tauri plugin-store settings.json and syncs serverUrl
|
||||||
|
* to SharedPreferences so native components (widget, voice) can use it.
|
||||||
|
*/
|
||||||
|
private fun syncServerUrlToPrefs() {
|
||||||
|
try {
|
||||||
|
val storeFile = File(filesDir, "app_tauri-plugin-store/settings.json")
|
||||||
|
if (!storeFile.exists()) return
|
||||||
|
|
||||||
|
val json = JSONObject(storeFile.readText())
|
||||||
|
val serverUrl = json.optString("serverUrl", "")
|
||||||
|
if (serverUrl.isNotEmpty()) {
|
||||||
|
ServerConfig.setServerUrl(this, serverUrl)
|
||||||
|
Log.d("AgentUI", "Synced serverUrl to prefs: $serverUrl")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("AgentUI", "Failed to sync server URL", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.agentui.desktop
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object ServerConfig {
|
||||||
|
private const val PREFS_NAME = "agent_ui_config"
|
||||||
|
private const val KEY_SERVER_URL = "server_url"
|
||||||
|
|
||||||
|
fun getServerUrl(context: Context): String? {
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_SERVER_URL, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setServerUrl(context: Context, url: String) {
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_SERVER_URL, url.trimEnd('/'))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** e.g. "http://192.168.1.10:4103" */
|
||||||
|
fun terminalBaseUrl(context: Context): String? {
|
||||||
|
val base = getServerUrl(context) ?: return null
|
||||||
|
val uri = android.net.Uri.parse(base)
|
||||||
|
val host = uri.host ?: return null
|
||||||
|
return "${uri.scheme}://$host:4103"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** e.g. "ws://192.168.1.10:4103" */
|
||||||
|
fun terminalWsUrl(context: Context): String? {
|
||||||
|
val base = terminalBaseUrl(context) ?: return null
|
||||||
|
return base.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** e.g. "http://192.168.1.10:4100/api" */
|
||||||
|
fun apiBaseUrl(context: Context): String? {
|
||||||
|
val base = getServerUrl(context) ?: return null
|
||||||
|
return "${base.trimEnd('/')}/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.agentui.desktop
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
|
||||||
|
class TranscriptWidgetProvider : AppWidgetProvider() {
|
||||||
|
|
||||||
|
override fun onUpdate(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetIds: IntArray
|
||||||
|
) {
|
||||||
|
for (appWidgetId in appWidgetIds) {
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.widget_transcript)
|
||||||
|
|
||||||
|
// Tap widget → open main app
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic refresh worker
|
||||||
|
TranscriptWidgetWorker.enqueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnabled(context: Context) {
|
||||||
|
TranscriptWidgetWorker.enqueue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisabled(context: Context) {
|
||||||
|
TranscriptWidgetWorker.cancel(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package com.agentui.desktop
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.work.*
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TranscriptWidgetWorker(
|
||||||
|
private val context: Context,
|
||||||
|
params: WorkerParameters
|
||||||
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val WORK_NAME = "transcript_widget_refresh"
|
||||||
|
private val MSG_IDS = intArrayOf(
|
||||||
|
R.id.msg1, R.id.msg2, R.id.msg3, R.id.msg4, R.id.msg5
|
||||||
|
)
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun enqueue(context: Context) {
|
||||||
|
val request = OneTimeWorkRequestBuilder<TranscriptWidgetWorker>()
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(context: Context) {
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val messages = fetchMessages()
|
||||||
|
updateWidget(messages)
|
||||||
|
scheduleNext()
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchMessages(): List<String> {
|
||||||
|
val apiBase = ServerConfig.apiBaseUrl(context) ?: return listOf("No server configured")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get most recent session for 'ejecutor' agent
|
||||||
|
val sessionsUrl = "$apiBase/transcript-debug/sessions?agent=ejecutor"
|
||||||
|
val sessionsReq = Request.Builder().url(sessionsUrl).build()
|
||||||
|
val sessionsResp = client.newCall(sessionsReq).execute()
|
||||||
|
if (!sessionsResp.isSuccessful) return listOf("Failed to fetch sessions")
|
||||||
|
|
||||||
|
val sessionsJson = JSONArray(sessionsResp.body?.string() ?: "[]")
|
||||||
|
if (sessionsJson.length() == 0) return listOf("No sessions found")
|
||||||
|
|
||||||
|
// Get the most recent session (sorted by mtime desc from server)
|
||||||
|
val latestSession = sessionsJson.getJSONObject(0)
|
||||||
|
val sessionId = latestSession.getString("id")
|
||||||
|
|
||||||
|
// Fetch raw JSONL
|
||||||
|
val rawUrl = "$apiBase/transcript-debug/$sessionId/raw?agent=ejecutor"
|
||||||
|
val rawReq = Request.Builder().url(rawUrl).build()
|
||||||
|
val rawResp = client.newCall(rawReq).execute()
|
||||||
|
if (!rawResp.isSuccessful) return listOf("Failed to fetch transcript")
|
||||||
|
|
||||||
|
val rawText = rawResp.body?.string() ?: return listOf("Empty transcript")
|
||||||
|
return parseJsonlMessages(rawText)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return listOf("Error: ${e.message?.take(40)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseJsonlMessages(jsonl: String): List<String> {
|
||||||
|
val messages = mutableListOf<String>()
|
||||||
|
val lines = jsonl.trim().split("\n").filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
try {
|
||||||
|
val obj = JSONObject(line)
|
||||||
|
val type = obj.optString("type", "")
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"human" -> {
|
||||||
|
val content = obj.optString("message", "")
|
||||||
|
.ifEmpty { extractMessageContent(obj) }
|
||||||
|
if (content.isNotEmpty()) {
|
||||||
|
messages.add("> ${content.take(80)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"assistant" -> {
|
||||||
|
val content = extractMessageContent(obj)
|
||||||
|
if (content.isNotEmpty()) {
|
||||||
|
messages.add("< ${content.take(80)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.takeLast(5).ifEmpty { listOf("No messages yet") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractMessageContent(obj: JSONObject): String {
|
||||||
|
// Try "message" field first (simple string)
|
||||||
|
val msg = obj.optString("message", "")
|
||||||
|
if (msg.isNotEmpty()) return msg
|
||||||
|
|
||||||
|
// Try "message" as array of content blocks
|
||||||
|
val msgArray = obj.optJSONArray("message")
|
||||||
|
if (msgArray != null) {
|
||||||
|
for (i in 0 until msgArray.length()) {
|
||||||
|
val block = msgArray.optJSONObject(i) ?: continue
|
||||||
|
if (block.optString("type") == "text") {
|
||||||
|
return block.optString("text", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWidget(messages: List<String>) {
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.widget_transcript)
|
||||||
|
|
||||||
|
for (i in MSG_IDS.indices) {
|
||||||
|
views.setTextViewText(MSG_IDS[i], messages.getOrElse(i) { "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val widgetComponent = ComponentName(context, TranscriptWidgetProvider::class.java)
|
||||||
|
appWidgetManager.updateAppWidget(widgetComponent, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleNext() {
|
||||||
|
val request = OneTimeWorkRequestBuilder<TranscriptWidgetWorker>()
|
||||||
|
.setInitialDelay(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.agentui.desktop
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.speech.RecognizerIntent
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class VoiceCommandActivity : Activity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "AgentUI.Voice"
|
||||||
|
private const val SPEECH_REQUEST_CODE = 1001
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Check if launched via share intent
|
||||||
|
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") {
|
||||||
|
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
if (!sharedText.isNullOrBlank()) {
|
||||||
|
sendToServer(sharedText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, start speech recognition
|
||||||
|
startSpeechRecognition()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSpeechRecognition() {
|
||||||
|
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||||
|
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||||
|
putExtra(RecognizerIntent.EXTRA_PROMPT, "Agent UI - Speak your command")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, SPEECH_REQUEST_CODE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, "Speech recognition not available", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (requestCode == SPEECH_REQUEST_CODE) {
|
||||||
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
|
val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||||
|
val text = results?.firstOrNull()
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
sendToServer(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendToServer(text: String) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val terminalBase = ServerConfig.terminalBaseUrl(this)
|
||||||
|
val terminalWs = ServerConfig.terminalWsUrl(this)
|
||||||
|
|
||||||
|
if (terminalBase == null || terminalWs == null) {
|
||||||
|
showToastAndFinish("No server configured")
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create a new terminal session
|
||||||
|
val createBody = JSONObject().apply {
|
||||||
|
put("agent", "ejecutor")
|
||||||
|
put("transcriptSessionId", "__new__")
|
||||||
|
put("label", text.take(60))
|
||||||
|
put("command", "ejecutor")
|
||||||
|
}.toString().toRequestBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val createReq = Request.Builder()
|
||||||
|
.url("$terminalBase/create-terminal")
|
||||||
|
.post(createBody)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val createResp = client.newCall(createReq).execute()
|
||||||
|
if (!createResp.isSuccessful) {
|
||||||
|
showToastAndFinish("Failed to create terminal")
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
val respJson = JSONObject(createResp.body?.string() ?: "{}")
|
||||||
|
val sessionId = respJson.optString("ephemeralSessionId", "")
|
||||||
|
if (sessionId.isEmpty()) {
|
||||||
|
showToastAndFinish("No session ID returned")
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Connect via WebSocket and send the input
|
||||||
|
val wsUrl = "$terminalWs/ws/terminal?session=$sessionId"
|
||||||
|
val wsReq = Request.Builder().url(wsUrl).build()
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
client.newWebSocket(wsReq, object : WebSocketListener() {
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
// Send the text as input
|
||||||
|
val inputMsg = JSONObject().apply {
|
||||||
|
put("type", "input")
|
||||||
|
put("data", text)
|
||||||
|
}.toString()
|
||||||
|
webSocket.send(inputMsg)
|
||||||
|
|
||||||
|
// Send Enter key after a short delay
|
||||||
|
Thread.sleep(80)
|
||||||
|
val enterMsg = JSONObject().apply {
|
||||||
|
put("type", "input")
|
||||||
|
put("data", "\r")
|
||||||
|
}.toString()
|
||||||
|
webSocket.send(enterMsg)
|
||||||
|
|
||||||
|
// Close after sending
|
||||||
|
Thread.sleep(200)
|
||||||
|
webSocket.close(1000, "done")
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
Log.e(TAG, "WebSocket failed", t)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
latch.await(5, TimeUnit.SECONDS)
|
||||||
|
showToastAndFinish("Sent: ${text.take(40)}")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to send command", e)
|
||||||
|
showToastAndFinish("Error: ${e.message?.take(40)}")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToastAndFinish(message: String) {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/widget_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="#DD1A1A2E">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Agent UI Transcript"
|
||||||
|
android:textColor="#8888FF"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/msg1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#CCCCCC"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/msg2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#CCCCCC"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/msg3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#CCCCCC"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/msg4"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#CCCCCC"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/msg5"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#CCCCCC"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Agent UI</string>
|
||||||
|
<string name="main_activity_title">Agent UI</string>
|
||||||
|
<string name="widget_description">Shows recent transcript messages from Agent UI</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="250dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:updatePeriodMillis="1800000"
|
||||||
|
android:initialLayout="@layout/widget_transcript"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen"
|
||||||
|
android:description="@string/widget_description" />
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 105 KiB |