diff --git a/src/renderer/components/settings/sections/NotificationsSection.tsx b/src/renderer/components/settings/sections/NotificationsSection.tsx index c2bca470..c1766eb7 100644 --- a/src/renderer/components/settings/sections/NotificationsSection.tsx +++ b/src/renderer/components/settings/sections/NotificationsSection.tsx @@ -64,6 +64,34 @@ export const NotificationsSection = ({ }: NotificationsSectionProps): React.JSX.Element => { return (
+ {/* Task Completion Notifications */} + +
+

+ Get native OS notifications when Claude finishes tasks — sounds, banners, and Dock/taskbar + badges. Works on macOS, Linux, and Windows. +

+ +
+ {/* Notification Triggers */} - -
-

- Get native OS notifications when Claude finishes tasks — sounds, banners, and Dock/taskbar - badges. Works on macOS, Linux, and Windows. -

- -
-

Notifications from these repositories will be ignored diff --git a/src/renderer/components/team/CollapsibleTeamSection.tsx b/src/renderer/components/team/CollapsibleTeamSection.tsx index e468328e..ded3ec17 100644 --- a/src/renderer/components/team/CollapsibleTeamSection.tsx +++ b/src/renderer/components/team/CollapsibleTeamSection.tsx @@ -49,17 +49,13 @@ export const CollapsibleTeamSection = ({ {badge} )} - {secondaryBadge != null && secondaryBadge >= 0 && ( + {secondaryBadge != null && secondaryBadge > 0 && ( 0 - ? 'bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-normal leading-none text-blue-400' - : 'px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]' - } - title={secondaryBadge > 0 ? `${secondaryBadge} unread` : undefined} + className="bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-normal leading-none text-blue-400" + title={`${secondaryBadge} unread`} > - {secondaryBadge} + {secondaryBadge} new )}

diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index b389b218..04885796 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -17,7 +17,17 @@ import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { buildTaskCountsByOwner } from '@renderer/utils/pathNormalize'; import { toMessageKey } from '@renderer/utils/teamMessageKey'; -import { GitBranch, Pencil, Play, Plus, Search, Trash2, UserPlus, X } from 'lucide-react'; +import { + CheckCheck, + GitBranch, + Pencil, + Play, + Plus, + Search, + Trash2, + UserPlus, + X, +} from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ActiveTasksBlock } from './activity/ActiveTasksBlock'; @@ -56,6 +66,7 @@ interface CreateTaskDialogState { defaultSubject: string; defaultDescription: string; defaultOwner: string; + defaultStartImmediately?: boolean; } interface TimeWindow { @@ -342,15 +353,21 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele return list; }, [data, timeWindow, messagesFilter, messagesSearchQuery]); - const { readSet, markRead } = useTeamMessagesRead(teamName ?? ''); + const { readSet, markRead, markAllRead } = useTeamMessagesRead(teamName ?? ''); const messagesUnreadCount = useMemo( - () => filteredMessages.filter((m) => !readSet.has(toMessageKey(m))).length, + () => filteredMessages.filter((m) => !m.read && !readSet.has(toMessageKey(m))).length, [filteredMessages, readSet] ); const handleMessageVisible = useCallback( (message: InboxMessage) => markRead(toMessageKey(message)), [markRead] ); + const handleMarkAllRead = useCallback(() => { + const keys = filteredMessages + .filter((m) => !m.read && !readSet.has(toMessageKey(m))) + .map((m) => toMessageKey(m)); + markAllRead(keys); + }, [filteredMessages, readSet, markAllRead]); const kanbanDisplayTasks = useMemo(() => { const query = kanbanSearch.trim(); @@ -385,12 +402,18 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele if (changed) setPendingRepliesByMember(next); }, [data, pendingRepliesByMember]); - const openCreateTaskDialog = (subject = '', description = '', owner = ''): void => { + const openCreateTaskDialog = ( + subject = '', + description = '', + owner = '', + startImmediately?: boolean + ): void => { setCreateTaskDialog({ open: true, defaultSubject: subject, defaultDescription: description, defaultOwner: owner, + defaultStartImmediately: startImmediately, }); }; @@ -400,6 +423,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele defaultSubject: '', defaultDescription: '', defaultOwner: '', + defaultStartImmediately: undefined, }); }; @@ -768,6 +792,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele } }} onTaskClick={(task) => setSelectedTask(task)} + onAddTask={(startImmediately) => openCreateTaskDialog('', '', '', startImmediately)} /> @@ -780,6 +805,23 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele defaultOpen action={
+ {messagesUnreadCount > 0 && ( + + + + + Mark all as read + + )}
void; onSubmit: ( subject: string, @@ -60,6 +61,7 @@ export const CreateTaskDialog = ({ defaultSubject = '', defaultDescription = '', defaultOwner = '', + defaultStartImmediately, onClose, onSubmit, submitting = false, @@ -86,7 +88,7 @@ export const CreateTaskDialog = ({ setOwner(defaultOwner); setBlockedBy([]); setRelated([]); - setStartImmediately(isTeamAlive); + setStartImmediately(defaultStartImmediately ?? isTeamAlive); promptDraft.clearDraft(); setBlockedBySearch(''); setRelatedSearch(''); diff --git a/src/renderer/components/team/kanban/KanbanBoard.tsx b/src/renderer/components/team/kanban/KanbanBoard.tsx index 8da4c114..b1d4b380 100644 --- a/src/renderer/components/team/kanban/KanbanBoard.tsx +++ b/src/renderer/components/team/kanban/KanbanBoard.tsx @@ -78,6 +78,8 @@ interface KanbanBoardProps { onColumnOrderChange?: (columnId: KanbanColumnId, orderedTaskIds: string[]) => void; /** Слот слева в одной строке с фильтром и переключателем вида (например, поле поиска). */ toolbarLeft?: React.ReactNode; + /** Opens the create-task dialog with pre-set startImmediately value. */ + onAddTask?: (startImmediately: boolean) => void; } type KanbanViewMode = 'grid' | 'columns'; @@ -218,6 +220,7 @@ export const KanbanBoard = ({ onTaskClick, onColumnOrderChange, toolbarLeft, + onAddTask, }: KanbanBoardProps): React.JSX.Element => { const [viewMode, setViewMode] = useState('grid'); @@ -395,6 +398,12 @@ export const KanbanBoard = ({ {COLUMNS.map((column) => { const columnTasks = groupedOrdered.get(column.id) ?? []; const accent = COLUMN_ACCENTS[column.id]; + const addHandler = + onAddTask && column.id === 'todo' + ? () => onAddTask(false) + : onAddTask && column.id === 'in_progress' + ? () => onAddTask(true) + : undefined; return ( {renderCards(column.id, columnTasks)} @@ -414,6 +424,12 @@ export const KanbanBoard = ({ {COLUMNS.map((column) => { const columnTasks = groupedOrdered.get(column.id) ?? []; const accent = COLUMN_ACCENTS[column.id]; + const addHandler = + onAddTask && column.id === 'todo' + ? () => onAddTask(false) + : onAddTask && column.id === 'in_progress' + ? () => onAddTask(true) + : undefined; return (
{renderCards(column.id, columnTasks)} diff --git a/src/renderer/components/team/kanban/KanbanColumn.tsx b/src/renderer/components/team/kanban/KanbanColumn.tsx index 61a9bf03..0b6b7fe9 100644 --- a/src/renderer/components/team/kanban/KanbanColumn.tsx +++ b/src/renderer/components/team/kanban/KanbanColumn.tsx @@ -1,5 +1,6 @@ import { Badge } from '@renderer/components/ui/badge'; import { cn } from '@renderer/lib/utils'; +import { Plus } from 'lucide-react'; interface KanbanColumnProps { title: string; @@ -7,6 +8,7 @@ interface KanbanColumnProps { icon?: React.ReactNode; headerBg?: string; bodyBg?: string; + onAddTask?: () => void; children: React.ReactNode; } @@ -16,6 +18,7 @@ export const KanbanColumn = ({ icon, headerBg, bodyBg, + onAddTask, children, }: KanbanColumnProps): React.JSX.Element => { return ( @@ -34,9 +37,21 @@ export const KanbanColumn = ({ {icon} {title} - - {count} - +
+ {onAddTask ? ( + + ) : null} + + {count} + +
{children}
diff --git a/src/renderer/hooks/useTeamMessagesRead.ts b/src/renderer/hooks/useTeamMessagesRead.ts index d9145514..4a1d24a4 100644 --- a/src/renderer/hooks/useTeamMessagesRead.ts +++ b/src/renderer/hooks/useTeamMessagesRead.ts @@ -2,12 +2,14 @@ import { useCallback, useMemo, useState } from 'react'; import { getReadSet as getReadSetStorage, + markBulkRead as markBulkReadStorage, markRead as markReadStorage, } from '@renderer/utils/teamMessageReadStorage'; export function useTeamMessagesRead(teamName: string): { readSet: Set; markRead: (messageKey: string) => void; + markAllRead: (messageKeys: string[]) => void; } { const [version, setVersion] = useState(0); const readSet = useMemo(() => { @@ -27,6 +29,24 @@ export function useTeamMessagesRead(teamName: string): { [teamName] ); + const markAllRead = useCallback( + (messageKeys: string[]) => { + if (!teamName || messageKeys.length === 0) return; + const existing = getReadSetStorage(teamName); + let changed = false; + for (const key of messageKeys) { + if (!existing.has(key)) { + existing.add(key); + changed = true; + } + } + if (!changed) return; + markBulkReadStorage(teamName, existing); + setVersion((v) => v + 1); + }, + [teamName] + ); + const effectiveReadSet = !teamName ? new Set() : readSet; - return { readSet: effectiveReadSet, markRead }; + return { readSet: effectiveReadSet, markRead, markAllRead }; } diff --git a/src/renderer/utils/teamMessageReadStorage.ts b/src/renderer/utils/teamMessageReadStorage.ts index 54772bda..8db768f0 100644 --- a/src/renderer/utils/teamMessageReadStorage.ts +++ b/src/renderer/utils/teamMessageReadStorage.ts @@ -36,3 +36,14 @@ export function markRead(teamName: string, messageKey: string, fullSet?: Set): void { + try { + localStorage.setItem(storageKey(teamName), JSON.stringify([...fullSet])); + } catch { + // quota or disabled + } +}