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
This commit is contained in:
@@ -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
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
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
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'
|
||||
|
||||
Reference in New Issue
Block a user