chore(dev): sync pending branch updates

This commit is contained in:
777genius 2026-04-18 14:12:37 +03:00
parent 5a7d5ea310
commit 8d5ee7d5ab
5 changed files with 2932 additions and 32 deletions

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ import {
getNonEmptyTaskCategories, getNonEmptyTaskCategories,
groupTasksByDate, groupTasksByDate,
groupTasksByProject, groupTasksByProject,
NO_PROJECT_KEY,
sortTasksByFreshness, sortTasksByFreshness,
} from '@renderer/utils/taskGrouping'; } from '@renderer/utils/taskGrouping';
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
@ -430,8 +431,18 @@ export const GlobalTaskList = ({
? categories.length > 0 ? categories.length > 0
: projectGroups.some((g) => g.tasks.length > 0)); : projectGroups.some((g) => g.tasks.length > 0));
const noProjectGroupColor = useMemo(
() => ({
border: 'var(--color-border)',
glow: 'transparent',
icon: 'var(--color-text-muted)',
text: 'var(--color-text-secondary)',
}),
[]
);
return ( return (
<div className="flex size-full min-w-0 flex-col"> <div className="flex size-full min-w-0 flex-col overflow-x-hidden">
{!hideHeader && ( {!hideHeader && (
<div <div
className="flex shrink-0 items-center gap-2 border-b px-3 py-1.5" className="flex shrink-0 items-center gap-2 border-b px-3 py-1.5"
@ -597,7 +608,7 @@ export const GlobalTaskList = ({
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto overflow-x-hidden">
{globalTasksLoading && !globalTasksInitialized && ( {globalTasksLoading && !globalTasksInitialized && (
<div className="space-y-2 p-3"> <div className="space-y-2 p-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
@ -644,7 +655,10 @@ export const GlobalTaskList = ({
projectGroups.map((group) => { projectGroups.map((group) => {
if (group.tasks.length === 0) return null; if (group.tasks.length === 0) return null;
const isGroupCollapsed = projectCollapsed.isCollapsed(group.projectKey); const isGroupCollapsed = projectCollapsed.isCollapsed(group.projectKey);
const groupColor = projectColor(group.projectLabel); const isNoProjectGroup = group.projectKey === NO_PROJECT_KEY;
const groupColor = isNoProjectGroup
? noProjectGroupColor
: projectColor(group.projectLabel);
const visibleCount = getProjectGroupVisibleCount( const visibleCount = getProjectGroupVisibleCount(
projectVisibleCountByKey[group.projectKey], projectVisibleCountByKey[group.projectKey],
group.tasks.length group.tasks.length
@ -661,7 +675,9 @@ export const GlobalTaskList = ({
className="hover:bg-surface-raised/40 sticky top-0 z-10 flex w-full cursor-pointer items-center gap-1.5 p-2 transition-colors" className="hover:bg-surface-raised/40 sticky top-0 z-10 flex w-full cursor-pointer items-center gap-1.5 p-2 transition-colors"
style={{ style={{
backgroundColor: 'var(--color-surface-sidebar)', backgroundColor: 'var(--color-surface-sidebar)',
backgroundImage: `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`, backgroundImage: isNoProjectGroup
? undefined
: `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`,
boxShadow: `inset 2px 0 0 ${groupColor.border}, inset 0 -1px 0 var(--color-border)`, boxShadow: `inset 2px 0 0 ${groupColor.border}, inset 0 -1px 0 var(--color-border)`,
}} }}
> >
@ -676,7 +692,7 @@ export const GlobalTaskList = ({
aria-hidden="true" aria-hidden="true"
/> />
<span <span
className="truncate text-[13px] font-bold leading-none" className="truncate text-[11px] font-bold leading-none"
style={{ color: groupColor.icon }} style={{ color: groupColor.icon }}
> >
{group.projectLabel} {group.projectLabel}

View file

@ -51,7 +51,8 @@ export const MemberMessagesTab = ({
const [pagedMessages, setPagedMessages] = useState<InboxMessage[]>([]); const [pagedMessages, setPagedMessages] = useState<InboxMessage[]>([]);
const [nextCursor, setNextCursor] = useState<string | null>(null); const [nextCursor, setNextCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const [loading, setLoading] = useState(false); const [initialPageLoading, setInitialPageLoading] = useState(false);
const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
const [activityFilter, setActivityFilter] = useState<MemberActivityFilter>(initialFilter); const [activityFilter, setActivityFilter] = useState<MemberActivityFilter>(initialFilter);
const [expandedItem, setExpandedItem] = useState<TimelineItem | null>(null); const [expandedItem, setExpandedItem] = useState<TimelineItem | null>(null);
const { readSet } = useTeamMessagesRead(teamName); const { readSet } = useTeamMessagesRead(teamName);
@ -74,7 +75,7 @@ export const MemberMessagesTab = ({
setPagedMessages([]); setPagedMessages([]);
setNextCursor(null); setNextCursor(null);
setHasMore(false); setHasMore(false);
setLoading(true); setInitialPageLoading(true);
void (async () => { void (async () => {
try { try {
@ -95,7 +96,9 @@ export const MemberMessagesTab = ({
setHasMore(false); setHasMore(false);
} }
} finally { } finally {
if (!cancelled) setLoading(false); if (!cancelled) {
setInitialPageLoading(false);
}
} }
})(); })();
@ -105,8 +108,8 @@ export const MemberMessagesTab = ({
}, [teamName, memberName]); }, [teamName, memberName]);
const loadOlderMessages = useCallback(async () => { const loadOlderMessages = useCallback(async () => {
if (!nextCursor || loading) return; if (!nextCursor || loadingOlderMessages) return;
setLoading(true); setLoadingOlderMessages(true);
try { try {
const page = await api.teams.getMessagesPage(teamName, { const page = await api.teams.getMessagesPage(teamName, {
beforeTimestamp: nextCursor, beforeTimestamp: nextCursor,
@ -121,9 +124,9 @@ export const MemberMessagesTab = ({
} catch { } catch {
// best-effort // best-effort
} finally { } finally {
setLoading(false); setLoadingOlderMessages(false);
} }
}, [teamName, memberName, nextCursor, loading]); }, [loadingOlderMessages, memberName, nextCursor, teamName]);
const effectiveMessages = useMemo( const effectiveMessages = useMemo(
() => mergeTeamMessages(messages, pagedMessages), () => mergeTeamMessages(messages, pagedMessages),
@ -198,7 +201,7 @@ export const MemberMessagesTab = ({
[onTaskClick, taskMap, tasks] [onTaskClick, taskMap, tasks]
); );
const emptyStateText = loading const emptyStateText = initialPageLoading
? 'Loading activity...' ? 'Loading activity...'
: activityFilter === 'comments' : activityFilter === 'comments'
? 'No comments for this member' ? 'No comments for this member'
@ -289,10 +292,11 @@ export const MemberMessagesTab = ({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-xs" className="text-xs"
disabled={loading} aria-busy={loadingOlderMessages}
disabled={loadingOlderMessages}
onClick={() => void loadOlderMessages()} onClick={() => void loadOlderMessages()}
> >
{loading ? 'Loading...' : 'Load older messages'} Load older messages
</Button> </Button>
</div> </div>
)} )}

View file

@ -152,13 +152,12 @@ export const MessagesPanel = memo(function MessagesPanel({
const [fetchedMessages, setFetchedMessages] = useState<InboxMessage[]>([]); const [fetchedMessages, setFetchedMessages] = useState<InboxMessage[]>([]);
const [nextCursor, setNextCursor] = useState<string | null>(null); const [nextCursor, setNextCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const [messagesLoading, setMessagesLoading] = useState(false); const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
const fetchIdRef = useRef(0); const fetchIdRef = useRef(0);
// Initial fetch on mount or team change // Initial fetch on mount or team change
useEffect(() => { useEffect(() => {
const id = ++fetchIdRef.current; const id = ++fetchIdRef.current;
setMessagesLoading(true);
void (async () => { void (async () => {
try { try {
const page = await api.teams.getMessagesPage(teamName, { limit: PAGE_SIZE }); const page = await api.teams.getMessagesPage(teamName, { limit: PAGE_SIZE });
@ -171,8 +170,6 @@ export const MessagesPanel = memo(function MessagesPanel({
if (fetchIdRef.current === id && messages.length > 0) { if (fetchIdRef.current === id && messages.length > 0) {
setFetchedMessages(messages); setFetchedMessages(messages);
} }
} finally {
if (fetchIdRef.current === id) setMessagesLoading(false);
} }
})(); })();
}, [teamName]); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally only on teamName change }, [teamName]); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally only on teamName change
@ -193,8 +190,8 @@ export const MessagesPanel = memo(function MessagesPanel({
}, [teamName, isTeamAlive, leadActivity]); }, [teamName, isTeamAlive, leadActivity]);
const loadOlderMessages = useCallback(async () => { const loadOlderMessages = useCallback(async () => {
if (!nextCursor || messagesLoading) return; if (!nextCursor || loadingOlderMessages) return;
setMessagesLoading(true); setLoadingOlderMessages(true);
try { try {
const page = await api.teams.getMessagesPage(teamName, { const page = await api.teams.getMessagesPage(teamName, {
beforeTimestamp: nextCursor, beforeTimestamp: nextCursor,
@ -206,9 +203,9 @@ export const MessagesPanel = memo(function MessagesPanel({
} catch { } catch {
// best-effort // best-effort
} finally { } finally {
setMessagesLoading(false); setLoadingOlderMessages(false);
} }
}, [teamName, nextCursor, messagesLoading]); }, [loadingOlderMessages, nextCursor, teamName]);
// Use fetched messages, fall back to prop messages during initial load // Use fetched messages, fall back to prop messages during initial load
const effectiveMessages = useMemo(() => { const effectiveMessages = useMemo(() => {
@ -307,7 +304,7 @@ export const MessagesPanel = memo(function MessagesPanel({
for (const [element, setHeight] of observedEntries) { for (const [element, setHeight] of observedEntries) {
if (!element) continue; if (!element) continue;
const updateHeight = () => { const updateHeight = (): void => {
const nextHeight = Math.ceil(element.getBoundingClientRect().height); const nextHeight = Math.ceil(element.getBoundingClientRect().height);
if (nextHeight > 0) { if (nextHeight > 0) {
setHeight(nextHeight); setHeight(nextHeight);
@ -684,10 +681,11 @@ export const MessagesPanel = memo(function MessagesPanel({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-xs text-text-muted" className="text-xs text-text-muted"
disabled={messagesLoading} aria-busy={loadingOlderMessages}
disabled={loadingOlderMessages}
onClick={() => void loadOlderMessages()} onClick={() => void loadOlderMessages()}
> >
{messagesLoading ? 'Loading...' : 'Load older messages'} Load older messages
</Button> </Button>
</div> </div>
)} )}
@ -869,10 +867,11 @@ export const MessagesPanel = memo(function MessagesPanel({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-xs text-text-muted" className="text-xs text-text-muted"
disabled={messagesLoading} aria-busy={loadingOlderMessages}
disabled={loadingOlderMessages}
onClick={() => void loadOlderMessages()} onClick={() => void loadOlderMessages()}
> >
{messagesLoading ? 'Loading...' : 'Load older messages'} Load older messages
</Button> </Button>
</div> </div>
)} )}
@ -1155,10 +1154,11 @@ export const MessagesPanel = memo(function MessagesPanel({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-xs text-text-muted" className="text-xs text-text-muted"
disabled={messagesLoading} aria-busy={loadingOlderMessages}
disabled={loadingOlderMessages}
onClick={() => void loadOlderMessages()} onClick={() => void loadOlderMessages()}
> >
{messagesLoading ? 'Loading...' : 'Load older messages'} Load older messages
</Button> </Button>
</div> </div>
)} )}

View file

@ -91,8 +91,8 @@ export function getNonEmptyTaskCategories(groups: DateGroupedTasks): DateCategor
return DATE_CATEGORY_ORDER.filter((cat) => groups[cat].length > 0); return DATE_CATEGORY_ORDER.filter((cat) => groups[cat].length > 0);
} }
const NO_PROJECT_KEY = '__no_project__'; export const NO_PROJECT_KEY = '__no_project__';
const NO_PROJECT_LABEL = 'Without project'; export const NO_PROJECT_LABEL = 'No project';
function trimTrailingPathSep(p: string): string { function trimTrailingPathSep(p: string): string {
let s = p; let s = p;