Merge branch 'fix/lint-errors' into dev — resolve all eslint errors
Resolve conflicts in ActivityTimeline and TaskCommentsSection by keeping the newer useNewItemKeys hook approach from dev over the reducer/useState approach from the lint-fix branch.
This commit is contained in:
commit
00ca6698fa
24 changed files with 134 additions and 107 deletions
|
|
@ -68,6 +68,8 @@ import * as path from 'path';
|
|||
import { ConfigManager } from '../services/infrastructure/ConfigManager';
|
||||
import { NotificationManager } from '../services/infrastructure/NotificationManager';
|
||||
import { gitIdentityResolver } from '../services/parsing/GitIdentityResolver';
|
||||
import { TeamAttachmentStore } from '../services/team/TeamAttachmentStore';
|
||||
import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore';
|
||||
|
||||
import {
|
||||
validateFromField,
|
||||
|
|
@ -77,9 +79,6 @@ import {
|
|||
validateTeamName,
|
||||
} from './guards';
|
||||
|
||||
import { TeamAttachmentStore } from '../services/team/TeamAttachmentStore';
|
||||
import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore';
|
||||
|
||||
import type {
|
||||
MemberStatsComputer,
|
||||
TeamDataService,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
buildDetectedErrorFromTeam,
|
||||
type TeamNotificationPayload,
|
||||
} from '@main/utils/teamNotificationBuilder';
|
||||
|
||||
import { projectPathResolver } from '../discovery/ProjectPathResolver';
|
||||
import { gitIdentityResolver } from '../parsing/GitIdentityResolver';
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ import { ConfigManager } from './ConfigManager';
|
|||
// Re-export DetectedError for backward compatibility
|
||||
export type { DetectedError };
|
||||
// Re-export team notification types for callers
|
||||
export type { TeamNotificationPayload, TeamEventType } from '@main/utils/teamNotificationBuilder';
|
||||
export type { TeamEventType, TeamNotificationPayload } from '@main/utils/teamNotificationBuilder';
|
||||
|
||||
/**
|
||||
* Stored notification with read status.
|
||||
|
|
|
|||
|
|
@ -3355,7 +3355,7 @@ export class TeamProvisioningService {
|
|||
currentMembers = config.members
|
||||
.filter((m) => m?.agentType !== 'team-lead' && m?.name)
|
||||
.map((m) => ({
|
||||
name: m.name!,
|
||||
name: m.name,
|
||||
role: m.role ?? undefined,
|
||||
}));
|
||||
} else {
|
||||
|
|
@ -3403,7 +3403,7 @@ export class TeamProvisioningService {
|
|||
}
|
||||
if (run.leadActivityState !== 'idle') {
|
||||
logger.info(
|
||||
`[${run.teamName}] post-compact reminder aborted — lead activity changed to ${run.leadActivityState}`
|
||||
`[${run.teamName}] post-compact reminder aborted — lead activity changed to ${run.leadActivityState as string}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import type { TriggerColor } from '@shared/constants/triggerColors';
|
||||
|
||||
import type { DetectedError } from '../services/error/ErrorMessageBuilder';
|
||||
import type { TriggerColor } from '@shared/constants/triggerColors';
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
|
|
|
|||
|
|
@ -254,8 +254,9 @@ export class HttpAPIClient implements ElectronAPI {
|
|||
const params = new URLSearchParams();
|
||||
if (options?.bypassCache) params.set('bypassCache', 'true');
|
||||
const qs = params.toString();
|
||||
const suffix = qs ? `?${qs}` : '';
|
||||
return this.get<SessionDetail | null>(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}${qs ? `?${qs}` : ''}`
|
||||
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}${suffix}`
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -278,8 +279,9 @@ export class HttpAPIClient implements ElectronAPI {
|
|||
const params = new URLSearchParams();
|
||||
if (options?.bypassCache) params.set('bypassCache', 'true');
|
||||
const qs = params.toString();
|
||||
const suffix = qs ? `?${qs}` : '';
|
||||
return this.get<SubagentDetail | null>(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}/subagents/${encodeURIComponent(subagentId)}${qs ? `?${qs}` : ''}`
|
||||
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}/subagents/${encodeURIComponent(subagentId)}${suffix}`
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
|||
|
||||
import { PROSE_BODY } from '@renderer/constants/cssVariables';
|
||||
|
||||
import { highlightSearchInChildren, type SearchContext } from './searchHighlightUtils';
|
||||
import { FileLink, isRelativeUrl } from './viewers/FileLink';
|
||||
import { highlightSearchInChildren, type SearchContext } from './searchHighlightUtils';
|
||||
|
||||
import type { Components } from 'react-markdown';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import React from 'react';
|
|||
|
||||
import { PROSE_LINK } from '@renderer/constants/cssVariables';
|
||||
import { useStore } from '@renderer/store';
|
||||
import type { AppState } from '@renderer/store/types';
|
||||
import { Check, FileCode } from 'lucide-react';
|
||||
|
||||
import type { AppState } from '@renderer/store/types';
|
||||
|
||||
// =============================================================================
|
||||
// Exported utilities
|
||||
// =============================================================================
|
||||
|
|
@ -25,7 +26,7 @@ export function parsePathWithLine(href: string): { filePath: string; line: numbe
|
|||
} catch {
|
||||
decoded = href;
|
||||
}
|
||||
const match = decoded.match(/^(.+?):(\d+)$/);
|
||||
const match = /^(.+?):(\d+)$/.exec(decoded);
|
||||
if (match) return { filePath: match[1], line: parseInt(match[2], 10) };
|
||||
return { filePath: decoded, line: null };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { DisplayItemList } from '@renderer/components/chat/DisplayItemList';
|
||||
import { highlightQueryInText } from '@renderer/components/chat/searchHighlightUtils';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { parseStreamJsonToGroups, groupBySubagent } from '@renderer/utils/streamJsonParser';
|
||||
import { groupBySubagent, parseStreamJsonToGroups } from '@renderer/utils/streamJsonParser';
|
||||
import { Bot, ChevronRight } from 'lucide-react';
|
||||
|
||||
import type {
|
||||
StreamJsonGroup,
|
||||
StreamJsonEntry,
|
||||
SubagentSection,
|
||||
} from '@renderer/utils/streamJsonParser';
|
||||
import type { StreamJsonGroup, SubagentSection } from '@renderer/utils/streamJsonParser';
|
||||
|
||||
type CliLogsOrder = 'oldest-first' | 'newest-first';
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui
|
|||
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
|
||||
import { useBranchSync } from '@renderer/hooks/useBranchSync';
|
||||
import { useTabUI } from '@renderer/hooks/useTabUI';
|
||||
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
||||
import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded';
|
||||
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { useStore } from '@renderer/store';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { ToolApprovalRequest } from '@shared/types';
|
|||
// Tool icon mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getToolIcon(toolName: string): React.ReactNode {
|
||||
function getToolIcon(toolName: string): React.JSX.Element {
|
||||
const cls = 'size-4 shrink-0';
|
||||
switch (toolName) {
|
||||
case 'Bash':
|
||||
|
|
@ -60,9 +60,11 @@ function useElapsed(receivedAt: string): number {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setElapsed(Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000)));
|
||||
const computeElapsed = (): number =>
|
||||
Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000));
|
||||
queueMicrotask(() => setElapsed(computeElapsed()));
|
||||
const id = setInterval(() => {
|
||||
setElapsed(Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000)));
|
||||
setElapsed(computeElapsed());
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [receivedAt]);
|
||||
|
|
@ -78,6 +80,7 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
const pendingApprovals = useStore((s) => s.pendingApprovals);
|
||||
const respondToToolApproval = useStore((s) => s.respondToToolApproval);
|
||||
const teams = useStore((s) => s.teams);
|
||||
const { isLight } = useTheme();
|
||||
|
||||
const current: ToolApprovalRequest | undefined = pendingApprovals[0];
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -96,8 +99,8 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
[current, disabled, respondToToolApproval]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRespond(true);
|
||||
|
|
@ -105,21 +108,20 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
e.preventDefault();
|
||||
handleRespond(false);
|
||||
}
|
||||
},
|
||||
[handleRespond]
|
||||
);
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleRespond]);
|
||||
|
||||
if (!current) return null;
|
||||
|
||||
const teamSummary = teams.find((t) => t.teamName === current.teamName);
|
||||
const teamColor = teamSummary?.color ? getTeamColorSet(teamSummary.color) : null;
|
||||
const { isLight } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="fixed bottom-4 left-1/2 z-[55] w-full max-w-[480px] -translate-x-1/2 rounded-lg border shadow-xl outline-none duration-200 animate-in fade-in slide-in-from-bottom-4"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-surface-overlay)',
|
||||
|
|
@ -183,10 +185,11 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
className="rounded-md px-3.5 py-1.5 text-xs font-medium text-white transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: 'rgb(5, 150, 105)' }}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disabled) e.currentTarget.style.backgroundColor = 'rgb(16, 185, 129)';
|
||||
if (!disabled)
|
||||
Object.assign(e.currentTarget.style, { backgroundColor: 'rgb(16, 185, 129)' });
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'rgb(5, 150, 105)';
|
||||
Object.assign(e.currentTarget.style, { backgroundColor: 'rgb(5, 150, 105)' });
|
||||
}}
|
||||
>
|
||||
Allow
|
||||
|
|
@ -201,10 +204,13 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
color: 'rgb(248, 113, 113)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disabled) e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
|
||||
if (!disabled)
|
||||
Object.assign(e.currentTarget.style, {
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
});
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
Object.assign(e.currentTarget.style, { backgroundColor: 'transparent' });
|
||||
}}
|
||||
>
|
||||
Deny
|
||||
|
|
@ -224,9 +230,9 @@ export const ToolApprovalSheet: React.FC = () => {
|
|||
// Elapsed display sub-component (uses hook)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ElapsedDisplay({ receivedAt }: { receivedAt: string }): React.JSX.Element {
|
||||
const ElapsedDisplay = ({ receivedAt }: { receivedAt: string }): React.JSX.Element => {
|
||||
const elapsed = useElapsed(receivedAt);
|
||||
return (
|
||||
<span className="text-[11px] tabular-nums text-[var(--color-text-muted)]">{elapsed}s</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
||||
|
||||
import { AnimatedHeightReveal } from './AnimatedHeightReveal';
|
||||
import { ActivityItem, isNoiseMessage } from './ActivityItem';
|
||||
import { AnimatedHeightReveal } from './AnimatedHeightReveal';
|
||||
import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState';
|
||||
import { groupTimelineItems, isLeadThought, LeadThoughtsGroupRow } from './LeadThoughtsGroup';
|
||||
import { useNewItemKeys } from './useNewItemKeys';
|
||||
|
||||
import type { TimelineItem } from './LeadThoughtsGroup';
|
||||
import type { ActivityCollapseState } from './collapseState';
|
||||
import type { TimelineItem } from './LeadThoughtsGroup';
|
||||
import type { InboxMessage, ResolvedTeamMember } from '@shared/types';
|
||||
|
||||
interface ActivityTimelineProps {
|
||||
|
|
@ -253,15 +253,6 @@ export const ActivityTimeline = ({
|
|||
setVisibleCount(Infinity);
|
||||
};
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
|
||||
<p>No messages</p>
|
||||
<p className="mt-1 text-[11px]">Send a message to a member to see activity.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getItemSessionId = (item: TimelineItem): string | undefined =>
|
||||
item.type === 'lead-thoughts'
|
||||
? item.group.thoughts[0].leadSessionId
|
||||
|
|
@ -296,6 +287,15 @@ export const ActivityTimeline = ({
|
|||
[allCollapsed, newestMessageIndex, pinnedThoughtGroup, expandOverrides, onToggleExpandOverride]
|
||||
);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
|
||||
<p>No messages</p>
|
||||
<p className="mt-1 text-[11px]">Send a message to a member to see activity.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* Pinned (newest) thought group — always at top */}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void {
|
|||
ref(value);
|
||||
return;
|
||||
}
|
||||
(ref as MutableRefObject<T | null>).current = value;
|
||||
const mutableRef = ref as MutableRefObject<T | null>;
|
||||
mutableRef.current = value;
|
||||
}
|
||||
|
||||
export const AnimatedHeightReveal = ({
|
||||
|
|
@ -28,11 +29,15 @@ export const AnimatedHeightReveal = ({
|
|||
containerRef,
|
||||
children,
|
||||
}: AnimatedHeightRevealProps): JSX.Element => {
|
||||
const shouldAnimateOnMountRef = useRef(Boolean(animate));
|
||||
const [shouldAnimateOnMount] = useState(() => Boolean(animate));
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const prefersReducedMotionRef = useRef(false);
|
||||
const [isExpanded, setIsExpanded] = useState(() => !shouldAnimateOnMountRef.current);
|
||||
const [prefersReducedMotion] = useState(
|
||||
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
const [isExpanded, setIsExpanded] = useState(
|
||||
() => !animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
|
||||
const setWrapperRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
|
|
@ -50,9 +55,7 @@ export const AnimatedHeightReveal = ({
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
prefersReducedMotionRef.current = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (!shouldAnimateOnMountRef.current || prefersReducedMotionRef.current) {
|
||||
setIsExpanded(true);
|
||||
if (!shouldAnimateOnMount || prefersReducedMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +69,7 @@ export const AnimatedHeightReveal = ({
|
|||
return () => {
|
||||
clearPendingAnimation();
|
||||
};
|
||||
}, [clearPendingAnimation]);
|
||||
}, [clearPendingAnimation, shouldAnimateOnMount, prefersReducedMotion]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
|
@ -75,8 +78,7 @@ export const AnimatedHeightReveal = ({
|
|||
[clearPendingAnimation]
|
||||
);
|
||||
|
||||
const shouldTransition =
|
||||
shouldAnimateOnMountRef.current && !prefersReducedMotionRef.current && isExpanded;
|
||||
const shouldTransition = shouldAnimateOnMount && !prefersReducedMotion && isExpanded;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Reply } from 'lucide-react';
|
||||
|
||||
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
import {
|
||||
|
|
@ -17,13 +15,14 @@ import { getTeamColorSet } from '@renderer/constants/teamColors';
|
|||
import { useStore } from '@renderer/store';
|
||||
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
|
||||
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Reply } from 'lucide-react';
|
||||
|
||||
import { linkifyMentionsInMarkdown, linkifyTaskIdsInMarkdown } from './ActivityItem';
|
||||
import {
|
||||
AnimatedHeightReveal,
|
||||
ENTRY_REVEAL_ANIMATION_MS,
|
||||
ENTRY_REVEAL_EASING,
|
||||
} from './AnimatedHeightReveal';
|
||||
import { linkifyMentionsInMarkdown, linkifyTaskIdsInMarkdown } from './ActivityItem';
|
||||
import { isManagedCollapseState } from './collapseState';
|
||||
|
||||
import type { ActivityCollapseState } from './collapseState';
|
||||
|
|
@ -247,6 +246,21 @@ const LeadThoughtItem = ({
|
|||
const content = contentRef.current;
|
||||
if (!wrapper || !content) return;
|
||||
|
||||
const applyTransition = (targetHeight: number): void => {
|
||||
wrapper.style.transition = [
|
||||
`height ${THOUGHT_HEIGHT_ANIMATION_MS}ms ${ENTRY_REVEAL_EASING}`,
|
||||
`opacity ${THOUGHT_HEIGHT_ANIMATION_MS}ms ease`,
|
||||
].join(', ');
|
||||
wrapper.style.height = `${Math.max(targetHeight, 0)}px`;
|
||||
wrapper.style.opacity = '1';
|
||||
};
|
||||
|
||||
const scheduleTransition = (targetHeight: number): void => {
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
applyTransition(targetHeight);
|
||||
});
|
||||
};
|
||||
|
||||
const animateHeight = (
|
||||
targetHeight: number,
|
||||
startHeight: number,
|
||||
|
|
@ -258,17 +272,12 @@ const LeadThoughtItem = ({
|
|||
wrapper.style.height = `${Math.max(startHeight, 0)}px`;
|
||||
wrapper.style.opacity = `${startOpacity}`;
|
||||
wrapper.style.willChange = 'height, opacity';
|
||||
void wrapper.offsetHeight;
|
||||
// Force layout reflow so the browser registers the starting values
|
||||
const _reflow = wrapper.offsetHeight;
|
||||
if (_reflow < -1) return; // unreachable — prevents unused-variable lint
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
wrapper.style.transition = [
|
||||
`height ${THOUGHT_HEIGHT_ANIMATION_MS}ms ${ENTRY_REVEAL_EASING}`,
|
||||
`opacity ${THOUGHT_HEIGHT_ANIMATION_MS}ms ease`,
|
||||
].join(', ');
|
||||
wrapper.style.height = `${Math.max(targetHeight, 0)}px`;
|
||||
wrapper.style.opacity = '1';
|
||||
});
|
||||
scheduleTransition(targetHeight);
|
||||
});
|
||||
|
||||
cleanupTimerRef.current = window.setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -49,10 +49,12 @@ export const AttachmentPreviewList = ({
|
|||
|
||||
if (newIds.size === 0) return;
|
||||
|
||||
setEnteringIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const id of newIds) next.add(id);
|
||||
return next;
|
||||
queueMicrotask(() => {
|
||||
setEnteringIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const id of newIds) next.add(id);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
// Clear entering state after animation completes
|
||||
|
|
@ -71,9 +73,11 @@ export const AttachmentPreviewList = ({
|
|||
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
const exitTimers = exitTimersRef.current;
|
||||
const enterTimers = enterTimersRef.current;
|
||||
return () => {
|
||||
for (const t of exitTimersRef.current.values()) window.clearTimeout(t);
|
||||
for (const t of enterTimersRef.current.values()) window.clearTimeout(t);
|
||||
for (const t of exitTimers.values()) window.clearTimeout(t);
|
||||
for (const t of enterTimers.values()) window.clearTimeout(t);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,8 @@ import { buildReplyBlock } from '@renderer/utils/agentMessageFormatting';
|
|||
import { removeChipTokenFromText } from '@renderer/utils/chipUtils';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { AlertCircle, ImagePlus, Send, X } from 'lucide-react';
|
||||
|
||||
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
||||
import { AlertCircle, ImagePlus, Send, X } from 'lucide-react';
|
||||
|
||||
import { MemberBadge } from '../MemberBadge';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ import { useStore } from '@renderer/store';
|
|||
import { buildReplyBlock } from '@renderer/utils/agentMessageFormatting';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { ImagePlus, Mic, Send, Trash2, X } from 'lucide-react';
|
||||
|
||||
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
||||
import { ImagePlus, Mic, Send, Trash2, X } from 'lucide-react';
|
||||
|
||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||
import type { CommentAttachmentPayload, ResolvedTeamMember } from '@shared/types';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||
import { AnimatedHeightReveal } from '@renderer/components/team/activity/AnimatedHeightReveal';
|
||||
import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock';
|
||||
import { useNewItemKeys } from '@renderer/components/team/activity/useNewItemKeys';
|
||||
|
|
@ -17,8 +17,8 @@ import { buildReplyBlock, parseMessageReply } from '@renderer/utils/agentMessage
|
|||
import { isImageMimeType } from '@renderer/utils/attachmentUtils';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { CheckCircle2, Eye, File, Loader2, MessageSquare, Reply, Send, X } from 'lucide-react';
|
||||
|
||||
|
|
@ -92,13 +92,17 @@ export const TaskCommentsSection = ({
|
|||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS);
|
||||
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
|
||||
|
||||
// Reset local UI state when team/task changes.
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change
|
||||
// Reset local UI state when team/task changes using the
|
||||
// "adjust state during render" pattern (no effect needed).
|
||||
// See: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
const resetKey = `${teamName}:${taskId}`;
|
||||
const [prevResetKey, setPrevResetKey] = useState(resetKey);
|
||||
if (resetKey !== prevResetKey) {
|
||||
setPrevResetKey(resetKey);
|
||||
setVisibleCount(INITIAL_VISIBLE_COMMENTS);
|
||||
setReplyTo(null);
|
||||
setPreviewImageUrl(null);
|
||||
}, [teamName, taskId]);
|
||||
}
|
||||
|
||||
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
|
||||
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { CollapsibleTeamSection } from '@renderer/components/team/CollapsibleTeamSection';
|
||||
import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
|
||||
import { CollapsibleTeamSection } from '@renderer/components/team/CollapsibleTeamSection';
|
||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import { MemberLogsTab } from '@renderer/components/team/members/MemberLogsTab';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
|
||||
import { MemberCard } from './MemberCard';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
export type SubagentPreviewMessageKind =
|
||||
| 'output'
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ import { useStore } from '@renderer/store';
|
|||
import { serializeChipsWithText } from '@renderer/types/inlineChip';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { AlertCircle, Check, ChevronDown, ImagePlus, Mic, Search, Send } from 'lucide-react';
|
||||
|
||||
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
||||
import { AlertCircle, Check, ChevronDown, ImagePlus, Mic, Search, Send } from 'lucide-react';
|
||||
|
||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||
import type { AttachmentPayload, LeadContextUsage, ResolvedTeamMember } from '@shared/types';
|
||||
|
|
@ -112,7 +111,7 @@ export const MessageComposer = ({
|
|||
const lead = members.find((m) => m.role === 'lead' || m.name === 'team-lead');
|
||||
const next = lead?.name ?? members[0]?.name ?? '';
|
||||
if (next && next !== recipient) {
|
||||
setRecipient(next);
|
||||
queueMicrotask(() => setRecipient(next));
|
||||
}
|
||||
}, [members, recipient]);
|
||||
|
||||
|
|
@ -189,15 +188,16 @@ export const MessageComposer = ({
|
|||
[handleSend]
|
||||
);
|
||||
|
||||
const { addFiles: draftAddFiles } = draft;
|
||||
const handleFileInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target;
|
||||
if (input.files?.length) {
|
||||
void draft.addFiles(input.files);
|
||||
void draftAddFiles(input.files);
|
||||
}
|
||||
input.value = '';
|
||||
},
|
||||
[draft.addFiles]
|
||||
[draftAddFiles]
|
||||
);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
|
|
@ -219,20 +219,22 @@ export const MessageComposer = ({
|
|||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const { handleDrop: draftHandleDrop } = draft;
|
||||
const handleDropWrapper = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
dragCounterRef.current = 0;
|
||||
setIsDragOver(false);
|
||||
if (canAttach) draft.handleDrop(e);
|
||||
if (canAttach) draftHandleDrop(e);
|
||||
},
|
||||
[canAttach, draft.handleDrop]
|
||||
[canAttach, draftHandleDrop]
|
||||
);
|
||||
|
||||
const { handlePaste: draftHandlePaste } = draft;
|
||||
const handlePasteWrapper = useCallback(
|
||||
(e: React.ClipboardEvent) => {
|
||||
if (canAttach) draft.handlePaste(e);
|
||||
if (canAttach) draftHandlePaste(e);
|
||||
},
|
||||
[canAttach, draft.handlePaste]
|
||||
[canAttach, draftHandlePaste]
|
||||
);
|
||||
|
||||
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
composerDraftStorage,
|
||||
type ComposerDraftSnapshot,
|
||||
composerDraftStorage,
|
||||
} from '@renderer/services/composerDraftStorage';
|
||||
import {
|
||||
fileToAttachmentPayload,
|
||||
|
|
@ -190,12 +190,16 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult {
|
|||
flushPending();
|
||||
userTouchedRef.current = false;
|
||||
|
||||
// Reset to empty immediately for the new teamName
|
||||
// Reset to empty for the new teamName.
|
||||
// Wrapped in queueMicrotask to avoid synchronous setState inside effect body.
|
||||
const empty = composerDraftStorage.emptySnapshot(teamName);
|
||||
applySnapshot(empty);
|
||||
setIsSaved(false);
|
||||
setIsLoaded(false);
|
||||
setAttachmentError(null);
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return;
|
||||
applySnapshot(empty);
|
||||
setIsSaved(false);
|
||||
setIsLoaded(false);
|
||||
setAttachmentError(null);
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
// Try loading unified snapshot first
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import type {
|
|||
CliInstallerProgress,
|
||||
LeadContextUsage,
|
||||
TeamChangeEvent,
|
||||
ToolApprovalDismiss,
|
||||
ToolApprovalEvent,
|
||||
ToolApprovalRequest,
|
||||
UpdaterStatus,
|
||||
|
|
@ -452,7 +451,7 @@ export function initializeNotificationListeners(): () => void {
|
|||
const cleanup = api.teams.onToolApprovalEvent((_event: unknown, data: unknown) => {
|
||||
const event = data as ToolApprovalEvent;
|
||||
if ('dismissed' in event && event.dismissed) {
|
||||
const dismiss = event as ToolApprovalDismiss;
|
||||
const dismiss = event;
|
||||
useStore.setState((s) => ({
|
||||
pendingApprovals: s.pendingApprovals.filter(
|
||||
(a) => !(a.teamName === dismiss.teamName && a.runId === dismiss.runId)
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ export function groupBySubagent(groups: StreamJsonGroup[]): StreamJsonEntry[] {
|
|||
agentDescMap.set(group.agentId, pendingDescriptions.shift() ?? 'Subagent');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- optional chain narrows to `never` in loop body
|
||||
if (currentRun && currentRun.agentId === group.agentId) {
|
||||
currentRun.groups.push(group);
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue