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