import { summarizeText as summarizeSharedText } from '../text/summarization.js'; export const createNotificationTemplateRuntime = (deps) => { const { readSettingsFromDisk, buildOpenCodeUrl, getOpenCodeAuthHeaders, resolveGitBinaryForSpawn, } = deps; const NOTIFICATION_BODY_MAX_CHARS = 1000; const SESSION_INFO_CACHE_TTL_MS = 60 * 1000; const cachedZenModels = { models: [] }; const sessionTitleCache = new Map(); const sessionInfoCache = new Map(); const createTimeoutSignal = (timeoutMs) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); return { signal: controller.signal, cleanup: () => clearTimeout(timer), }; }; const formatProjectLabel = (label) => { if (!label || typeof label !== 'string') return ''; return label .replace(/[-_]/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()); }; const resolveNotificationTemplate = (template, variables) => { if (!template || typeof template !== 'string') return ''; return template.replace(/\{(\w+)\}/g, (_match, key) => { const value = variables[key]; if (value === undefined || value === null) return ''; return String(value); }); }; const shouldApplyResolvedTemplateMessage = (template, resolved, variables) => { if (!resolved) { return false; } if (typeof template !== 'string') { return true; } if (template.includes('{last_message}')) { return typeof variables?.last_message === 'string' && variables.last_message.trim().length > 0; } return true; }; const fetchFreeZenModels = async () => []; const resolveZenModel = async (override) => { const overrideModel = typeof override === 'string' ? override.trim() : ''; if (overrideModel) return overrideModel; const settings = await readSettingsFromDisk().catch(() => ({})); return typeof settings?.zenModel === 'string' && settings.zenModel.trim().length > 0 ? settings.zenModel.trim() : ''; }; const validateZenModelAtStartup = async () => {}; const summarizeText = async (text, targetLength, zenModel) => { if (!text || typeof text !== 'string' || text.trim().length === 0) return text; const result = await summarizeSharedText({ text, threshold: 0, maxLength: targetLength, zenModel, mode: 'notification', }); return typeof result?.summary === 'string' && result.summary.trim().length > 0 ? result.summary : text; }; const isNotificationTextPart = (part) => { if (!part || typeof part !== 'object') return false; if (part.type !== 'text') return false; return typeof part.text === 'string' || typeof part.content === 'string'; }; const extractTextFromParts = (parts, maxLength = NOTIFICATION_BODY_MAX_CHARS) => { if (!Array.isArray(parts) || parts.length === 0) return ''; const textParts = parts .filter(isNotificationTextPart) .map((part) => part.text || part.content || '') .filter(Boolean); let text = textParts.length > 0 ? textParts.join('\n').trim() : ''; if (maxLength > 0 && text.length > maxLength) { text = text.slice(0, maxLength); } return text; }; const extractLastMessageText = (payload, maxLength = NOTIFICATION_BODY_MAX_CHARS) => { const info = payload?.properties?.info; if (!info) return ''; const parts = info.parts || payload?.properties?.parts; const text = extractTextFromParts(parts, maxLength); if (text) return text; const content = info.content; if (Array.isArray(content)) { const textContent = content .filter(isNotificationTextPart) .map((entry) => entry.text || '') .filter(Boolean); if (textContent.length > 0) { let result = textContent.join('\n').trim(); if (maxLength > 0 && result.length > maxLength) { result = result.slice(0, maxLength); } return result; } } return ''; }; const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS) => { if (!sessionId) return ''; try { const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, ''); const response = await fetch(`${url}?limit=5`, { method: 'GET', headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders(), }, signal: AbortSignal.timeout(3000), }); if (!response.ok) return ''; const messages = await response.json().catch(() => null); if (!Array.isArray(messages)) return ''; let target = null; if (messageId) { target = messages.find((message) => message?.info?.id === messageId && message?.info?.role === 'assistant'); } if (!target) { for (let i = messages.length - 1; i >= 0; i -= 1) { const message = messages[i]; if (message?.info?.role === 'assistant' && message?.info?.finish === 'stop') { target = message; break; } } } if (!target || !Array.isArray(target.parts)) return ''; return extractTextFromParts(target.parts, maxLength); } catch { return ''; } }; const cacheSessionTitle = (sessionId, title) => { if (typeof sessionId === 'string' && sessionId.length > 0 && typeof title === 'string' && title.length > 0) { sessionTitleCache.set(sessionId, title); } }; const getCachedSessionTitle = (sessionId) => { return sessionTitleCache.get(sessionId) ?? null; }; const maybeCacheSessionInfoFromEvent = (payload) => { if (!payload || typeof payload !== 'object') return; const type = payload.type; if (type !== 'session.updated' && type !== 'session.created') return; const info = payload.properties?.info; if (!info || typeof info !== 'object') return; cacheSessionTitle(info.id, info.title); }; const fetchSessionInfo = async (sessionId) => { if (!sessionId) return null; const cached = sessionInfoCache.get(sessionId); if (cached && Date.now() - cached.at < SESSION_INFO_CACHE_TTL_MS) { return cached.data; } try { const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, ''); const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(2000), }); if (!response.ok) { console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`); return null; } const data = await response.json().catch(() => null); if (data && typeof data === 'object') { sessionInfoCache.set(sessionId, { data, at: Date.now() }); return data; } return null; } catch (error) { console.warn(`[Notification] fetchSessionInfo failed for ${sessionId}:`, error?.message || error); return null; } }; const buildTemplateVariables = async (payload, sessionId) => { const info = payload?.properties?.info || {}; let sessionTitle = payload?.properties?.sessionTitle || payload?.properties?.session?.title || (typeof info.sessionTitle === 'string' ? info.sessionTitle : '') || ''; if (!sessionTitle && sessionId) { const cached = getCachedSessionTitle(sessionId); if (cached) { sessionTitle = cached; } } let sessionInfo = null; if (!sessionTitle && sessionId) { sessionInfo = await fetchSessionInfo(sessionId); if (sessionInfo && typeof sessionInfo.title === 'string') { sessionTitle = sessionInfo.title; cacheSessionTitle(sessionId, sessionTitle); } } const agentName = (() => { const mode = typeof info.agent === 'string' && info.agent.trim().length > 0 ? info.agent.trim() : (typeof info.mode === 'string' ? info.mode.trim() : ''); if (!mode) return 'Agent'; return mode.split(/[-_\s]+/).filter(Boolean) .map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(' '); })(); const modelName = (() => { const raw = typeof info.modelID === 'string' ? info.modelID.trim() : (typeof info.model?.modelID === 'string' ? info.model.modelID.trim() : ''); if (!raw) return 'Assistant'; return raw.split(/[-_]+/).filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' '); })(); let projectName = ''; let branch = ''; let worktreeDir = ''; const infoPath = info.path; if (typeof infoPath?.root === 'string' && infoPath.root.length > 0) { worktreeDir = infoPath.root; } else if (typeof infoPath?.cwd === 'string' && infoPath.cwd.length > 0) { worktreeDir = infoPath.cwd; } try { const settings = await readSettingsFromDisk(); const projects = Array.isArray(settings.projects) ? settings.projects : []; if (worktreeDir) { const normalizedDir = worktreeDir.replace(/\/+$/, ''); const matchedProject = projects.find((project) => { if (!project || typeof project.path !== 'string') return false; return project.path.replace(/\/+$/, '') === normalizedDir; }); if (matchedProject && typeof matchedProject.label === 'string' && matchedProject.label.trim().length > 0) { projectName = matchedProject.label.trim(); } else { projectName = normalizedDir.split('/').filter(Boolean).pop() || ''; } } else { const activeId = typeof settings.activeProjectId === 'string' ? settings.activeProjectId : ''; const activeProject = activeId ? projects.find((project) => project && project.id === activeId) : projects[0]; if (activeProject) { projectName = typeof activeProject.label === 'string' && activeProject.label.trim().length > 0 ? activeProject.label.trim() : typeof activeProject.path === 'string' ? activeProject.path.split('/').pop() || '' : ''; worktreeDir = typeof activeProject.path === 'string' ? activeProject.path : ''; } } } catch { if (worktreeDir && !projectName) { projectName = worktreeDir.split('/').filter(Boolean).pop() || ''; } } if (worktreeDir) { try { const { simpleGit } = await import('simple-git'); const git = simpleGit({ baseDir: worktreeDir, spawnOptions: { windowsHide: true }, binary: resolveGitBinaryForSpawn(), }); branch = await Promise.race([ git.revparse(['--abbrev-ref', 'HEAD']), new Promise((_, reject) => setTimeout(() => reject(new Error('git timeout')), 3000)), ]).catch(() => ''); } catch { } } return { project_name: formatProjectLabel(projectName), worktree: worktreeDir, branch: typeof branch === 'string' ? branch.trim() : '', session_name: sessionTitle, agent_name: agentName, model_name: modelName, last_message: '', session_id: sessionId || '', }; }; const getCachedZenModels = () => cachedZenModels; return { createTimeoutSignal, formatProjectLabel, resolveNotificationTemplate, shouldApplyResolvedTemplateMessage, fetchFreeZenModels, resolveZenModel, validateZenModelAtStartup, summarizeText, extractTextFromParts, extractLastMessageText, fetchLastAssistantMessageText, maybeCacheSessionInfoFromEvent, buildTemplateVariables, getCachedZenModels, }; };