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)
|
||||
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
|
||||
.claude-*/plugins/
|
||||
.claude-*/plans/
|
||||
|
||||
279
frontend/package-lock.json
generated
@@ -9,6 +9,12 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@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-web-links": "^0.12.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
@@ -19,6 +25,7 @@
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.10.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
@@ -2415,6 +2422,278 @@
|
||||
"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": {
|
||||
"version": "1.0.8",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -8,10 +8,25 @@
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"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": {
|
||||
"@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-web-links": "^0.12.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
@@ -22,6 +37,7 @@
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.10.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
|
||||
@@ -10,19 +10,27 @@ import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
|
||||
import TerminalFabStack from './components/transcript-debug/TerminalFabStack.vue'
|
||||
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
||||
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
||||
import ServerConfigDialog from './components/ServerConfigDialog.vue'
|
||||
import { useGlobalApproval } from './composables/useGlobalApproval'
|
||||
import { initWebMCP, getWebMCP } from './services/webmcp'
|
||||
import { initTorch, destroyTorch } from './services/torch'
|
||||
import { initSessionStateWS, destroySessionStateWS } from './services/session-state-ws'
|
||||
import { endpoints } from './config/endpoints'
|
||||
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||
import { setResponseControls } from './services/tools/handlers/responseHandlers'
|
||||
import { useCanvasStore } from './stores/canvas'
|
||||
import { useProjectCanvasStore } from './stores/projectCanvas'
|
||||
import { useSessionState } from './stores/session-state'
|
||||
import { isTauri } from './lib/tauri'
|
||||
import { useServerConfig } from './stores/server-config'
|
||||
|
||||
const route = useRoute()
|
||||
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 showTranscriptDebug = ref(false)
|
||||
const showDebugConsole = ref(false)
|
||||
@@ -293,6 +301,20 @@ watch(() => route.name, (newPage) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -325,7 +347,12 @@ watch(() => route.name, (newPage) => {
|
||||
</svg>
|
||||
<span v-if="debugLogs.length" class="log-count">{{ debugLogs.length }}</span>
|
||||
</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 class="header-right">
|
||||
<button
|
||||
@@ -427,6 +454,9 @@ watch(() => route.name, (newPage) => {
|
||||
<!-- Global Hooks Approval Modal -->
|
||||
<HooksApprovalModal />
|
||||
|
||||
<!-- Tauri Server Config Dialog -->
|
||||
<ServerConfigDialog v-if="needsServerConfig || showServerConfig" />
|
||||
|
||||
<!-- Debug Console Panel -->
|
||||
<Teleport to="body">
|
||||
<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-btn {
|
||||
display: flex;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
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)
|
||||
interface SpeechRecognitionEvent extends Event {
|
||||
@@ -64,7 +65,7 @@ const containerRef = ref<HTMLElement | null>(null)
|
||||
let recognition: SpeechRecognition | null = null as SpeechRecognition | null
|
||||
|
||||
// WebSocket connection to terminal
|
||||
const WS_URL = endpoints.terminal
|
||||
const WS_URL = resolveEndpoints().terminal
|
||||
let socket: WebSocket | null = null
|
||||
const connected = ref(false)
|
||||
|
||||
@@ -78,7 +79,7 @@ let pendingWhisperSend = false // Flag to send transcript when Whisper responds
|
||||
const useWhisper = ref(false)
|
||||
const whisperReady = ref(false)
|
||||
const whisperLoading = ref(false)
|
||||
const WHISPER_WS_URL = endpoints.whisper
|
||||
const WHISPER_WS_URL = resolveEndpoints().whisper
|
||||
let whisperSocket: WebSocket | null = null
|
||||
let mediaRecorder: MediaRecorder | null = null
|
||||
let audioChunks: Blob[] = []
|
||||
@@ -172,7 +173,7 @@ async function saveRecordingToBackend(blob: Blob) {
|
||||
reader.onloadend = async () => {
|
||||
const base64 = (reader.result as string).split(',')[1]
|
||||
|
||||
const response = await fetch('/api/recordings', {
|
||||
const response = await apiFetch('/api/recordings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -331,7 +332,7 @@ function initRecognition() {
|
||||
|
||||
async function checkWhisperStatus(updateLoading = true) {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/status')
|
||||
const res = await apiFetch('/api/whisper/status')
|
||||
const data = await res.json()
|
||||
useWhisper.value = data.enabled
|
||||
whisperReady.value = data.running
|
||||
@@ -365,7 +366,7 @@ async function toggleWhisperMode() {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/whisper/toggle', {
|
||||
const res = await apiFetch('/api/whisper/toggle', {
|
||||
method: 'POST'
|
||||
})
|
||||
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">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { AgentName, SessionInfo } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -51,7 +52,7 @@ async function fetchAllSessions() {
|
||||
await Promise.all(
|
||||
props.agents.map(async (a) => {
|
||||
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()
|
||||
else map[a.id] = []
|
||||
} catch {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { TableInfo, TableSchema, DbStats } from '@/types/database'
|
||||
|
||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||
@@ -17,7 +18,7 @@ export function useDatabaseApi() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
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')
|
||||
tables.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -29,7 +30,7 @@ export function useDatabaseApi() {
|
||||
|
||||
async function fetchDbStats() {
|
||||
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')
|
||||
dbStats.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -39,7 +40,7 @@ export function useDatabaseApi() {
|
||||
|
||||
async function fetchTableSchema(tableName: string) {
|
||||
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')
|
||||
tableSchema.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -52,7 +53,7 @@ export function useDatabaseApi() {
|
||||
loading.value = true
|
||||
try {
|
||||
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')
|
||||
const result = await res.json()
|
||||
tableData.value = result.rows
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||
const API_BASE = '/api/database'
|
||||
@@ -17,7 +18,7 @@ export function useQueryExecutor() {
|
||||
queryResult.value = null
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/query`, {
|
||||
const res = await apiFetch(`${API_BASE}/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: queryText.value })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { GitStatus, CommitInfo, FileDiff, BranchInfo, CompareResult, DiffResult, TreeNode, FileContent } from '@/types/git'
|
||||
|
||||
const API_BASE = '/api/git'
|
||||
@@ -20,7 +21,7 @@ export function useGitApi() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
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')
|
||||
status.value = await res.json()
|
||||
currentBranch.value = status.value?.branch || ''
|
||||
@@ -39,7 +40,7 @@ export function useGitApi() {
|
||||
if (options?.staged) params.set('staged', 'true')
|
||||
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')
|
||||
diff.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -53,7 +54,7 @@ export function useGitApi() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
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')
|
||||
const data = await res.json()
|
||||
commits.value = append ? [...commits.value, ...data] : data
|
||||
@@ -68,7 +69,7 @@ export function useGitApi() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
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')
|
||||
selectedCommit.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -81,7 +82,7 @@ export function useGitApi() {
|
||||
async function fetchBranches() {
|
||||
error.value = null
|
||||
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')
|
||||
branches.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -93,7 +94,7 @@ export function useGitApi() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/compare`, {
|
||||
const res = await apiFetch(`${API_BASE}/compare`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ base, head })
|
||||
@@ -125,7 +126,7 @@ export function useGitApi() {
|
||||
const params = new URLSearchParams()
|
||||
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')
|
||||
fileTree.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -140,7 +141,7 @@ export function useGitApi() {
|
||||
error.value = null
|
||||
try {
|
||||
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')
|
||||
fileContent.value = await res.json()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ref } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
||||
|
||||
export function useHooksApproval() {
|
||||
@@ -33,7 +34,7 @@ export function useHooksApproval() {
|
||||
|
||||
async function respondPermission(requestId: string, decision: 'allow' | 'deny') {
|
||||
try {
|
||||
await fetch('/api/hooks-approval/respond', {
|
||||
await apiFetch('/api/hooks-approval/respond', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision })
|
||||
@@ -48,7 +49,7 @@ export function useHooksApproval() {
|
||||
|
||||
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
||||
try {
|
||||
await fetch('/api/hooks-approval/respond-plan', {
|
||||
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision, reason })
|
||||
@@ -63,7 +64,7 @@ export function useHooksApproval() {
|
||||
|
||||
async function fetchPending() {
|
||||
try {
|
||||
const res = await fetch('/api/hooks-approval')
|
||||
const res = await apiFetch('/api/hooks-approval')
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useSessionState } from '@/stores/session-state'
|
||||
import type {
|
||||
@@ -102,7 +103,7 @@ export function useTranscriptDebug() {
|
||||
updates: { transcriptSessionId?: string; label?: string }
|
||||
) {
|
||||
try {
|
||||
await fetch(terminalApiUrl('/update-terminal'), {
|
||||
await apiFetch(terminalApiUrl('/update-terminal'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ephemeralSessionId, ...updates })
|
||||
@@ -112,7 +113,7 @@ export function useTranscriptDebug() {
|
||||
|
||||
async function unregisterTerminalOnServer(ephemeralSessionId: string) {
|
||||
try {
|
||||
await fetch(terminalApiUrl('/unregister-terminal'), {
|
||||
await apiFetch(terminalApiUrl('/unregister-terminal'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ephemeralSessionId })
|
||||
@@ -141,7 +142,7 @@ export function useTranscriptDebug() {
|
||||
|
||||
try {
|
||||
// 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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -215,7 +216,7 @@ export function useTranscriptDebug() {
|
||||
} else if (ephSid) {
|
||||
// No local terminal — kill the PTY directly on server
|
||||
try {
|
||||
await fetch(terminalApiUrl('/kill-session'), {
|
||||
await apiFetch(terminalApiUrl('/kill-session'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: ephSid })
|
||||
@@ -324,7 +325,7 @@ export function useTranscriptDebug() {
|
||||
if (socket?.readyState === WebSocket.OPEN) return
|
||||
|
||||
// Same sync server as git (port 4105)
|
||||
socket = new WebSocket(endpoints.git)
|
||||
socket = new WebSocket(resolveEndpoints().git)
|
||||
|
||||
socket.onopen = () => {
|
||||
isRealtime.value = true
|
||||
@@ -436,7 +437,7 @@ export function useTranscriptDebug() {
|
||||
if (!selectedSessionId.value) return
|
||||
|
||||
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}`)
|
||||
rawContent.value = await res.text()
|
||||
const parsed = parseJsonl(rawContent.value, selectedSessionId.value)
|
||||
@@ -544,7 +545,7 @@ export function useTranscriptDebug() {
|
||||
|
||||
async function fetchSessions() {
|
||||
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}`)
|
||||
sessions.value = await res.json()
|
||||
} catch (e: any) {
|
||||
@@ -557,7 +558,7 @@ export function useTranscriptDebug() {
|
||||
// Determine agent from registry if different from selected
|
||||
const entry = serverRegistry.value.find(e => e.transcriptSessionId === sessionId)
|
||||
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}`)
|
||||
rawContent.value = await res.text()
|
||||
conversation.value = parseJsonl(rawContent.value, sessionId)
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
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'
|
||||
|
||||
@@ -95,7 +96,7 @@ export function useEphemeralTerminal(
|
||||
if (state.value !== 'off') return
|
||||
state.value = 'connecting'
|
||||
|
||||
const wsBase = endpoints.terminal
|
||||
const wsBase = resolveEndpoints().terminal
|
||||
const sep = wsBase.includes('?') ? '&' : '?'
|
||||
const wsUrl = `${wsBase}${sep}session=${ephemeralSessionId}`
|
||||
|
||||
@@ -202,7 +203,7 @@ export function useEphemeralTerminal(
|
||||
|
||||
// Force-kill via HTTP as safety net
|
||||
try {
|
||||
await fetch(terminalApiUrl('/kill-session'), {
|
||||
await apiFetch(terminalApiUrl('/kill-session'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: ephemeralSessionId })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useSessionState } from '@/stores/session-state'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
|
||||
|
||||
export interface ApprovalSessionGroup {
|
||||
@@ -79,7 +80,7 @@ export function useGlobalApproval() {
|
||||
async function respondPermission(requestId: string, decision: string, reason?: string) {
|
||||
console.log(`[GlobalApproval] Responding permission ${requestId}: ${decision}${reason ? ' reason=' + reason : ''}`)
|
||||
try {
|
||||
await fetch('/api/hooks-approval/respond', {
|
||||
await apiFetch('/api/hooks-approval/respond', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision, reason })
|
||||
@@ -93,7 +94,7 @@ export function useGlobalApproval() {
|
||||
async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
|
||||
console.log(`[GlobalApproval] Responding plan ${requestId}: ${decision}`)
|
||||
try {
|
||||
await fetch('/api/hooks-approval/respond-plan', {
|
||||
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision, reason })
|
||||
@@ -106,7 +107,7 @@ export function useGlobalApproval() {
|
||||
async function ignoreApproval(requestId: string) {
|
||||
console.log(`[GlobalApproval] Ignoring ${requestId} (UI-only removal)`)
|
||||
try {
|
||||
await fetch('/api/hooks-approval/ignore', {
|
||||
await apiFetch('/api/hooks-approval/ignore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId })
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Centralized endpoint configuration for Agent UI
|
||||
* 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)
|
||||
export const isSecure = window.location.protocol === 'https:'
|
||||
|
||||
@@ -30,7 +33,7 @@ function buildHttpUrl(securePath: string, devPort: number): string {
|
||||
return `http://${hostname}:${devPort}`
|
||||
}
|
||||
|
||||
// Endpoint configuration
|
||||
// Static endpoint configuration (used in web mode)
|
||||
export const endpoints = {
|
||||
// Terminal WebSocket
|
||||
terminal: buildWsUrl('/ws/terminal', 4103),
|
||||
@@ -57,17 +60,85 @@ export const endpoints = {
|
||||
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
|
||||
export function agentTerminalUrl(agentId: string): string {
|
||||
const base = endpoints.terminal
|
||||
const base = resolveEndpoints().terminal
|
||||
const sep = base.includes('?') ? '&' : '?'
|
||||
return `${base}${sep}session=agent-${agentId}`
|
||||
}
|
||||
|
||||
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}`
|
||||
return `http://${hostname}:4103${path}`
|
||||
}
|
||||
|
||||
// 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 router from './router'
|
||||
import './styles/main.css'
|
||||
import { isTauri } from './lib/tauri'
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
@@ -14,9 +15,23 @@ app.use(router)
|
||||
;(window as any).__vueApp = app
|
||||
;(window as any).__pinia = pinia
|
||||
|
||||
// Inicializar tema antes de montar la app
|
||||
import { useThemeStore } from './stores/theme'
|
||||
const themeStore = useThemeStore(pinia)
|
||||
themeStore.fetchThemes().then(() => {
|
||||
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
|
||||
const { useThemeStore } = await import('./stores/theme')
|
||||
const themeStore = useThemeStore(pinia)
|
||||
await themeStore.fetchThemes().catch(() => {
|
||||
// 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 { useGitApi } from '@/composables/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'
|
||||
|
||||
@@ -50,7 +50,7 @@ const isRealtime = ref(false)
|
||||
function connectGitWatcher() {
|
||||
if (gitSocket?.readyState === WebSocket.OPEN) return
|
||||
|
||||
gitSocket = new WebSocket(endpoints.git)
|
||||
gitSocket = new WebSocket(resolveEndpoints().git)
|
||||
|
||||
gitSocket.onopen = () => {
|
||||
isRealtime.value = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
interface FileNode {
|
||||
name: string
|
||||
@@ -82,7 +83,7 @@ async function connect() {
|
||||
|
||||
try {
|
||||
// Test connection and get repo info
|
||||
const res = await fetch('/api/gitea/repo', {
|
||||
const res = await apiFetch('/api/gitea/repo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -119,7 +120,7 @@ async function connect() {
|
||||
|
||||
async function loadFileTree(path: string = '') {
|
||||
try {
|
||||
const res = await fetch('/api/gitea/tree', {
|
||||
const res = await apiFetch('/api/gitea/tree', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -184,7 +185,7 @@ async function selectFile(node: FileNode) {
|
||||
fileContent.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gitea/file', {
|
||||
const res = await apiFetch('/api/gitea/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useCanvasStore } from '../stores/canvas'
|
||||
import { useThemeStore } from '../stores/theme'
|
||||
import { useWindowsStore } from '../stores/windows'
|
||||
import WindowContainer from '../components/WindowContainer.vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||
const API_URL = ''
|
||||
@@ -128,7 +129,7 @@ export const componentsApi = {
|
||||
if (opts?.includeArchived) params.set('include_archived', 'true')
|
||||
if (opts?.limit) params.set('limit', String(opts.limit))
|
||||
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()
|
||||
return data.map((row: any) => ({
|
||||
...row,
|
||||
@@ -139,7 +140,7 @@ export const componentsApi = {
|
||||
},
|
||||
|
||||
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
|
||||
const row = await res.json()
|
||||
return {
|
||||
@@ -151,7 +152,7 @@ export const componentsApi = {
|
||||
},
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(component)
|
||||
@@ -160,7 +161,7 @@ export const componentsApi = {
|
||||
},
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -173,12 +174,12 @@ export const componentsApi = {
|
||||
},
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { endpoints } from '@/config/endpoints'
|
||||
import { resolveEndpoints } from '@/config/endpoints'
|
||||
import { useSessionState } from '@/stores/session-state'
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
@@ -10,7 +10,7 @@ function connect() {
|
||||
const store = useSessionState()
|
||||
|
||||
// Connect to terminal server (4103) — same base as terminal WS
|
||||
const url = endpoints.terminal
|
||||
const url = resolveEndpoints().terminal
|
||||
console.log('[SessionStateWS] Connecting to', url)
|
||||
|
||||
ws = new WebSocket(url)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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
|
||||
const API_URL = ''
|
||||
@@ -9,24 +10,24 @@ const API_URL = ''
|
||||
|
||||
export const themesApi = {
|
||||
async getAll(): Promise<Theme[]> {
|
||||
const res = await fetch(`${API_URL}/api/themes`)
|
||||
const res = await apiFetch(`${API_URL}/api/themes`)
|
||||
return res.json()
|
||||
},
|
||||
|
||||
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
|
||||
return res.json()
|
||||
},
|
||||
|
||||
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
|
||||
return res.json()
|
||||
},
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(theme)
|
||||
@@ -35,17 +36,17 @@ export const themesApi = {
|
||||
},
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToolConfig } from './index'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||
const API_BASE = ''
|
||||
@@ -12,7 +13,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
||||
schema: { type: 'object', properties: {} },
|
||||
handler: async () => {
|
||||
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')
|
||||
const tables = await res.json()
|
||||
|
||||
@@ -40,7 +41,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
||||
},
|
||||
handler: async (args: { table: string }) => {
|
||||
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.status === 404) return `Tabla "${args.table}" no encontrada`
|
||||
throw new Error('Failed to fetch schema')
|
||||
@@ -81,7 +82,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
||||
const limit = Math.min(args.limit || 20, 100)
|
||||
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}`
|
||||
)
|
||||
if (!res.ok) {
|
||||
@@ -118,7 +119,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
||||
schema: { type: 'object', properties: {} },
|
||||
handler: async () => {
|
||||
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')
|
||||
const stats = await res.json()
|
||||
|
||||
@@ -146,7 +147,7 @@ export function createDatabaseHandlers(): ToolConfig[] {
|
||||
},
|
||||
handler: async (args: { query: string }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/database/query`, {
|
||||
const res = await apiFetch(`${API_BASE}/api/database/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: args.query })
|
||||
|
||||
@@ -5,18 +5,19 @@ import {
|
||||
type VueComponentDefinition
|
||||
} from '../../dynamicComponents'
|
||||
import { getWindowDefinitions } from './canvasHandlers'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
const API_URL = ''
|
||||
|
||||
export const fsComponentsApi = {
|
||||
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}`)
|
||||
return res.json()
|
||||
},
|
||||
|
||||
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`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToolConfig } from './index'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
const API_BASE = ''
|
||||
|
||||
@@ -14,7 +15,7 @@ export function createGitHandlers(): ToolConfig[] {
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/git/status`)
|
||||
const res = await apiFetch(`${API_BASE}/api/git/status`)
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
@@ -83,7 +84,7 @@ export function createGitHandlers(): ToolConfig[] {
|
||||
if (args.staged) params.set('staged', 'true')
|
||||
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) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
@@ -131,7 +132,7 @@ export function createGitHandlers(): ToolConfig[] {
|
||||
},
|
||||
handler: async (args: { base: string; head: string }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/git/compare`, {
|
||||
const res = await apiFetch(`${API_BASE}/api/git/compare`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(args)
|
||||
@@ -197,7 +198,7 @@ export function createGitHandlers(): ToolConfig[] {
|
||||
if (args.author) params.set('author', args.author)
|
||||
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) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
@@ -235,7 +236,7 @@ export function createGitHandlers(): ToolConfig[] {
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/git/branches`)
|
||||
const res = await apiFetch(`${API_BASE}/api/git/branches`)
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToolConfig } from './index'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
let routerInstance: any = null
|
||||
|
||||
@@ -234,7 +235,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/status')
|
||||
const res = await apiFetch('/api/whisper/status')
|
||||
const data = await res.json()
|
||||
return `Whisper GPU Status:\n` +
|
||||
` Enabled: ${data.enabled ? 'Yes' : 'No'}\n` +
|
||||
@@ -257,7 +258,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/toggle', {
|
||||
const res = await apiFetch('/api/whisper/toggle', {
|
||||
method: 'POST'
|
||||
})
|
||||
const data = await res.json()
|
||||
@@ -287,7 +288,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/start', {
|
||||
const res = await apiFetch('/api/whisper/start', {
|
||||
method: 'POST'
|
||||
})
|
||||
const data = await res.json()
|
||||
@@ -315,7 +316,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/stop', {
|
||||
const res = await apiFetch('/api/whisper/stop', {
|
||||
method: 'POST'
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToolConfig } from './index'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||
const API_BASE = ''
|
||||
@@ -34,7 +35,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/repo`, {
|
||||
const res = await apiFetch(`${API_BASE}/api/gitea/repo`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(giteaCredentials)
|
||||
@@ -75,7 +76,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/tree`, {
|
||||
const res = await apiFetch(`${API_BASE}/api/gitea/tree`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
||||
@@ -131,7 +132,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/file`, {
|
||||
const res = await apiFetch(`${API_BASE}/api/gitea/file`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...giteaCredentials, path: args.path })
|
||||
@@ -177,7 +178,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
||||
}
|
||||
|
||||
try {
|
||||
const treeRes = await fetch(`${API_BASE}/api/gitea/tree`, {
|
||||
const treeRes = await apiFetch(`${API_BASE}/api/gitea/tree`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...giteaCredentials, path: args.path || '' })
|
||||
@@ -199,7 +200,7 @@ export function createSourceCodeHandlers(): ToolConfig[] {
|
||||
|
||||
for (const file of files.slice(0, maxFiles)) {
|
||||
try {
|
||||
const fileRes = await fetch(`${API_BASE}/api/gitea/file`, {
|
||||
const fileRes = await apiFetch(`${API_BASE}/api/gitea/file`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...giteaCredentials, path: file.path })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTorchStore } from '../stores/torch'
|
||||
import { autoConnect, disconnectWebMCP } from './webmcp'
|
||||
import { onTorchConnected, onTorchDisconnected } from './toolRegistry'
|
||||
import { endpoints } from '../config/endpoints'
|
||||
import { resolveEndpoints } from '../config/endpoints'
|
||||
|
||||
let torchWs: WebSocket | null = null
|
||||
let clientId: string | null = null
|
||||
@@ -18,7 +18,7 @@ function connectToTorchServer(): Promise<void> {
|
||||
}
|
||||
|
||||
console.log('[Torch] Connecting to server...')
|
||||
torchWs = new WebSocket(endpoints.torch)
|
||||
torchWs = new WebSocket(resolveEndpoints().torch)
|
||||
|
||||
torchWs.onopen = () => {
|
||||
console.log('[Torch] Connected to server')
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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
|
||||
const WEBMCP_HTTP = endpoints.webmcpHttp
|
||||
// WebMCP HTTP API base for direct token requests (resolved dynamically for Tauri)
|
||||
function getWebmcpHttp() { return resolveEndpoints().webmcpHttp }
|
||||
|
||||
let webmcpInstance: any = null
|
||||
const registeredTools = new Set<string>()
|
||||
const eventUnsubscribers: Array<() => void> = []
|
||||
|
||||
const API_BASE = endpoints.api
|
||||
function getApiBase() { return resolveEndpoints().api }
|
||||
let tokenPollingInterval: number | null = null
|
||||
|
||||
export async function initWebMCP() {
|
||||
@@ -258,7 +260,7 @@ export function isToolRegistered(name: string): boolean {
|
||||
// Token polling functions
|
||||
export async function checkForToken(): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/webmcp-token`)
|
||||
const res = await apiFetch(`${getApiBase()}/webmcp-token`)
|
||||
const data = await res.json()
|
||||
return data.token || null
|
||||
} catch (e) {
|
||||
@@ -268,7 +270,7 @@ export async function checkForToken(): Promise<string | null> {
|
||||
|
||||
export async function clearToken(): Promise<void> {
|
||||
try {
|
||||
await fetch(`${API_BASE}/webmcp-token`, { method: 'DELETE' })
|
||||
await apiFetch(`${getApiBase()}/webmcp-token`, { method: 'DELETE' })
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
@@ -315,11 +317,11 @@ export async function requestToken(): Promise<string | null> {
|
||||
try {
|
||||
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
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
@@ -392,9 +394,18 @@ export async function connectWithToken(token: string): Promise<boolean> {
|
||||
// Clear the pending token from server
|
||||
await clearToken()
|
||||
|
||||
// If behind HTTPS/Traefik, modify token to use secure WebSocket
|
||||
// Modify token for correct WebSocket URL based on environment
|
||||
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)
|
||||
if (parsed) {
|
||||
// Replace ws://localhost:4102 with wss://hostname/ws/mcp
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
@@ -28,8 +29,8 @@ const listeners = new Set<TranscriptionCallback>()
|
||||
function connect() {
|
||||
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return
|
||||
|
||||
console.log('[WhisperSocket] Connecting to', endpoints.whisper)
|
||||
socket = new WebSocket(endpoints.whisper)
|
||||
console.log('[WhisperSocket] Connecting to', resolveEndpoints().whisper)
|
||||
socket = new WebSocket(resolveEndpoints().whisper)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (socket && socket.readyState !== WebSocket.OPEN) {
|
||||
@@ -86,7 +87,7 @@ function scheduleReconnect() {
|
||||
|
||||
async function checkStatusAndConnect() {
|
||||
try {
|
||||
const res = await fetch('/api/whisper/status')
|
||||
const res = await apiFetch('/api/whisper/status')
|
||||
const data = await res.json()
|
||||
if (data.running) {
|
||||
connect()
|
||||
@@ -148,7 +149,7 @@ export async function reconnect() {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/whisper/toggle', { method: 'POST' })
|
||||
const res = await apiFetch('/api/whisper/toggle', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.running) {
|
||||
connect()
|
||||
@@ -158,7 +159,7 @@ export async function reconnect() {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
try {
|
||||
const s = await fetch('/api/whisper/status')
|
||||
const s = await apiFetch('/api/whisper/status')
|
||||
const d = await s.json()
|
||||
if (d.running) {
|
||||
connect()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// ── Existing types ──
|
||||
|
||||
@@ -238,7 +239,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/api/agents')
|
||||
const res = await apiFetch('/api/agents')
|
||||
if (!res.ok) throw new Error('Failed to fetch agents')
|
||||
agents.value = await res.json()
|
||||
if (agents.value.length && !selectedAgentId.value) {
|
||||
@@ -263,7 +264,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
async function loadFile(agentId: string, file: AgentFile) {
|
||||
error.value = null
|
||||
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) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error || 'Failed to load file')
|
||||
@@ -288,7 +289,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/api/agents/file', {
|
||||
const res = await apiFetch('/api/agents/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -339,8 +340,8 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
try {
|
||||
const agentId = selectedAgentId.value || 'main'
|
||||
const [configRes, knownRes] = await Promise.all([
|
||||
fetch(`/api/agents/config?agentId=${encodeURIComponent(agentId)}`),
|
||||
fetch('/api/agents/known-tools')
|
||||
apiFetch(`/api/agents/config?agentId=${encodeURIComponent(agentId)}`),
|
||||
apiFetch('/api/agents/known-tools')
|
||||
])
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -558,8 +559,8 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
error.value = null
|
||||
try {
|
||||
const [mcpRes, configRes] = await Promise.all([
|
||||
fetch('/api/agents/mcp-json'),
|
||||
fetch(`/api/agents/config?agentId=${encodeURIComponent(selectedAgentId.value || 'main')}`)
|
||||
apiFetch('/api/agents/mcp-json'),
|
||||
apiFetch(`/api/agents/config?agentId=${encodeURIComponent(selectedAgentId.value || 'main')}`)
|
||||
])
|
||||
if (!mcpRes.ok) throw new Error('Failed to fetch MCP config')
|
||||
|
||||
@@ -604,7 +605,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
mcpServersObj[s.name] = entry
|
||||
}
|
||||
|
||||
const res = await fetch('/api/agents/config/mcp', {
|
||||
const res = await apiFetch('/api/agents/config/mcp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mcpServers: mcpServersObj })
|
||||
@@ -636,7 +637,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/api/agents/config/hooks', {
|
||||
const res = await apiFetch('/api/agents/config/hooks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -692,7 +693,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
error.value = null
|
||||
try {
|
||||
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')
|
||||
skills.value = await res.json()
|
||||
if (skills.value.length && !selectedSkill.value) {
|
||||
@@ -711,7 +712,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
||||
pluginsLoading.value = true
|
||||
error.value = null
|
||||
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')
|
||||
plugins.value = await res.json()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
export interface HookNotification {
|
||||
id: string
|
||||
@@ -172,7 +173,7 @@ export const useClaudeHooksStore = defineStore('claude-hooks', () => {
|
||||
|
||||
async function respondPermission(notifId: string, requestId: string, decision: 'allow' | 'deny') {
|
||||
try {
|
||||
await fetch('/api/claude-permission-respond', {
|
||||
await apiFetch('/api/claude-permission-respond', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas'
|
||||
|
||||
// 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
|
||||
try {
|
||||
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')
|
||||
canvases.value = await res.json()
|
||||
} catch (e) {
|
||||
@@ -64,7 +65,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
|
||||
async function fetchToolbarCanvases() {
|
||||
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')
|
||||
toolbarCanvases.value = await res.json()
|
||||
} catch (e) {
|
||||
@@ -75,7 +76,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
|
||||
async function fetchDefaultCanvas(): Promise<ProjectCanvas | null> {
|
||||
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')
|
||||
const data = await res.json()
|
||||
if (data.hasDefault) {
|
||||
@@ -93,7 +94,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
|
||||
async function fetchCanvasById(id: string): Promise<ProjectCanvas | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas/${id}`)
|
||||
const res = await apiFetch(`${API_URL}/api/canvas/${id}`)
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return null
|
||||
throw new Error('Failed to fetch canvas')
|
||||
@@ -109,7 +110,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas`, {
|
||||
const res = await apiFetch(`${API_URL}/api/canvas`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -131,7 +132,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas/${id}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/canvas/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -158,7 +159,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
async function deleteCanvas(id: string): Promise<boolean> {
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas/${id}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/canvas/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (!res.ok) {
|
||||
@@ -185,7 +186,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas/${id}/clone`, {
|
||||
const res = await apiFetch(`${API_URL}/api/canvas/${id}/clone`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newName })
|
||||
@@ -207,7 +208,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
async function fetchCanvasComponents(canvasId: string) {
|
||||
loading.value = true
|
||||
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')
|
||||
activeCanvasComponents.value = await res.json()
|
||||
} catch (e) {
|
||||
@@ -225,7 +226,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
position?: number
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components`, {
|
||||
const res = await apiFetch(`${API_URL}/api/canvas/${canvasId}/components`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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> {
|
||||
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'
|
||||
})
|
||||
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 }
|
||||
): Promise<boolean> {
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -276,7 +277,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => {
|
||||
// Component Usage
|
||||
async function getComponentUsage(componentId: string): Promise<ComponentUsage | null> {
|
||||
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')
|
||||
return await res.json()
|
||||
} 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 { ref, computed } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import { terminalApiUrl } from '../config/endpoints'
|
||||
|
||||
// ── Types (mirror server/services/session-state.ts) ──
|
||||
@@ -196,7 +197,7 @@ export const useSessionState = defineStore('session-state', () => {
|
||||
// ── Actions ──
|
||||
|
||||
async function respondApproval(requestId: string, decision: string, reason?: string) {
|
||||
await fetch('/api/hooks-approval/respond', {
|
||||
await apiFetch('/api/hooks-approval/respond', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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) {
|
||||
await fetch('/api/hooks-approval/respond-plan', {
|
||||
await apiFetch('/api/hooks-approval/respond-plan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requestId, decision, reason })
|
||||
@@ -213,7 +214,7 @@ export const useSessionState = defineStore('session-state', () => {
|
||||
|
||||
async function refreshAgentState(agent: string) {
|
||||
try {
|
||||
const res = await fetch(terminalApiUrl(`/session-state/${agent}`))
|
||||
const res = await apiFetch(terminalApiUrl(`/session-state/${agent}`))
|
||||
if (!res.ok) return
|
||||
const state = await res.json() as AgentSessionState
|
||||
agents.value = { ...agents.value, [agent]: state }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
import { useWindowsStore } from './windows'
|
||||
import {
|
||||
getScriptLog,
|
||||
@@ -142,7 +143,7 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
||||
async function save(name: string): Promise<string> {
|
||||
const snapshot = captureState(name)
|
||||
|
||||
const res = await fetch(`${API_URL}/api/snapshots`, {
|
||||
const res = await apiFetch(`${API_URL}/api/snapshots`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -159,7 +160,7 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
||||
|
||||
// ---- List snapshots ----
|
||||
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()
|
||||
snapshots.value = data
|
||||
return data
|
||||
@@ -167,14 +168,14 @@ export const useSnapshotsStore = defineStore('snapshots', () => {
|
||||
|
||||
// ---- Load full snapshot ----
|
||||
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()
|
||||
return row.data
|
||||
}
|
||||
|
||||
// ---- Remove snapshot ----
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiFetch } from '@/lib/tauri'
|
||||
|
||||
// =====================
|
||||
// Types
|
||||
@@ -95,7 +96,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/themes`)
|
||||
const res = await apiFetch(`${API_URL}/api/themes`)
|
||||
themes.value = await res.json()
|
||||
|
||||
const defaultTheme = themes.value.find(t => t.is_default)
|
||||
@@ -113,7 +114,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
|
||||
async function fetchDesignTokens() {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/design-tokens`)
|
||||
const res = await apiFetch(`${API_URL}/api/design-tokens`)
|
||||
designTokens.value = await res.json()
|
||||
} catch (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 }) {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/themes`, {
|
||||
const res = await apiFetch(`${API_URL}/api/themes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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 }) {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/themes/${id}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/themes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -164,7 +165,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
|
||||
async function deleteTheme(id: string) {
|
||||
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()
|
||||
if (result.error) {
|
||||
error.value = result.error
|
||||
@@ -180,7 +181,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
|
||||
async function setDefaultTheme(id: string) {
|
||||
try {
|
||||
await fetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
||||
await apiFetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' })
|
||||
await fetchThemes()
|
||||
} catch (e) {
|
||||
error.value = 'Error setting default theme'
|
||||
|
||||
@@ -6,7 +6,10 @@ import path from 'path'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const isTauri = !!process.env.TAURI_ENV_PLATFORM
|
||||
|
||||
export default defineConfig({
|
||||
envPrefix: ['VITE_', 'TAURI_ENV_'],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
@@ -16,7 +19,8 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
// PWA only in web builds (not Tauri)
|
||||
...(!isTauri ? [VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg', 'icons/*.svg', 'icons/*.png'],
|
||||
devOptions: {
|
||||
@@ -67,7 +71,7 @@ export default defineConfig({
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})] : [])
|
||||
],
|
||||
server: {
|
||||
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:api": "cd server && bun --watch run index.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": {
|
||||
"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 |