import { ref, computed } from 'vue' import { defineStore } from 'pinia' export interface HookNotification { id: string event: string icon: string title: string detail: string type: 'info' | 'success' | 'warning' | 'error' timestamp: number persistent?: boolean // Permission-specific requestId?: string toolName?: string toolInput?: unknown } export const useClaudeHooksStore = defineStore('claude-hooks', () => { const notifications = ref([]) const MAX_VISIBLE = 5 const visible = computed(() => notifications.value.slice(-MAX_VISIBLE)) function add(notif: HookNotification) { notifications.value.push(notif) if (!notif.persistent) { const duration = notif.type === 'warning' ? 5000 : 3500 setTimeout(() => remove(notif.id), duration) } // Cap total stored if (notifications.value.length > 30) { notifications.value = notifications.value.slice(-20) } } function remove(id: string) { const idx = notifications.value.findIndex(n => n.id === id) if (idx !== -1) notifications.value.splice(idx, 1) } function clear() { notifications.value = [] } // Process a raw claude-hook WS message into a notification (or ignore it) function processHook(data: Record) { const event = data.hook_event_name const toolName = data.tool_name || '' const id = `hook_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` switch (event) { case 'SessionStart': add({ id, event, type: 'info', icon: '', title: 'Session started', detail: data.model ? `Model: ${data.model}` : '', timestamp: Date.now() }) break case 'UserPromptSubmit': { const prompt = data.prompt || '' add({ id, event, type: 'info', icon: '', title: 'Prompt', detail: prompt.length > 100 ? prompt.slice(0, 100) + '...' : prompt, timestamp: Date.now() }) break } case 'PreToolUse': { // Only notify for interesting tools, not reads if (/^(Read|Glob|Grep)$/.test(toolName)) return const input = data.tool_input || {} let detail = '' if (toolName === 'Bash') { const cmd = input.command || '' detail = cmd.length > 80 ? cmd.slice(0, 80) + '...' : cmd } else if (toolName === 'Edit' || toolName === 'Write') { detail = input.file_path ? shortPath(input.file_path) : '' } else if (toolName === 'Task') { detail = input.description || input.prompt?.slice(0, 60) || '' } else if (toolName === 'WebSearch') { detail = input.query || '' } else if (toolName === 'WebFetch') { detail = input.url || '' } else { detail = toolName } add({ id, event, type: 'info', icon: '', title: formatToolName(toolName), detail, timestamp: Date.now() }) break } case 'PostToolUse': { // Skip noisy read tools if (/^(Read|Glob|Grep)$/.test(toolName)) return const response = data.tool_response let detail = '' if (typeof response === 'string') { detail = response.length > 120 ? response.slice(0, 120) + '...' : response } else if (response) { const json = JSON.stringify(response) detail = json.length > 120 ? json.slice(0, 120) + '...' : json } // Clean control chars and excessive whitespace detail = detail.replace(/[\x00-\x1f]+/g, ' ').replace(/\s+/g, ' ').trim() if (!detail) return add({ id, event, type: 'success', icon: '', title: `${toolName} result`, detail, timestamp: Date.now() }) break } case 'Notification': add({ id, event, type: 'warning', icon: '', title: 'Claude notification', detail: data.message || '', timestamp: Date.now() }) break case 'Stop': { const response = data.assistant_response || '' const detail = response.length > 200 ? response.slice(0, 200) + '...' : response add({ id, event, type: 'success', icon: '', title: response ? 'Claude response' : 'Session finished', detail, timestamp: Date.now(), persistent: !!response // Keep visible if there's a response to read }) break } } } // Process a claude-permission WS message function processPermission(data: Record) { const id = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` const toolName = data.tool_name || 'Unknown' const input = data.tool_input || {} let detail = '' if (toolName === 'Bash') { const cmd = input.command || '' detail = cmd.length > 120 ? cmd.slice(0, 120) + '...' : cmd } else if (toolName === 'Edit' || toolName === 'Write') { detail = input.file_path ? shortPath(input.file_path) : '' } else { detail = JSON.stringify(input).slice(0, 100) } add({ id, event: 'PermissionRequest', type: 'error', icon: '', title: `Permission: ${toolName}`, detail, timestamp: Date.now(), persistent: true, requestId: data.requestId, toolName, toolInput: input }) } async function respondPermission(notifId: string, requestId: string, decision: 'allow' | 'deny') { try { await fetch('/api/claude-permission-respond', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId, decision }) }) } catch (e) { console.error('[Hooks] Failed to respond permission:', e) } remove(notifId) } return { notifications, visible, add, remove, clear, processHook, processPermission, respondPermission } }) function shortPath(p: string): string { const parts = p.replace(/\\/g, '/').split('/') if (parts.length <= 3) return parts.join('/') return '.../' + parts.slice(-2).join('/') } function formatToolName(name: string): string { switch (name) { case 'Bash': return 'Running command' case 'Edit': return 'Editing file' case 'Write': return 'Writing file' case 'Task': return 'Spawning agent' case 'WebSearch': return 'Searching web' case 'WebFetch': return 'Fetching URL' case 'NotebookEdit': return 'Editing notebook' default: return `Tool: ${name}` } }