feat: show session name in notification and don't reload tab on clicking

--- the notification
This commit is contained in:
Haileyesus
2026-03-13 18:54:31 +03:00
parent ae494ea383
commit d7906cd928
8 changed files with 131 additions and 20 deletions

View File

@@ -68,7 +68,7 @@ self.addEventListener('push', event => {
icon: '/logo-256.png', icon: '/logo-256.png',
badge: '/logo-128.png', badge: '/logo-128.png',
data: payload.data || {}, data: payload.data || {},
tag: payload.data?.code || 'default', tag: payload.data?.tag || `${payload.data?.sessionId || 'global'}:${payload.data?.code || 'default'}`,
renotify: true renotify: true
}; };
@@ -82,16 +82,20 @@ self.addEventListener('notificationclick', event => {
event.notification.close(); event.notification.close();
const sessionId = event.notification.data?.sessionId; const sessionId = event.notification.data?.sessionId;
const provider = event.notification.data?.provider || null;
const urlPath = sessionId ? `/session/${sessionId}` : '/'; const urlPath = sessionId ? `/session/${sessionId}` : '/';
event.waitUntil( event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => { self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async clientList => {
for (const client of clientList) { for (const client of clientList) {
if (client.url.includes(self.location.origin)) { if (client.url.includes(self.location.origin)) {
client.focus(); await client.focus();
if (sessionId) { client.postMessage({
client.navigate(self.location.origin + urlPath); type: 'notification:navigate',
} sessionId: sessionId || null,
provider,
urlPath
});
return; return;
} }
} }

View File

@@ -467,7 +467,7 @@ async function loadMcpConfig(cwd) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function queryClaudeSDK(command, options = {}, ws) { async function queryClaudeSDK(command, options = {}, ws) {
const { sessionId } = options; const { sessionId, sessionSummary } = options;
let capturedSessionId = sessionId; let capturedSessionId = sessionId;
let sessionCreatedSent = false; let sessionCreatedSent = false;
let tempImagePaths = []; let tempImagePaths = [];
@@ -507,7 +507,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
sessionId: capturedSessionId || sessionId || null, sessionId: capturedSessionId || sessionId || null,
kind: 'action_required', kind: 'action_required',
code: 'agent.notification', code: 'agent.notification',
meta: { message }, meta: { message, sessionName: sessionSummary },
severity: 'warning', severity: 'warning',
requiresUserAction: true, requiresUserAction: true,
dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}` dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
@@ -553,7 +553,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
sessionId: capturedSessionId || sessionId || null, sessionId: capturedSessionId || sessionId || null,
kind: 'action_required', kind: 'action_required',
code: 'permission.required', code: 'permission.required',
meta: { toolName }, meta: { toolName, sessionName: sessionSummary },
severity: 'warning', severity: 'warning',
requiresUserAction: true, requiresUserAction: true,
dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}` dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
@@ -707,6 +707,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'claude', provider: 'claude',
sessionId: capturedSessionId || sessionId || null, sessionId: capturedSessionId || sessionId || null,
sessionName: sessionSummary,
stopReason: 'completed' stopReason: 'completed'
}); });
console.log('claude-complete event sent'); console.log('claude-complete event sent');
@@ -732,6 +733,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'claude', provider: 'claude',
sessionId: capturedSessionId || sessionId || null, sessionId: capturedSessionId || sessionId || null,
sessionName: sessionSummary,
error error
}); });

View File

