Files

355 lines
11 KiB
JavaScript
Raw Permalink Normal View History

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;
2026-05-19 02:06:52 +03:00
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 '';
// Folder names are shown exactly as they are on disk — no title-casing.
return label.trim();
};
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;
};
2026-05-19 02:06:52 +03:00
const fetchFreeZenModels = async () => [];
const resolveZenModel = async (override) => {
2026-04-30 13:07:37 +03:00
const overrideModel = typeof override === 'string' ? override.trim() : '';
2026-05-19 02:06:52 +03:00
if (overrideModel) return overrideModel;
const settings = await readSettingsFromDisk().catch(() => ({}));
return typeof settings?.zenModel === 'string' && settings.zenModel.trim().length > 0
? settings.zenModel.trim()
: '';
};
2026-05-19 02:06:52 +03:00
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,
2026-05-19 02:06:52 +03:00
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,
};
};