From 78978813cd4bd3022cc2c123bdd57cd27dd68fa7 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Tue, 24 Feb 2026 12:13:15 -0600 Subject: [PATCH] feat: compact boundary divider, overlay fix, approval window, PiP, Tauri enhancements - Add CompactBoundaryDivider component for compact_boundary system messages - Fix readability overlay: v-if removes element entirely at 0% opacity - Add approval page and window composable - Add PiP window support and loading screen - Tauri: add window management commands and capabilities - Disable Ctrl+1..5 shortcuts in Tauri (handled by global shortcuts) --- frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/public/loading.html | 42 ++ frontend/src/App.vue | 178 ++++++++- .../components/FloatingTranscriptDebug.vue | 23 +- .../CompactBoundaryDivider.vue | 203 ++++++++++ frontend/src/composables/useApprovalWindow.ts | 124 ++++++ frontend/src/composables/useGlobalApproval.ts | 50 ++- frontend/src/composables/usePipWindow.ts | 123 ++++++ frontend/src/lib/loadingWindow.ts | 52 +++ frontend/src/pages/ApprovalPage.vue | 274 +++++++++++++ frontend/src/pages/TranscriptDebugPage.vue | 2 +- frontend/src/router/index.ts | 5 + .../src/services/approvalNotifications.ts | 360 ++++++++++++++++++ package.json | 2 +- server/index.ts | 1 + server/routes/transcript-debug.ts | 3 + server/services/sync-server.ts | 1 + src-tauri/Cargo.lock | 41 ++ src-tauri/Cargo.toml | 3 +- src-tauri/capabilities/default.json | 16 +- .../android/app/src/main/AndroidManifest.xml | 3 + src-tauri/src/lib.rs | 133 +++++++ 23 files changed, 1616 insertions(+), 34 deletions(-) create mode 100644 frontend/public/loading.html create mode 100644 frontend/src/components/transcript-debug/CompactBoundaryDivider.vue create mode 100644 frontend/src/composables/useApprovalWindow.ts create mode 100644 frontend/src/composables/usePipWindow.ts create mode 100644 frontend/src/lib/loadingWindow.ts create mode 100644 frontend/src/pages/ApprovalPage.vue create mode 100644 frontend/src/services/approvalNotifications.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3daca20..befd5f4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-global-shortcut": "^2.3.1", "@tauri-apps/plugin-http": "^2.5.7", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-store": "^2.4.2", @@ -2667,6 +2668,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-global-shortcut": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.1.tgz", + "integrity": "sha512-vr40W2N6G63dmBPaha1TsBQLLURXG538RQbH5vAm0G/ovVZyXJrmZR1HF1W+WneNloQvwn4dm8xzwpEXRW560g==", + "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", diff --git a/frontend/package.json b/frontend/package.json index d4f548f..bf670d0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-global-shortcut": "^2.3.1", "@tauri-apps/plugin-http": "^2.5.7", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-store": "^2.4.2", diff --git a/frontend/public/loading.html b/frontend/public/loading.html new file mode 100644 index 0000000..6121c78 --- /dev/null +++ b/frontend/public/loading.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
+
+ + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6163e9d..be32d58 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -22,7 +22,9 @@ import { useCanvasStore } from './stores/canvas' import { useProjectCanvasStore } from './stores/projectCanvas' import { useSessionState } from './stores/session-state' import { isTauri, isMobileTauri, getTauriNotification } from './lib/tauri' +import { initApprovalNotifications } from './services/approvalNotifications' import { useServerConfig } from './stores/server-config' +import { useApprovalWindow } from './composables/useApprovalWindow' const route = useRoute() const router = useRouter() @@ -32,7 +34,7 @@ const serverConfig = isTauri ? useServerConfig() : null const showServerConfig = ref(false) const needsServerConfig = computed(() => isTauri && serverConfig && !serverConfig.isConfigured) -const isPipWindow = computed(() => route.query.pip === '1') +const isPipWindow = computed(() => route.query.pip === '1' || route.query.window === '1') const showVoice = ref(false) const showTranscriptDebug = ref(false) const showDebugConsole = ref(false) @@ -85,6 +87,15 @@ const canvasStore = useCanvasStore() const projectCanvasStore = useProjectCanvasStore() const sessionState = useSessionState() const { totalPending, modalVisible, connect: connectApproval, disconnect: disconnectApproval, fetchPending: fetchApprovalPending } = useGlobalApproval() +const { approvalWindowOpen, openApprovalWindow } = useApprovalWindow() + +function handleApprovalBadgeClick() { + if (isTauri) { + openApprovalWindow() + } else { + modalVisible.value = !modalVisible.value + } +} // Voice FAB push-to-talk state const voicePTTActive = ref(false) let voiceTouchStarted = false @@ -173,6 +184,16 @@ function handleGlobalKeydown(e: KeyboardEvent) { showTranscriptDebug.value = !showTranscriptDebug.value } } + + // Ctrl+W → close current window (PiP closes, main hides to tray) + if (e.ctrlKey && e.key === 'w') { + e.preventDefault() + if (isTauri) { + import('@tauri-apps/api/webviewWindow').then(({ getCurrentWebviewWindow }) => { + getCurrentWebviewWindow().close() + }).catch(() => {}) + } + } } // Voice FAB push-to-talk handlers @@ -249,6 +270,11 @@ onMounted(async () => { connectApproval() fetchApprovalPending() + // Initialize native approval notifications (Tauri only) + if (isTauri) { + initApprovalNotifications() + } + // Connect centralized session state WS initSessionStateWS() @@ -309,21 +335,145 @@ onMounted(async () => { async function sendTestNotification() { const title = 'Agent UI' - const body = 'Test notification from Agent UI — all platforms!' + const body = 'Test notification — ' + new Date().toLocaleTimeString() if (isTauri) { + const isMobile = isMobileTauri() try { - const { isPermissionGranted, requestPermission, sendNotification } = await getTauriNotification() - let granted = await isPermissionGranted() + const { invoke } = await import('@tauri-apps/api/core') + + // ── Step 1: Check permission ── + console.log('[TestNotif] === START === isMobile:', isMobile) + console.log('[TestNotif] Step 1: checking permission...') + let granted = false + + if (isMobile) { + // Android: use Kotlin @Command methods directly (bypass window.Notification polyfill) + // check_permissions → Kotlin checkPermissions (auto snake→camel routing) + try { + const check = await invoke<{ permissionState: string }>('plugin:notification|check_permissions') + console.log('[TestNotif] check_permissions result:', JSON.stringify(check)) + granted = check?.permissionState === 'granted' + } catch (e) { + console.warn('[TestNotif] check_permissions failed, trying is_permission_granted:', e) + try { + const result = await invoke('plugin:notification|is_permission_granted') + console.log('[TestNotif] is_permission_granted:', result) + granted = result === true + } catch (e2) { + console.error('[TestNotif] is_permission_granted also failed:', e2) + } + } + + // ── Step 2: Request if needed ── + if (!granted) { + console.log('[TestNotif] Step 2: requesting permission...') + try { + const req = await invoke<{ permissionState: string }>('plugin:notification|request_permissions') + console.log('[TestNotif] request_permissions result:', JSON.stringify(req)) + granted = req?.permissionState === 'granted' + } catch (e) { + console.warn('[TestNotif] request_permissions failed, trying request_permission:', e) + try { + const perm = await invoke('plugin:notification|request_permission') + console.log('[TestNotif] request_permission (Rust):', perm) + granted = perm === 'granted' + } catch (e2) { + console.error('[TestNotif] request_permission also failed:', e2) + } + } + } + } else { + // Desktop: JS wrapper works fine (window.Notification exists on desktop WebView) + try { + const { isPermissionGranted, requestPermission } = await getTauriNotification() + granted = await isPermissionGranted() + console.log('[TestNotif] Desktop isPermissionGranted:', granted) + if (!granted) { + const perm = await requestPermission() + granted = perm === 'granted' + console.log('[TestNotif] Desktop requestPermission:', perm) + } + } catch (e) { + console.warn('[TestNotif] Desktop permission check failed:', e) + granted = true // Desktop usually grants by default + } + } + + console.log('[TestNotif] Permission granted:', granted) if (!granted) { - const perm = await requestPermission() - granted = perm === 'granted' + console.error('[TestNotif] STOPPED: Permission denied') + return } - if (granted) { - sendNotification({ title, body }) + + // ── Step 3: Ensure channel exists (Android only) ── + if (isMobile) { + console.log('[TestNotif] Step 3: ensuring channel exists...') + try { + await invoke('plugin:notification|create_channel', { + id: 'test', + name: 'Test', + description: 'Test notifications', + importance: 4, + visibility: 1, + vibration: true, + lights: true, + }) + console.log('[TestNotif] Channel "test" created/updated') + } catch (e) { + console.warn('[TestNotif] create_channel failed (will use default):', e) + } } + + // ── Step 4: Send notification via invoke ── + console.log('[TestNotif] Step 4: sending via invoke...') + const notifId = Math.floor(Math.random() * 100000) + try { + const result = await invoke('plugin:notification|notify', { + options: { + id: notifId, + title, + body, + channelId: isMobile ? 'test' : undefined, + autoCancel: true, + } + }) + console.log('[TestNotif] notify result:', JSON.stringify(result), 'id:', notifId) + } catch (e) { + console.error('[TestNotif] notify FAILED:', e) + // Fallback: try without channel (uses "default") + if (isMobile) { + console.log('[TestNotif] Retrying without channelId...') + try { + const result2 = await invoke('plugin:notification|notify', { + options: { id: notifId + 1, title, body: body + ' (default channel)', autoCancel: true } + }) + console.log('[TestNotif] notify (default channel) result:', JSON.stringify(result2)) + } catch (e2) { + console.error('[TestNotif] notify (default channel) also FAILED:', e2) + } + } + } + + // ── Step 5: Diagnostic — list active notifications ── + if (isMobile) { + try { + const active = await invoke('plugin:notification|get_active') + console.log('[TestNotif] Active notifications:', JSON.stringify(active)) + } catch (e) { + console.warn('[TestNotif] get_active failed:', e) + } + try { + const channels = await invoke('plugin:notification|listChannels') + console.log('[TestNotif] Channels:', JSON.stringify(channels)) + } catch (e) { + console.warn('[TestNotif] listChannels failed:', e) + } + } + + console.log('[TestNotif] === DONE ===') } catch (e) { - console.warn('[Notification] Tauri plugin failed:', e) + console.error('[TestNotif] Fatal error:', e) } } else if ('Notification' in window) { if (Notification.permission === 'granted') { @@ -404,10 +554,10 @@ if (serverConfig) {