diff --git a/dcp.schema.json b/dcp.schema.json index a9bfed22..d044098f 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -26,6 +26,12 @@ "default": "detailed", "description": "Level of notification shown when pruning occurs" }, + "pruneNotificationType": { + "type": "string", + "enum": ["chat", "toast"], + "default": "chat", + "description": "Where to display prune notifications (chat message or toast notification)" + }, "commands": { "type": "object", "description": "Configuration for DCP slash commands (/dcp)", diff --git a/lib/config.ts b/lib/config.ts index 6ec97640..bfac6dbd 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -60,6 +60,7 @@ export interface PluginConfig { enabled: boolean debug: boolean pruneNotification: "off" | "minimal" | "detailed" + pruneNotificationType: "chat" | "toast" commands: Commands turnProtection: TurnProtection protectedFilePatterns: string[] @@ -91,6 +92,7 @@ export const VALID_CONFIG_KEYS = new Set([ "debug", "showUpdateToasts", // Deprecated but kept for backwards compatibility "pruneNotification", + "pruneNotificationType", "turnProtection", "turnProtection.enabled", "turnProtection.turns", @@ -173,6 +175,17 @@ function validateConfigTypes(config: Record): ValidationError[] { } } + if (config.pruneNotificationType !== undefined) { + const validValues = ["chat", "toast"] + if (!validValues.includes(config.pruneNotificationType)) { + errors.push({ + key: "pruneNotificationType", + expected: '"chat" | "toast"', + actual: JSON.stringify(config.pruneNotificationType), + }) + } + } + if (config.protectedFilePatterns !== undefined) { if (!Array.isArray(config.protectedFilePatterns)) { errors.push({ @@ -454,6 +467,7 @@ const defaultConfig: PluginConfig = { enabled: true, debug: false, pruneNotification: "detailed", + pruneNotificationType: "chat", commands: { enabled: true, protectedTools: [...DEFAULT_PROTECTED_TOOLS], @@ -732,6 +746,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + pruneNotificationType: + result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, @@ -775,6 +791,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + pruneNotificationType: + result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, @@ -815,6 +833,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruneNotification: result.data.pruneNotification ?? config.pruneNotification, + pruneNotificationType: + result.data.pruneNotificationType ?? config.pruneNotificationType, commands: mergeCommands(config.commands, result.data.commands as any), turnProtection: { enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled, diff --git a/lib/ui/notification.ts b/lib/ui/notification.ts index ec6d399b..9d628175 100644 --- a/lib/ui/notification.ts +++ b/lib/ui/notification.ts @@ -63,6 +63,42 @@ function buildDetailedMessage( return (message + formatExtracted(showDistillation ? distillation : undefined)).trim() } +const TOAST_BODY_MAX_LINES = 12 +const TOAST_SUMMARY_MAX_CHARS = 600 + +function truncateToastBody(body: string, maxLines: number = TOAST_BODY_MAX_LINES): string { + const lines = body.split("\n") + if (lines.length <= maxLines) { + return body + } + const kept = lines.slice(0, maxLines - 1) + const remaining = lines.length - maxLines + 1 + return kept.join("\n") + `\n... and ${remaining} more` +} + +function truncateToastSummary(summary: string, maxChars: number = TOAST_SUMMARY_MAX_CHARS): string { + if (summary.length <= maxChars) { + return summary + } + return summary.slice(0, maxChars - 3) + "..." +} + +function truncateExtractedSection( + message: string, + maxChars: number = TOAST_SUMMARY_MAX_CHARS, +): string { + const marker = "\n\n▣ Extracted" + const index = message.indexOf(marker) + if (index === -1) { + return message + } + const extracted = message.slice(index) + if (extracted.length <= maxChars) { + return message + } + return message.slice(0, index) + truncateToastSummary(extracted, maxChars) +} + export async function sendUnifiedNotification( client: any, logger: Logger, @@ -100,6 +136,22 @@ export async function sendUnifiedNotification( showDistillation, ) + if (config.pruneNotificationType === "toast") { + let toastMessage = truncateExtractedSection(message) + toastMessage = + config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage) + + await client.tui.showToast({ + body: { + title: "DCP: Prune Notification", + message: toastMessage, + variant: "info", + duration: 5000, + }, + }) + return true + } + await sendIgnoredMessage(client, sessionId, message, params, logger) return true } @@ -150,6 +202,31 @@ export async function sendCompressNotification( } } + if (config.pruneNotificationType === "toast") { + let toastMessage = message + if (config.tools.compress.showCompression) { + const truncatedSummary = truncateToastSummary(summary) + if (truncatedSummary !== summary) { + toastMessage = toastMessage.replace( + `\n→ Compression: ${summary}`, + `\n→ Compression: ${truncatedSummary}`, + ) + } + } + toastMessage = + config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage) + + await client.tui.showToast({ + body: { + title: "DCP: Compress Notification", + message: toastMessage, + variant: "info", + duration: 5000, + }, + }) + return true + } + await sendIgnoredMessage(client, sessionId, message, params, logger) return true }