Files
tolaria/src/hooks/useAiActivity.ts
lucaronin 93d598d04c fix: MCP UI tools (highlight, open_note) now work in real-time
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>
2026-03-04 10:05:43 +01:00

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 }
}