feat: Improve notifications with templates, summarization, and more (#317)

* feat: Successfully added new notification settings to Zustand

* feat: Successfully implemented the Events subsection

* feat: Successfully wired new notification settings

* feat: Added notification template editor section to Notifications

* feat: Added server-side template resolution and summarize

* feat: Improve notifications with templates, summarization, and more

* fix: session not populated in messages

* fix notification template first-run defaults

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
gsxdsm
2026-02-09 03:58:29 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 4194cd25d6
commit fbf0ec50ee
7 changed files with 1153 additions and 72 deletions
+114
View File
@@ -16,6 +16,41 @@ export type EventStreamStatus =
| 'offline'
| 'error';
const LEGACY_DEFAULT_NOTIFICATION_TEMPLATES = {
completion: { title: '{agent_name} is ready', message: '{last_message}' },
error: { title: 'Tool error', message: '{last_message}' },
question: { title: '{agent_name} needs input', message: '{last_message}' },
subtask: { title: 'Subtask complete', message: '{last_message}' },
} as const;
const EMPTY_NOTIFICATION_TEMPLATES = {
completion: { title: '', message: '' },
error: { title: '', message: '' },
question: { title: '', message: '' },
subtask: { title: '', message: '' },
} as const;
const isSameTemplateValue = (
a: { title: string; message: string } | undefined,
b: { title: string; message: string }
) => {
if (!a) return false;
return a.title === b.title && a.message === b.message;
};
const isLegacyDefaultTemplates = (value: unknown): boolean => {
if (!value || typeof value !== 'object') {
return false;
}
const candidate = value as Record<string, { title: string; message: string } | undefined>;
return (
isSameTemplateValue(candidate.completion, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.completion)
&& isSameTemplateValue(candidate.error, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.error)
&& isSameTemplateValue(candidate.question, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.question)
&& isSameTemplateValue(candidate.subtask, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.subtask)
);
};
interface UIStore {
theme: 'light' | 'dark' | 'system';
@@ -79,6 +114,25 @@ interface UIStore {
notificationMode: 'always' | 'hidden-only';
notifyOnSubtasks: boolean;
// Event toggles (which events trigger notifications)
notifyOnCompletion: boolean;
notifyOnError: boolean;
notifyOnQuestion: boolean;
// Per-event notification templates
notificationTemplates: {
completion: { title: string; message: string };
error: { title: string; message: string };
question: { title: string; message: string };
subtask: { title: string; message: string };
};
// Summarization settings
summarizeLastMessage: boolean;
summaryThreshold: number; // chars — messages longer than this get summarized
summaryLength: number; // chars — target length for summary
maxLastMessageLength: number; // chars — truncate {last_message} when summarization is off
showTerminalQuickKeysOnDesktop: boolean;
persistChatDraft: boolean;
isMobileSessionStatusBarCollapsed: boolean;
@@ -147,6 +201,14 @@ interface UIStore {
setNotificationMode: (mode: 'always' | 'hidden-only') => void;
setShowTerminalQuickKeysOnDesktop: (value: boolean) => void;
setNotifyOnSubtasks: (value: boolean) => void;
setNotifyOnCompletion: (value: boolean) => void;
setNotifyOnError: (value: boolean) => void;
setNotifyOnQuestion: (value: boolean) => void;
setNotificationTemplates: (templates: UIStore['notificationTemplates']) => void;
setSummarizeLastMessage: (value: boolean) => void;
setSummaryThreshold: (value: number) => void;
setSummaryLength: (value: number) => void;
setMaxLastMessageLength: (value: number) => void;
setPersistChatDraft: (value: boolean) => void;
setIsMobileSessionStatusBarCollapsed: (value: boolean) => void;
openMultiRunLauncher: () => void;
@@ -217,6 +279,23 @@ export const useUIStore = create<UIStore>()(
notificationMode: 'hidden-only',
notifyOnSubtasks: true,
// Event toggles (which events trigger notifications)
notifyOnCompletion: true,
notifyOnError: true,
notifyOnQuestion: true,
notificationTemplates: {
completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion },
error: { ...EMPTY_NOTIFICATION_TEMPLATES.error },
question: { ...EMPTY_NOTIFICATION_TEMPLATES.question },
subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask },
},
// Summarization settings
summarizeLastMessage: false,
summaryThreshold: 200,
summaryLength: 100,
maxLastMessageLength: 250,
showTerminalQuickKeysOnDesktop: false,
persistChatDraft: true,
isMobileSessionStatusBarCollapsed: false,
@@ -784,6 +863,14 @@ export const useUIStore = create<UIStore>()(
set({ notifyOnSubtasks: value });
},
setNotifyOnCompletion: (value) => { set({ notifyOnCompletion: value }); },
setNotifyOnError: (value) => { set({ notifyOnError: value }); },
setNotifyOnQuestion: (value) => { set({ notifyOnQuestion: value }); },
setNotificationTemplates: (templates) => { set({ notificationTemplates: templates }); },
setSummarizeLastMessage: (value) => { set({ summarizeLastMessage: value }); },
setSummaryThreshold: (value) => { set({ summaryThreshold: value }); },
setSummaryLength: (value) => { set({ summaryLength: value }); },
setMaxLastMessageLength: (value) => { set({ maxLastMessageLength: value }); },
setPersistChatDraft: (value) => {
set({ persistChatDraft: value });
},
@@ -794,6 +881,25 @@ export const useUIStore = create<UIStore>()(
{
name: 'ui-store',
storage: createJSONStorage(() => getSafeStorage()),
version: 1,
migrate: (persistedState, version) => {
if (version >= 1 || !persistedState || typeof persistedState !== 'object') {
return persistedState;
}
const state = persistedState as Record<string, unknown>;
if (!isLegacyDefaultTemplates(state.notificationTemplates)) {
return persistedState;
}
return {
...state,
notificationTemplates: {
completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion },
error: { ...EMPTY_NOTIFICATION_TEMPLATES.error },
question: { ...EMPTY_NOTIFICATION_TEMPLATES.question },
subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask },
},
};
},
partialize: (state) => ({
theme: state.theme,
isSidebarOpen: state.isSidebarOpen,
@@ -831,6 +937,14 @@ export const useUIStore = create<UIStore>()(
notificationMode: state.notificationMode,
showTerminalQuickKeysOnDesktop: state.showTerminalQuickKeysOnDesktop,
notifyOnSubtasks: state.notifyOnSubtasks,
notifyOnCompletion: state.notifyOnCompletion,
notifyOnError: state.notifyOnError,
notifyOnQuestion: state.notifyOnQuestion,
notificationTemplates: state.notificationTemplates,
summarizeLastMessage: state.summarizeLastMessage,
summaryThreshold: state.summaryThreshold,
summaryLength: state.summaryLength,
maxLastMessageLength: state.maxLastMessageLength,
persistChatDraft: state.persistChatDraft,
isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed,
})