Root causes: - index.js tried to start its own UI bridge server on port 9711, but ws-bridge.js (spawned by Tauri) already owns it → broadcastUiAction was a no-op. Fixed by connecting index.js as a WebSocket CLIENT that sends messages through the existing bridge. - ws-bridge.js UI bridge had no relay — client messages weren't forwarded. Added relay so messages from the MCP server reach the React frontend. - useAiActivity hook existed but was never imported in App.tsx. - useAiActivity only handled highlight, not open_note/open_tab/set_filter. - No vault_changed events after write operations. - set_filter payload used `type` key which overwrote `type: 'ui_action'`. Changes: - mcp-server/index.js: connect as WS client instead of starting server; broadcast vault_changed after all write operations - mcp-server/ws-bridge.js: add message relay in UI bridge; broadcast vault_changed after write operations; fix set_filter payload key - useAiActivity: handle all UI actions (highlight, open_note, open_tab, set_filter, vault_changed); accept callbacks; auto-reconnect on close - App.tsx: wire useAiActivity into vault/notes/selection actions; apply ai-highlight CSS class to editor and note list panels - App.css: add ai-highlight-glow keyframe animation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
|
export type HighlightElement = 'editor' | 'tab' | 'properties' | 'notelist' | null
|
|
|
|
export interface AiActivity {
|
|
highlightElement: HighlightElement
|
|
highlightPath: string | null
|
|
}
|
|
|
|
export interface AiActivityCallbacks {
|
|
onOpenNote?: (path: string) => void
|
|
onOpenTab?: (path: string) => void
|
|
onSetFilter?: (type: string) => void
|
|
onVaultChanged?: (path?: string) => void
|
|
}
|
|
|
|
const WS_UI_URL = 'ws://localhost:9711'
|
|
const HIGHLIGHT_DURATION_MS = 800
|
|
const RECONNECT_DELAY_MS = 3000
|
|
|
|
/**
|
|
* Listens on the UI WebSocket bridge (port 9711) for UI action events
|
|
* from the MCP server. Handles highlight, open_note, open_tab, set_filter,
|
|
* and vault_changed actions.
|
|
*/
|
|
export function useAiActivity(callbacks?: AiActivityCallbacks): AiActivity {
|
|
const [highlightElement, setHighlightElement] = useState<HighlightElement>(null)
|
|
const [highlightPath, setHighlightPath] = useState<string | null>(null)
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const callbacksRef = useRef(callbacks)
|
|
useEffect(() => { callbacksRef.current = callbacks })
|
|
|
|
const handleMessage = useCallback((event: MessageEvent) => {
|
|
try {
|
|
const data = JSON.parse(event.data as string)
|
|
if (data.type !== 'ui_action') return
|
|
switch (data.action) {
|
|
case 'highlight':
|
|
setHighlightElement(data.element ?? null)
|
|
setHighlightPath(data.path ?? null)
|
|
if (timerRef.current) clearTimeout(timerRef.current)
|
|
timerRef.current = setTimeout(() => {
|
|
setHighlightElement(null)
|
|
setHighlightPath(null)
|
|
}, HIGHLIGHT_DURATION_MS)
|
|
break
|
|
case 'open_note':
|
|
if (data.path) callbacksRef.current?.onOpenNote?.(data.path)
|
|
break
|
|
case 'open_tab':
|
|
if (data.path) callbacksRef.current?.onOpenTab?.(data.path)
|
|
break
|
|
case 'set_filter':
|
|
if (data.filterType) callbacksRef.current?.onSetFilter?.(data.filterType)
|
|
break
|
|
case 'vault_changed':
|
|
callbacksRef.current?.onVaultChanged?.(data.path)
|
|
break
|
|
}
|
|
} catch {
|
|
// Ignore parse errors from malformed messages
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
let ws: WebSocket | null = null
|
|
let mounted = true
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
function connect() {
|
|
if (!mounted) return
|
|
try {
|
|
ws = new WebSocket(WS_UI_URL)
|
|
ws.onmessage = handleMessage
|
|
ws.onclose = () => {
|
|
if (mounted) reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS)
|
|
}
|
|
ws.onerror = () => { /* Silent — bridge may not be running */ }
|
|
} catch {
|
|
if (mounted) reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS)
|
|
}
|
|
}
|
|
|
|
connect()
|
|
|
|
return () => {
|
|
mounted = false
|
|
ws?.close()
|
|
if (timerRef.current) clearTimeout(timerRef.current)
|
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
}
|
|
}, [handleMessage])
|
|
|
|
return { highlightElement, highlightPath }
|
|
}
|