@@ -24,7 +24,7 @@ function isWorkspaceTrustPrompt(text = '') {
async function spawnCursor(command, options = {}, ws) { async function spawnCursor(command, options = {}, ws) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model } = options; const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
let capturedSessionId = sessionId; // Track session ID throughout the process let capturedSessionId = sessionId; // Track session ID throughout the process
let sessionCreatedSent = false; // Track if we've already sent session-created event let sessionCreatedSent = false; // Track if we've already sent session-created event
let hasRetriedWithTrust = false; let hasRetriedWithTrust = false;
@@ -97,6 +97,7 @@ async function spawnCursor(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'cursor', provider: 'cursor',
sessionId: finalSessionId, sessionId: finalSessionId,
sessionName: sessionSummary,
stopReason: 'completed' stopReason: 'completed'
}); });
return; return;
@@ -106,6 +107,7 @@ async function spawnCursor(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'cursor', provider: 'cursor',
sessionId: finalSessionId, sessionId: finalSessionId,
sessionName: sessionSummary,
error: error || `Cursor CLI exited with code ${code}` error: error || `Cursor CLI exited with code ${code}`
}); });
}; };

View File

@@ -14,7 +14,7 @@ import { notifyRunFailed, notifyRunStopped } from './services/notification-orche
let activeGeminiProcesses = new Map(); // Track active processes by session ID let activeGeminiProcesses = new Map(); // Track active processes by session ID
async function spawnGemini(command, options = {}, ws) { async function spawnGemini(command, options = {}, ws) {
const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images } = options; const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images, sessionSummary } = options;
let capturedSessionId = sessionId; // Track session ID throughout the process let capturedSessionId = sessionId; // Track session ID throughout the process
let sessionCreatedSent = false; // Track if we've already sent session-created event let sessionCreatedSent = false; // Track if we've already sent session-created event
let assistantBlocks = []; // Accumulate the full response blocks including tools let assistantBlocks = []; // Accumulate the full response blocks including tools
@@ -189,6 +189,7 @@ async function spawnGemini(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'gemini', provider: 'gemini',
sessionId: finalSessionId, sessionId: finalSessionId,
sessionName: sessionSummary,
stopReason: 'completed' stopReason: 'completed'
}); });
return; return;
@@ -198,6 +199,7 @@ async function spawnGemini(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'gemini', provider: 'gemini',
sessionId: finalSessionId, sessionId: finalSessionId,
sessionName: sessionSummary,
error: error || terminalFailureReason || `Gemini CLI exited with code ${code}` error: error || terminalFailureReason || `Gemini CLI exited with code ${code}`
}); });
}; };

View File

@@ -192,6 +192,7 @@ function mapPermissionModeToCodexOptions(permissionMode) {
export async function queryCodex(command, options = {}, ws) { export async function queryCodex(command, options = {}, ws) {
const { const {
sessionId, sessionId,
sessionSummary,
cwd, cwd,
projectPath, projectPath,
model, model,
@@ -276,6 +277,7 @@ export async function queryCodex(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'codex', provider: 'codex',
sessionId: currentSessionId, sessionId: currentSessionId,
sessionName: sessionSummary,
error: terminalFailure error: terminalFailure
}); });
} }
@@ -306,6 +308,7 @@ export async function queryCodex(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'codex', provider: 'codex',
sessionId: currentSessionId, sessionId: currentSessionId,
sessionName: sessionSummary,
stopReason: 'completed' stopReason: 'completed'
}); });
} }
@@ -330,6 +333,7 @@ export async function queryCodex(command, options = {}, ws) {
userId: ws?.userId || null, userId: ws?.userId || null,
provider: 'codex', provider: 'codex',
sessionId: currentSessionId, sessionId: currentSessionId,
sessionName: sessionSummary,
error error
}); });
} }

View File

@@ -1,5 +1,5 @@
import webPush from 'web-push'; import webPush from 'web-push';
import { notificationPreferencesDb, pushSubscriptionsDb } from '../database/db.js'; import { notificationPreferencesDb, pushSubscriptionsDb, sessionNamesDb } from '../database/db.js';
const KIND_TO_PREF_KEY = { const KIND_TO_PREF_KEY = {
action_required: 'actionRequired', action_required: 'actionRequired',
@@ -7,6 +7,14 @@ const KIND_TO_PREF_KEY = {
error: 'error' error: 'error'
}; };
const PROVIDER_LABELS = {
claude: 'Claude',
cursor: 'Cursor',
codex: 'Codex',
gemini: 'Gemini',
system: 'System'
};
const recentEventKeys = new Map(); const recentEventKeys = new Map();
const DEDUPE_WINDOW_MS = 20000; const DEDUPE_WINDOW_MS = 20000;
@@ -76,6 +84,32 @@ function normalizeErrorMessage(error) {
return String(error); return String(error);
} }
function normalizeSessionName(sessionName) {
if (typeof sessionName !== 'string') {
return null;
}
const normalized = sessionName.replace(/\s+/g, ' ').trim();
if (!normalized) {
return null;
}
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
}
function resolveSessionName(event) {
const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
if (explicitSessionName) {
return explicitSessionName;
}
if (!event.sessionId || !event.provider) {
return null;
}
return normalizeSessionName(sessionNamesDb.getName(event.sessionId, event.provider));
}
function buildPushBody(event) { function buildPushBody(event) {
const CODE_MAP = { const CODE_MAP = {
'permission.required': event.meta?.toolName 'permission.required': event.meta?.toolName
@@ -86,13 +120,19 @@ function buildPushBody(event) {
'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification', 'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification',
'push.enabled': 'Push notifications are now enabled!' 'push.enabled': 'Push notifications are now enabled!'
}; };
const providerLabel = PROVIDER_LABELS[event.provider] || 'Assistant';
const sessionName = resolveSessionName(event);
const message = CODE_MAP[event.code] || 'You have a new notification';
return { return {
title: 'Claude Code UI', title: sessionName || 'Claude Code UI',
body: CODE_MAP[event.code] || 'You have a new notification', body: `${providerLabel}: ${message}`,
data: { data: {
sessionId: event.sessionId || null, sessionId: event.sessionId || null,
code: event.code code: event.code,
provider: event.provider || null,
sessionName,
tag: `${event.provider || 'assistant'}:${event.sessionId || 'none'}:${event.code}`
} }
}; };
} }
@@ -147,7 +187,7 @@ function notifyUserIfEnabled({ userId, event }) {
}); });
} }
function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed' }) { function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
notifyUserIfEnabled({ notifyUserIfEnabled({
userId, userId,
event: createNotificationEvent({ event: createNotificationEvent({
@@ -155,14 +195,14 @@ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'co
sessionId, sessionId,
kind: 'stop', kind: 'stop',
code: 'run.stopped', code: 'run.stopped',
meta: { stopReason }, meta: { stopReason, sessionName },
severity: 'info', severity: 'info',
dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}` dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
}) })
}); });
} }
function notifyRunFailed({ userId, provider, sessionId = null, error }) { function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
const errorMessage = normalizeErrorMessage(error); const errorMessage = normalizeErrorMessage(error);
notifyUserIfEnabled({ notifyUserIfEnabled({
@@ -172,7 +212,7 @@ function notifyRunFailed({ userId, provider, sessionId = null, error }) {
sessionId, sessionId,
kind: 'error', kind: 'error',
code: 'run.failed', code: 'run.failed',
meta: { error: errorMessage }, meta: { error: errorMessage, sessionName },
severity: 'error', severity: 'error',
dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}` dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
}) })

View File

@@ -72,6 +72,40 @@ export default function AppContent() {
}; };
}, [openSettings]); }, [openSettings]);
useEffect(() => {
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
return undefined;
}
const handleServiceWorkerMessage = (event: MessageEvent) => {
const message = event.data;
if (!message || message.type !== 'notification:navigate') {
return;
}
if (typeof message.provider === 'string' && message.provider.trim()) {
localStorage.setItem('selected-provider', message.provider);
}
setActiveTab('chat');
setSidebarOpen(false);
void refreshProjectsSilently();
if (typeof message.sessionId === 'string' && message.sessionId) {
navigate(`/session/${message.sessionId}`);
return;
}
navigate('/');
};
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
return () => {
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage);
};
}, [navigate, refreshProjectsSilently, setActiveTab, setSidebarOpen]);
// Permission recovery: query pending permissions on WebSocket reconnect or session change // Permission recovery: query pending permissions on WebSocket reconnect or session change
useEffect(() => { useEffect(() => {
const isReconnect = isConnected && !wasConnectedRef.current; const isReconnect = isConnected && !wasConnectedRef.current;

View File

@@ -82,6 +82,24 @@ const createFakeSubmitEvent = () => {
const isTemporarySessionId = (sessionId: string | null | undefined) => const isTemporarySessionId = (sessionId: string | null | undefined) =>
Boolean(sessionId && sessionId.startsWith('new-session-')); Boolean(sessionId && sessionId.startsWith('new-session-'));
const getNotificationSessionSummary = (
selectedSession: ProjectSession | null,
fallbackInput: string,
): string | null => {
const sessionSummary = selectedSession?.summary || selectedSession?.name || selectedSession?.title;
if (typeof sessionSummary === 'string' && sessionSummary.trim()) {
const normalized = sessionSummary.replace(/\s+/g, ' ').trim();
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
}
const normalizedFallback = fallbackInput.replace(/\s+/g, ' ').trim();
if (!normalizedFallback) {
return null;
}
return normalizedFallback.length > 80 ? `${normalizedFallback.slice(0, 77)}...` : normalizedFallback;
};
export function useChatComposerState({ export function useChatComposerState({
selectedProject, selectedProject,
selectedSession, selectedSession,
@@ -603,6 +621,7 @@ export function useChatComposerState({
const toolsSettings = getToolsSettings(); const toolsSettings = getToolsSettings();
const resolvedProjectPath = selectedProject.fullPath || selectedProject.path || ''; const resolvedProjectPath = selectedProject.fullPath || selectedProject.path || '';
const sessionSummary = getNotificationSessionSummary(selectedSession, currentInput);
if (provider === 'cursor') { if (provider === 'cursor') {
sendMessage({ sendMessage({
@@ -616,6 +635,7 @@ export function useChatComposerState({
resume: Boolean(effectiveSessionId), resume: Boolean(effectiveSessionId),
model: cursorModel, model: cursorModel,
skipPermissions: toolsSettings?.skipPermissions || false, skipPermissions: toolsSettings?.skipPermissions || false,
sessionSummary,
toolsSettings, toolsSettings,
}, },
}); });
@@ -630,6 +650,7 @@ export function useChatComposerState({
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
resume: Boolean(effectiveSessionId), resume: Boolean(effectiveSessionId),
model: codexModel, model: codexModel,
sessionSummary,
permissionMode: permissionMode === 'plan' ? 'default' : permissionMode, permissionMode: permissionMode === 'plan' ? 'default' : permissionMode,
}, },
}); });
@@ -644,6 +665,7 @@ export function useChatComposerState({
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
resume: Boolean(effectiveSessionId), resume: Boolean(effectiveSessionId),
model: geminiModel, model: geminiModel,
sessionSummary,
permissionMode, permissionMode,
toolsSettings, toolsSettings,
}, },
@@ -660,6 +682,7 @@ export function useChatComposerState({
toolsSettings, toolsSettings,
permissionMode, permissionMode,
model: claudeModel, model: claudeModel,
sessionSummary,
images: uploadedImages, images: uploadedImages,
}, },
}); });
@@ -681,6 +704,7 @@ export function useChatComposerState({
safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`); safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`);
}, },
[ [
selectedSession,
attachedImages, attachedImages,
claudeModel, claudeModel,
codexModel, codexModel,
@@ -697,7 +721,6 @@ export function useChatComposerState({
resetCommandMenuState, resetCommandMenuState,
scrollToBottom, scrollToBottom,
selectedProject, selectedProject,
selectedSession?.id,
sendMessage, sendMessage,
setCanAbortSession, setCanAbortSession,
setChatMessages, setChatMessages,