fix(perf): resolve all PR #93 review blockers

- Fix react/display-name in DisplayItemList, MarkdownViewer, SessionItem,
  SidebarTaskItem, TeamDetailView, TeamListView, KanbanBoard, GlobalTaskList,
  MemberCard, TaskRow — all anonymous arrows inside memo() replaced with
  named function form
- Fix simple-import-sort violations in TeamDetailView, TeamListView,
  SchedulesView, ScheduleSection — static imports moved before lazy() consts
- Gate all lazy dialogs in TeamDetailView by their open flag so dynamic
  import() only fires when the dialog is actually opened:
  launchDialogOpen, createTaskDialog.open, sendDialogOpen,
  selectedTask !== null, reviewDialogState.open
This commit is contained in:
Mike 2026-05-03 08:57:59 +05:00
parent 49006ee589
commit 053caed8b6
12 changed files with 4537 additions and 4501 deletions

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { memo, useCallback, useState } from 'react';
import { import {
CODE_BG, CODE_BG,
@ -65,9 +65,6 @@ function buildItemMetaTooltip(
return parts.length > 0 ? parts.join(' • ') : undefined; return parts.length > 0 ? parts.join(' • ') : undefined;
} }
/**
* Truncates text to a maximum length and adds ellipsis if needed.
*/
function truncateText(text: string, maxLength: number): string { function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) { if (text.length <= maxLength) {
return text; return text;
@ -75,25 +72,63 @@ function truncateText(text: string, maxLength: number): string {
return text.substring(0, maxLength) + '...'; return text.substring(0, maxLength) + '...';
} }
/** function getItemKey(item: AIGroupDisplayItem, index: number): string {
* Renders a flat list of AIGroupDisplayItem[] into the appropriate components. switch (item.type) {
* case 'thinking':
* This component maps each display item to its corresponding component based on type: return `thinking-${index}`;
* - thinking -> ThinkingItem case 'output':
* - output -> TextItem return `output-${index}`;
* - tool -> LinkedToolItem case 'tool':
* - subagent -> SubagentItem return `tool-${item.tool.id}-${index}`;
* - slash -> SlashItem case 'subagent':
* return `subagent-${item.subagent.id}-${index}`;
* The list is completely flat with no nested toggles or hierarchies. case 'slash':
*/ return `slash-${item.slash.name}-${index}`;
export const DisplayItemList = React.memo( case 'teammate_message':
({ return `teammate-${item.teammateMessage.id}-${index}`;
items, case 'subagent_input':
return `input-${index}`;
case 'compact_boundary':
return `compact-${index}`;
default:
return `unknown-${index}`;
}
}
// =============================================================================
// Per-item row — memoized to prevent re-renders from parent state changes
// =============================================================================
interface DisplayItemRowProps {
item: AIGroupDisplayItem;
index: number;
itemKey: string;
isExpanded: boolean;
isDimmed: boolean;
hasReplyLink: boolean;
onItemClick: (key: string) => void;
onReplyHover: (toolId: string | null) => void;
aiGroupId: string;
searchQueryOverride?: string;
highlightToolUseId?: string;
highlightColor?: TriggerColor;
notificationColorMap?: Map<string, TriggerColor>;
registerToolRef?: (toolId: string, el: HTMLDivElement | null) => void;
previewMaxLength?: number;
timestampFormat?: string;
showItemMetaTooltip?: boolean;
}
const DisplayItemRow = memo(function DisplayItemRow({
item,
index: _index,
itemKey,
isExpanded,
isDimmed,
hasReplyLink,
onItemClick, onItemClick,
expandedItemIds, onReplyHover,
aiGroupId, aiGroupId,
order = 'chronological',
searchQueryOverride, searchQueryOverride,
highlightToolUseId, highlightToolUseId,
highlightColor, highlightColor,
@ -102,47 +137,13 @@ export const DisplayItemList = React.memo(
previewMaxLength, previewMaxLength,
timestampFormat, timestampFormat,
showItemMetaTooltip = false, showItemMetaTooltip = false,
}: Readonly<DisplayItemListProps>): React.JSX.Element => { }: DisplayItemRowProps): React.JSX.Element | null {
// Reply-link highlight: when hovering a reply badge, dim everything except the linked pair const handleClick = useCallback(() => onItemClick(itemKey), [onItemClick, itemKey]);
const [replyLinkToolId, setReplyLinkToolId] = useState<string | null>(null);
const handleReplyHover = useCallback((toolId: string | null) => {
setReplyLinkToolId(toolId);
}, []);
/** Check if an item is part of the currently highlighted reply link */
const isItemInReplyLink = (item: AIGroupDisplayItem): boolean => {
if (!replyLinkToolId) return false;
if (item.type === 'tool' && item.tool.id === replyLinkToolId) return true;
if (
item.type === 'teammate_message' &&
item.teammateMessage.replyToToolId === replyLinkToolId
)
return true;
return false;
};
if (!items || items.length === 0) {
return (
<div className="px-3 py-2 text-sm italic text-claude-dark-text-secondary">
No items to display
</div>
);
}
return (
<div
className={
order === 'newest-first' ? 'flex min-w-0 flex-col-reverse gap-2' : 'min-w-0 space-y-2'
}
>
{items.map((item, index) => {
let itemKey = '';
let element: React.ReactNode = null; let element: React.ReactNode = null;
switch (item.type) { switch (item.type) {
case 'thinking': { case 'thinking': {
itemKey = `thinking-${index}`;
const thinkingStep = { const thinkingStep = {
id: itemKey, id: itemKey,
type: 'thinking' as const, type: 'thinking' as const,
@ -157,8 +158,8 @@ export const DisplayItemList = React.memo(
<ThinkingItem <ThinkingItem
step={thinkingStep} step={thinkingStep}
preview={truncateText(item.content, previewMaxLength ?? 150)} preview={truncateText(item.content, previewMaxLength ?? 150)}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
timestamp={item.timestamp} timestamp={item.timestamp}
timestampFormat={timestampFormat} timestampFormat={timestampFormat}
titleText={ titleText={
@ -174,7 +175,6 @@ export const DisplayItemList = React.memo(
} }
case 'output': { case 'output': {
itemKey = `output-${index}`;
const textStep = { const textStep = {
id: itemKey, id: itemKey,
type: 'output' as const, type: 'output' as const,
@ -189,8 +189,8 @@ export const DisplayItemList = React.memo(
<TextItem <TextItem
step={textStep} step={textStep}
preview={truncateText(item.content, previewMaxLength ?? 150)} preview={truncateText(item.content, previewMaxLength ?? 150)}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
timestamp={item.timestamp} timestamp={item.timestamp}
timestampFormat={timestampFormat} timestampFormat={timestampFormat}
titleText={ titleText={
@ -206,37 +206,29 @@ export const DisplayItemList = React.memo(
} }
case 'tool': { case 'tool': {
itemKey = `tool-${item.tool.id}-${index}`;
element = ( element = (
<LinkedToolItem <LinkedToolItem
linkedTool={item.tool} linkedTool={item.tool}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
timestamp={item.tool.startTime} timestamp={item.tool.startTime}
timestampFormat={timestampFormat} timestampFormat={timestampFormat}
titleText={ titleText={
showItemMetaTooltip showItemMetaTooltip
? buildItemMetaTooltip( ? buildItemMetaTooltip(item.tool.startTime, getToolContextTokens(item.tool), 'tokens')
item.tool.startTime,
getToolContextTokens(item.tool),
'tokens'
)
: undefined : undefined
} }
searchQueryOverride={searchQueryOverride} searchQueryOverride={searchQueryOverride}
isHighlighted={highlightToolUseId === item.tool.id} isHighlighted={highlightToolUseId === item.tool.id}
highlightColor={highlightColor} highlightColor={highlightColor}
notificationDotColor={notificationColorMap?.get(item.tool.id)} notificationDotColor={notificationColorMap?.get(item.tool.id)}
registerRef={ registerRef={registerToolRef ? (el) => registerToolRef(item.tool.id, el) : undefined}
registerToolRef ? (el) => registerToolRef(item.tool.id, el) : undefined
}
/> />
); );
break; break;
} }
case 'subagent': { case 'subagent': {
itemKey = `subagent-${item.subagent.id}-${index}`;
const subagentStep = { const subagentStep = {
id: itemKey, id: itemKey,
type: 'subagent' as const, type: 'subagent' as const,
@ -254,8 +246,8 @@ export const DisplayItemList = React.memo(
<SubagentItem <SubagentItem
step={subagentStep} step={subagentStep}
subagent={item.subagent} subagent={item.subagent}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
aiGroupId={aiGroupId} aiGroupId={aiGroupId}
highlightToolUseId={highlightToolUseId} highlightToolUseId={highlightToolUseId}
highlightColor={highlightColor} highlightColor={highlightColor}
@ -267,12 +259,11 @@ export const DisplayItemList = React.memo(
} }
case 'slash': { case 'slash': {
itemKey = `slash-${item.slash.name}-${index}`;
element = ( element = (
<SlashItem <SlashItem
slash={item.slash} slash={item.slash}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
timestamp={item.slash.timestamp} timestamp={item.slash.timestamp}
timestampFormat={timestampFormat} timestampFormat={timestampFormat}
titleText={ titleText={
@ -290,20 +281,18 @@ export const DisplayItemList = React.memo(
} }
case 'teammate_message': { case 'teammate_message': {
itemKey = `teammate-${item.teammateMessage.id}-${index}`;
element = ( element = (
<TeammateMessageItem <TeammateMessageItem
teammateMessage={item.teammateMessage} teammateMessage={item.teammateMessage}
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
onReplyHover={handleReplyHover} onReplyHover={onReplyHover}
/> />
); );
break; break;
} }
case 'subagent_input': { case 'subagent_input': {
itemKey = `input-${index}`;
const inputContent = item.content; const inputContent = item.content;
const inputTokenCount = item.tokenCount; const inputTokenCount = item.tokenCount;
element = ( element = (
@ -319,8 +308,8 @@ export const DisplayItemList = React.memo(
? buildItemMetaTooltip(item.timestamp, inputTokenCount, 'tokens') ? buildItemMetaTooltip(item.timestamp, inputTokenCount, 'tokens')
: undefined : undefined
} }
onClick={() => onItemClick(itemKey)} onClick={handleClick}
isExpanded={expandedItemIds.has(itemKey)} isExpanded={isExpanded}
> >
<MarkdownViewer <MarkdownViewer
content={inputContent} content={inputContent}
@ -334,34 +323,26 @@ export const DisplayItemList = React.memo(
} }
case 'compact_boundary': { case 'compact_boundary': {
itemKey = `compact-${index}`;
const compactContent = item.content; const compactContent = item.content;
const compactExpanded = expandedItemIds.has(itemKey);
element = ( element = (
<div> <div>
<button <button
onClick={() => onItemClick(itemKey)} onClick={handleClick}
className="group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 transition-all duration-200" className="group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 transition-all duration-200"
style={{ style={{
backgroundColor: TOOL_CALL_BG, backgroundColor: TOOL_CALL_BG,
border: `1px solid ${TOOL_CALL_BORDER}`, border: `1px solid ${TOOL_CALL_BORDER}`,
}} }}
aria-expanded={compactExpanded} aria-expanded={isExpanded}
>
<div
className="flex shrink-0 items-center gap-1.5"
style={{ color: TOOL_CALL_TEXT }}
> >
<div className="flex shrink-0 items-center gap-1.5" style={{ color: TOOL_CALL_TEXT }}>
<ChevronRight <ChevronRight
size={14} size={14}
className={`transition-transform duration-200 ${compactExpanded ? 'rotate-90' : ''}`} className={`transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
/> />
<Layers size={14} /> <Layers size={14} />
</div> </div>
<span <span className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }}>
className="shrink-0 text-xs font-medium"
style={{ color: TOOL_CALL_TEXT }}
>
Compacted Compacted
</span> </span>
{item.tokenDelta && ( {item.tokenDelta && (
@ -386,14 +367,11 @@ export const DisplayItemList = React.memo(
> >
Phase {item.phaseNumber} Phase {item.phaseNumber}
</span> </span>
<span <span className="ml-auto shrink-0 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
className="ml-auto shrink-0 text-[11px]"
style={{ color: COLOR_TEXT_MUTED }}
>
{format(new Date(item.timestamp), 'h:mm:ss a')} {format(new Date(item.timestamp), 'h:mm:ss a')}
</span> </span>
</button> </button>
{compactExpanded && compactContent && ( {isExpanded && compactContent && (
<div <div
className="mt-1 overflow-hidden rounded-lg" className="mt-1 overflow-hidden rounded-lg"
style={{ style={{
@ -418,22 +396,102 @@ export const DisplayItemList = React.memo(
return null; return null;
} }
// Apply reply-link spotlight: dim items not in the highlighted pair
const isDimmed = replyLinkToolId !== null && !isItemInReplyLink(item);
return ( return (
<div <div
key={itemKey}
style={ style={
replyLinkToolId !== null hasReplyLink ? { opacity: isDimmed ? 0.2 : 1, transition: 'opacity 150ms ease' } : undefined
? { opacity: isDimmed ? 0.2 : 1, transition: 'opacity 150ms ease' }
: undefined
} }
> >
{element} {element}
</div> </div>
); );
})} });
// =============================================================================
// Main component
// =============================================================================
/**
* Renders a flat list of AIGroupDisplayItem[] into the appropriate components.
*
* This component maps each display item to its corresponding component based on type:
* - thinking -> ThinkingItem
* - output -> TextItem
* - tool -> LinkedToolItem
* - subagent -> SubagentItem
* - slash -> SlashItem
*
* The list is completely flat with no nested toggles or hierarchies.
*/
export const DisplayItemList = React.memo(function DisplayItemList({
items,
onItemClick,
expandedItemIds,
aiGroupId,
order = 'chronological',
searchQueryOverride,
highlightToolUseId,
highlightColor,
notificationColorMap,
registerToolRef,
previewMaxLength,
timestampFormat,
showItemMetaTooltip = false,
}: Readonly<DisplayItemListProps>): React.JSX.Element {
const [replyLinkToolId, setReplyLinkToolId] = useState<string | null>(null);
const handleReplyHover = useCallback((toolId: string | null) => {
setReplyLinkToolId(toolId);
}, []);
if (!items || items.length === 0) {
return (
<div className="px-3 py-2 text-sm italic text-claude-dark-text-secondary">
No items to display
</div> </div>
); );
} }
return (
<div
className={
order === 'newest-first' ? 'flex min-w-0 flex-col-reverse gap-2' : 'min-w-0 space-y-2'
}
>
{items.map((item, index) => {
const itemKey = getItemKey(item, index);
const isExpanded = expandedItemIds.has(itemKey);
const isInReplyLink =
replyLinkToolId !== null &&
((item.type === 'tool' && item.tool.id === replyLinkToolId) ||
(item.type === 'teammate_message' &&
item.teammateMessage.replyToToolId === replyLinkToolId));
const isDimmed = replyLinkToolId !== null && !isInReplyLink;
return (
<DisplayItemRow
key={itemKey}
item={item}
index={index}
itemKey={itemKey}
isExpanded={isExpanded}
isDimmed={isDimmed}
hasReplyLink={replyLinkToolId !== null}
onItemClick={onItemClick}
onReplyHover={handleReplyHover}
aiGroupId={aiGroupId}
searchQueryOverride={searchQueryOverride}
highlightToolUseId={highlightToolUseId}
highlightColor={highlightColor}
notificationColorMap={notificationColorMap}
registerToolRef={registerToolRef}
previewMaxLength={previewMaxLength}
timestampFormat={timestampFormat}
showItemMetaTooltip={showItemMetaTooltip}
/>
); );
})}
</div>
);
});

View file

@ -946,8 +946,7 @@ export const CompactMarkdownPreview: React.FC<CompactMarkdownPreviewProps> = Rea
} }
); );
export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo( export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function MarkdownViewer({
({
content, content,
maxHeight = 'max-h-96', maxHeight = 'max-h-96',
className = '', className = '',
@ -959,7 +958,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
baseDir, baseDir,
teamColorByName: providedTeamColorByName, teamColorByName: providedTeamColorByName,
onTeamClick: providedOnTeamClick, onTeamClick: providedOnTeamClick,
}) => { }) {
const [showRaw, setShowRaw] = React.useState(false); const [showRaw, setShowRaw] = React.useState(false);
const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS); const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS);
const { isLight } = useTheme(); const { isLight } = useTheme();
@ -1063,8 +1062,8 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
{isTooLarge && ( {isTooLarge && (
<div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}> <div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to
to keep the UI responsive. keep the UI responsive.
</div> </div>
)} )}
@ -1196,5 +1195,4 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
</div> </div>
</div> </div>
); );
} });
);

View file

@ -25,12 +25,12 @@ import {
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog'; import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog';
import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow';
import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge';
const LaunchTeamDialog = lazy(() => const LaunchTeamDialog = lazy(() =>
import('../team/dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog })) import('../team/dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
); );
import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow';
import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge';
import type { Schedule, ScheduleRun, ScheduleStatus } from '@shared/types'; import type { Schedule, ScheduleRun, ScheduleStatus } from '@shared/types';

View file

@ -173,14 +173,13 @@ function applyProjectFilter(tasks: GlobalTask[], projectPath: string | null): Gl
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized); return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
} }
export const GlobalTaskList = memo( export const GlobalTaskList = memo(function GlobalTaskList({
({
hideHeader = false, hideHeader = false,
filters: externalFilters, filters: externalFilters,
onFiltersChange: externalOnFiltersChange, onFiltersChange: externalOnFiltersChange,
filtersPopoverOpen: externalFiltersPopoverOpen, filtersPopoverOpen: externalFiltersPopoverOpen,
onFiltersPopoverOpenChange: externalOnFiltersPopoverOpenChange, onFiltersPopoverOpenChange: externalOnFiltersPopoverOpenChange,
}: GlobalTaskListProps = {}): React.JSX.Element => { }: GlobalTaskListProps = {}): React.JSX.Element {
const { const {
globalTasks, globalTasks,
globalTasksLoading, globalTasksLoading,
@ -210,8 +209,7 @@ export const GlobalTaskList = memo(
const filters = externalFilters ?? internalFilters; const filters = externalFilters ?? internalFilters;
const setFilters = externalOnFiltersChange ?? setInternalFilters; const setFilters = externalOnFiltersChange ?? setInternalFilters;
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen; const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
const setFiltersPopoverOpen = const setFiltersPopoverOpen = externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode); const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode); const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
@ -273,16 +271,20 @@ export const GlobalTaskList = memo(
saveSortMode(mode); saveSortMode(mode);
}; };
const handleRenameComplete = (teamName: string, taskId: string, newSubject: string): void => { const handleRenameComplete = useCallback(
(teamName: string, taskId: string, newSubject: string): void => {
taskLocalState.renameTask(teamName, taskId, newSubject); taskLocalState.renameTask(teamName, taskId, newSubject);
setRenamingTaskKey(null); setRenamingTaskKey(null);
}; },
[taskLocalState]
);
const handleRenameCancel = (): void => { const handleRenameCancel = useCallback((): void => {
setRenamingTaskKey(null); setRenamingTaskKey(null);
}; }, []);
const handleDeleteTask = async (teamName: string, taskId: string): Promise<void> => { const handleDeleteTask = useCallback(
async (teamName: string, taskId: string): Promise<void> => {
const confirmed = await confirm({ const confirmed = await confirm({
title: 'Delete task', title: 'Delete task',
message: `Move task #${deriveTaskDisplayId(taskId)} to trash?`, message: `Move task #${deriveTaskDisplayId(taskId)} to trash?`,
@ -303,7 +305,9 @@ export const GlobalTaskList = memo(
}); });
} }
} }
}; },
[fetchAllTasks, softDeleteTask]
);
// Fetch tasks on mount — loading guard in the store action prevents // Fetch tasks on mount — loading guard in the store action prevents
// duplicate IPC calls when the centralized init chain is already fetching. // duplicate IPC calls when the centralized init chain is already fetching.
@ -418,10 +422,7 @@ export const GlobalTaskList = memo(
); );
const projectVisibleCountByKey = useMemo( const projectVisibleCountByKey = useMemo(
() => () =>
syncProjectGroupVisibleCountByKey( syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
projectRequestedVisibleCountByKey,
projectGroupVisibility
),
[projectRequestedVisibleCountByKey, projectGroupVisibility] [projectRequestedVisibleCountByKey, projectGroupVisibility]
); );
@ -578,9 +579,7 @@ export const GlobalTaskList = memo(
onClick={() => setGroupingMode(mode)} onClick={() => setGroupingMode(mode)}
className={cn( className={cn(
'rounded px-1.5 py-0.5 transition-colors', 'rounded px-1.5 py-0.5 transition-colors',
groupingMode === mode groupingMode === mode ? 'text-text' : 'text-text-muted hover:text-text-secondary'
? 'text-text'
: 'text-text-muted hover:text-text-secondary'
)} )}
> >
{label} {label}
@ -857,5 +856,4 @@ export const GlobalTaskList = memo(
</div> </div>
</div> </div>
); );
} });
);

View file

@ -155,15 +155,14 @@ const SessionRuntimeBadge = ({
); );
}; };
export const SessionItem = memo( export const SessionItem = memo(function SessionItem({
({
session, session,
isActive, isActive,
isPinned, isPinned,
isHidden, isHidden,
multiSelectActive, multiSelectActive,
isSelected, isSelected,
}: Readonly<SessionItemProps>): React.JSX.Element => { }: Readonly<SessionItemProps>): React.JSX.Element {
const { const {
openTab, openTab,
activeProjectId, activeProjectId,
@ -346,9 +345,7 @@ export const SessionItem = memo(
{session.messageCount} {session.messageCount}
</span> </span>
<span style={{ opacity: 0.5 }}>·</span> <span style={{ opacity: 0.5 }}>·</span>
<span className="tabular-nums"> <span className="tabular-nums">{formatShortTime(new Date(session.createdAt))}</span>
{formatShortTime(new Date(session.createdAt))}
</span>
{session.model && ( {session.model && (
<> <>
<span style={{ opacity: 0.5 }}>·</span> <span style={{ opacity: 0.5 }}>·</span>
@ -393,5 +390,4 @@ export const SessionItem = memo(
)} )}
</> </>
); );
} });
);

View file

@ -69,8 +69,7 @@ interface SidebarTaskItemProps {
getDisplaySubject?: (task: GlobalTask) => string | undefined; getDisplaySubject?: (task: GlobalTask) => string | undefined;
} }
export const SidebarTaskItem = memo( export const SidebarTaskItem = memo(function SidebarTaskItem({
({
task, task,
hideTeamName, hideTeamName,
showTeamName, showTeamName,
@ -78,7 +77,7 @@ export const SidebarTaskItem = memo(
onRenameComplete, onRenameComplete,
onRenameCancel, onRenameCancel,
getDisplaySubject, getDisplaySubject,
}: SidebarTaskItemProps): React.JSX.Element => { }: SidebarTaskItemProps): React.JSX.Element {
const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail); const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail);
const teamMembers = useStore(useShallow((s) => s.teamByName[task.teamName]?.members)); const teamMembers = useStore(useShallow((s) => s.teamByName[task.teamName]?.members));
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments); const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
@ -284,5 +283,4 @@ export const SidebarTaskItem = memo(
)} )}
</button> </button>
); );
} });
);

View file

@ -79,7 +79,6 @@ import { useShallow } from 'zustand/react/shallow';
import { AddMemberDialog } from './dialogs/AddMemberDialog'; import { AddMemberDialog } from './dialogs/AddMemberDialog';
import { EditTeamDialog } from './dialogs/EditTeamDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog';
import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
import { ReviewDialog } from './dialogs/ReviewDialog'; import { ReviewDialog } from './dialogs/ReviewDialog';
import { executeTeamRelaunch } from './dialogs/teamRelaunchFlow'; import { executeTeamRelaunch } from './dialogs/teamRelaunchFlow';
import { KanbanBoard } from './kanban/KanbanBoard'; import { KanbanBoard } from './kanban/KanbanBoard';
@ -90,6 +89,7 @@ import { MemberDetailDialog } from './members/MemberDetailDialog';
import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes'; import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes';
import type { AddMemberEntry } from './dialogs/AddMemberDialog'; import type { AddMemberEntry } from './dialogs/AddMemberDialog';
import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode'; import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
import type { ComponentProps, CSSProperties } from 'react'; import type { ComponentProps, CSSProperties } from 'react';
@ -958,8 +958,10 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
); );
}); });
export const TeamDetailView = memo( export const TeamDetailView = memo(function TeamDetailView({
({ teamName, isPaneFocused = false }: TeamDetailViewProps): React.JSX.Element => { teamName,
isPaneFocused = false,
}: TeamDetailViewProps): React.JSX.Element {
const { isLight } = useTheme(); const { isLight } = useTheme();
const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null); const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null); const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
@ -968,8 +970,8 @@ export const TeamDetailView = memo(
initialTab?: MemberDetailTab; initialTab?: MemberDetailTab;
initialActivityFilter?: MemberActivityFilter; initialActivityFilter?: MemberActivityFilter;
} | null>(null); } | null>(null);
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>( const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(() =>
() => getTeamPendingRepliesState(teamName) getTeamPendingRepliesState(teamName)
); );
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({ const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
open: false, open: false,
@ -1201,9 +1203,7 @@ export const TeamDetailView = memo(
const [stoppingTeam, setStoppingTeam] = useState(false); const [stoppingTeam, setStoppingTeam] = useState(false);
const [trashOpen, setTrashOpen] = useState(false); const [trashOpen, setTrashOpen] = useState(false);
const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined); const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined);
const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>( const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(undefined);
undefined
);
const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>( const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>(
undefined undefined
); );
@ -1364,8 +1364,7 @@ export const TeamDetailView = memo(
maxWidth: 600, maxWidth: 600,
side: 'left', side: 'left',
}); });
const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } = const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } = useResizablePanel({
useResizablePanel({
height: sidebarLogsHeight, height: sidebarLogsHeight,
onHeightChange: setSidebarLogsHeight, onHeightChange: setSidebarLogsHeight,
minHeight: 120, minHeight: 120,
@ -1715,17 +1714,11 @@ export const TeamDetailView = memo(
}, [activeMembers, data?.config.members, launchParams?.providerId]); }, [activeMembers, data?.config.members, launchParams?.providerId]);
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId); const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
const taskMap = useMemo( const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]);
() => new Map((data?.tasks ?? []).map((t) => [t.id, t])),
[data?.tasks]
);
const taskMapRef = useRef(taskMap); const taskMapRef = useRef(taskMap);
taskMapRef.current = taskMap; taskMapRef.current = taskMap;
const memberTaskCounts = useMemo( const memberTaskCounts = useMemo(() => buildTaskCountsByOwner(data?.tasks ?? []), [data?.tasks]);
() => buildTaskCountsByOwner(data?.tasks ?? []),
[data?.tasks]
);
const openCreateTaskDialog = useCallback( const openCreateTaskDialog = useCallback(
(subject = '', description = '', owner = '', startImmediately?: boolean): void => { (subject = '', description = '', owner = '', startImmediately?: boolean): void => {
@ -2029,13 +2022,7 @@ export const TeamDetailView = memo(
startImmediately, startImmediately,
}); });
if ( if (prompt && owner && data?.isAlive && !isTeamProvisioning && startImmediately !== false) {
prompt &&
owner &&
data?.isAlive &&
!isTeamProvisioning &&
startImmediately !== false
) {
const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`; const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`;
try { try {
await api.teams.processSend(teamName, msg); await api.teams.processSend(teamName, msg);
@ -2187,6 +2174,7 @@ export const TeamDetailView = memo(
</div> </div>
</div> </div>
</div> </div>
{launchDialogOpen && (
<Suspense fallback={null}> <Suspense fallback={null}>
<LaunchTeamDialog <LaunchTeamDialog
mode={launchDialogState.mode} mode={launchDialogState.mode}
@ -2201,6 +2189,7 @@ export const TeamDetailView = memo(
onRelaunch={handleRelaunchDialogSubmit} onRelaunch={handleRelaunchDialogSubmit}
/> />
</Suspense> </Suspense>
)}
</> </>
); );
} }
@ -2377,10 +2366,7 @@ export const TeamDetailView = memo(
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5"> <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5">
{data.config.projectPath && ( {data.config.projectPath && (
<span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]"> <span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]">
<FolderOpen <FolderOpen size={11} className="shrink-0 text-[var(--color-text-muted)]" />
size={11}
className="shrink-0 text-[var(--color-text-muted)]"
/>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="max-w-60 truncate font-mono"> <span className="max-w-60 truncate font-mono">
@ -2415,10 +2401,7 @@ export const TeamDetailView = memo(
className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]" className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]"
title={leadBranch} title={leadBranch}
> >
<GitBranch <GitBranch size={11} className="shrink-0 text-[var(--color-text-muted)]" />
size={11}
className="shrink-0 text-[var(--color-text-muted)]"
/>
<span className="max-w-32 truncate">{leadBranch}</span> <span className="max-w-32 truncate">{leadBranch}</span>
</span> </span>
)} )}
@ -2447,9 +2430,7 @@ export const TeamDetailView = memo(
</div> </div>
{(() => { {(() => {
const currentPath = data.config.projectPath; const currentPath = data.config.projectPath;
const history = data.config.projectPathHistory?.filter( const history = data.config.projectPathHistory?.filter((p) => p !== currentPath);
(p) => p !== currentPath
);
if (!history || history.length === 0) return null; if (!history || history.length === 0) return null;
return ( return (
<div <div
@ -2542,9 +2523,7 @@ export const TeamDetailView = memo(
sessionsError={sessionsError} sessionsError={sessionsError}
leadSessionId={data.config.leadSessionId} leadSessionId={data.config.leadSessionId}
selectedSessionId={kanbanFilter.sessionId} selectedSessionId={kanbanFilter.sessionId}
onSelectSession={(id) => onSelectSession={(id) => setKanbanFilter((prev) => ({ ...prev, sessionId: id }))}
setKanbanFilter((prev) => ({ ...prev, sessionId: id }))
}
projectPath={data.config.projectPath} projectPath={data.config.projectPath}
/> />
</CollapsibleTeamSection> </CollapsibleTeamSection>
@ -2865,6 +2844,7 @@ export const TeamDetailView = memo(
}} }}
/> />
{createTaskDialog.open && (
<Suspense fallback={null}> <Suspense fallback={null}>
<CreateTaskDialog <CreateTaskDialog
open={createTaskDialog.open} open={createTaskDialog.open}
@ -2882,6 +2862,7 @@ export const TeamDetailView = memo(
submitting={creatingTask} submitting={creatingTask}
/> />
</Suspense> </Suspense>
)}
<EditTeamDialog <EditTeamDialog
open={editDialogOpen} open={editDialogOpen}
@ -2948,11 +2929,7 @@ export const TeamDetailView = memo(
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button <Button variant="ghost" size="sm" onClick={() => setRemoveMemberConfirm(null)}>
variant="ghost"
size="sm"
onClick={() => setRemoveMemberConfirm(null)}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@ -2976,8 +2953,8 @@ export const TeamDetailView = memo(
<DialogHeader> <DialogHeader>
<DialogTitle>Delete team</DialogTitle> <DialogTitle>Delete team</DialogTitle>
<DialogDescription> <DialogDescription>
Delete team &ldquo;{data.config.name}&rdquo;? This action is irreversible. Delete team &ldquo;{data.config.name}&rdquo;? This action is irreversible. All
All team data and tasks will be deleted. team data and tasks will be deleted.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
@ -2991,6 +2968,7 @@ export const TeamDetailView = memo(
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{launchDialogOpen && (
<Suspense fallback={null}> <Suspense fallback={null}>
<LaunchTeamDialog <LaunchTeamDialog
mode={launchDialogState.mode} mode={launchDialogState.mode}
@ -3006,7 +2984,9 @@ export const TeamDetailView = memo(
onRelaunch={handleRelaunchDialogSubmit} onRelaunch={handleRelaunchDialogSubmit}
/> />
</Suspense> </Suspense>
)}
{sendDialogOpen && (
<Suspense fallback={null}> <Suspense fallback={null}>
<SendMessageDialog <SendMessageDialog
open={sendDialogOpen} open={sendDialogOpen}
@ -3064,7 +3044,9 @@ export const TeamDetailView = memo(
}} }}
/> />
</Suspense> </Suspense>
)}
{selectedTask !== null && (
<Suspense fallback={null}> <Suspense fallback={null}>
<TaskDetailDialog <TaskDetailDialog
open={selectedTask !== null} open={selectedTask !== null}
@ -3108,6 +3090,7 @@ export const TeamDetailView = memo(
onDeleteTask={handleDeleteTask} onDeleteTask={handleDeleteTask}
/> />
</Suspense> </Suspense>
)}
<TrashDialog <TrashDialog
open={trashOpen} open={trashOpen}
@ -3124,6 +3107,7 @@ export const TeamDetailView = memo(
}} }}
/> />
{reviewDialogState.open && (
<Suspense fallback={null}> <Suspense fallback={null}>
<ChangeReviewDialog <ChangeReviewDialog
open={reviewDialogState.open} open={reviewDialogState.open}
@ -3146,6 +3130,7 @@ export const TeamDetailView = memo(
onEditorAction={handleEditorAction} onEditorAction={handleEditorAction}
/> />
</Suspense> </Suspense>
)}
</div> </div>
<div <div
ref={setMessagesPanelMountPoint} ref={setMessagesPanelMountPoint}
@ -3213,5 +3198,4 @@ export const TeamDetailView = memo(
{renderBody()} {renderBody()}
</> </>
); );
} });
);

View file

@ -45,7 +45,6 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
import { TeamEmptyState } from './TeamEmptyState'; import { TeamEmptyState } from './TeamEmptyState';
import { EMPTY_TEAM_FILTER, TeamListFilterPopover } from './TeamListFilterPopover'; import { EMPTY_TEAM_FILTER, TeamListFilterPopover } from './TeamListFilterPopover';
import { import {
@ -54,13 +53,7 @@ import {
teamMatchesProjectSelection, teamMatchesProjectSelection,
} from './teamProjectSelection'; } from './teamProjectSelection';
const CreateTeamDialog = lazy(() => import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
import('./dialogs/CreateTeamDialog').then((m) => ({ default: m.CreateTeamDialog }))
);
const LaunchTeamDialog = lazy(() =>
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
);
import type { TeamListFilterState } from './TeamListFilterPopover'; import type { TeamListFilterState } from './TeamListFilterPopover';
import type { TeamStatus } from '@renderer/utils/teamListStatus'; import type { TeamStatus } from '@renderer/utils/teamListStatus';
import type { import type {
@ -72,6 +65,13 @@ import type {
TeamSummaryMember, TeamSummaryMember,
} from '@shared/types'; } from '@shared/types';
const CreateTeamDialog = lazy(() =>
import('./dialogs/CreateTeamDialog').then((m) => ({ default: m.CreateTeamDialog }))
);
const LaunchTeamDialog = lazy(() =>
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
);
function generateUniqueName(sourceName: string, existingNames: string[]): string { function generateUniqueName(sourceName: string, existingNames: string[]): string {
const base = sourceName.replace(/-\d+$/, ''); const base = sourceName.replace(/-\d+$/, '');
const existing = new Set(existingNames); const existing = new Set(existingNames);
@ -238,7 +238,7 @@ const StatusBadge = ({ status }: { status: TeamStatus }): React.JSX.Element => {
} }
}; };
export const TeamListView = memo((): React.JSX.Element => { export const TeamListView = memo(function TeamListView(): React.JSX.Element {
const { isLight } = useTheme(); const { isLight } = useTheme();
const electronMode = isElectronMode(); const electronMode = isElectronMode();
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);

View file

@ -311,8 +311,7 @@ const SortableKanbanTaskCard = ({
); );
}; };
export const KanbanBoard = memo( export const KanbanBoard = memo(function KanbanBoard({
({
tasks, tasks,
teamName, teamName,
kanbanState, kanbanState,
@ -339,7 +338,7 @@ export const KanbanBoard = memo(
onDeleteTask, onDeleteTask,
deletedTaskCount, deletedTaskCount,
onOpenTrash, onOpenTrash,
}: KanbanBoardProps): React.JSX.Element => { }: KanbanBoardProps): React.JSX.Element {
const boardRef = useRef<HTMLDivElement>(null); const boardRef = useRef<HTMLDivElement>(null);
const scrollRestoreTimeoutsRef = useRef<number[]>([]); const scrollRestoreTimeoutsRef = useRef<number[]>([]);
const [viewMode, setViewMode] = useState<KanbanViewMode>('grid'); const [viewMode, setViewMode] = useState<KanbanViewMode>('grid');
@ -423,11 +422,8 @@ export const KanbanBoard = memo(
[onColumnOrderChange, groupedOrdered] [onColumnOrderChange, groupedOrdered]
); );
const renderCards = ( const renderCards = useCallback(
columnId: KanbanColumnId, (columnId: KanbanColumnId, columnTasks: TeamTask[], compact?: boolean): React.JSX.Element => {
columnTasks: TeamTask[],
compact?: boolean
): React.JSX.Element => {
const addHandler = const addHandler =
onAddTask && columnId === 'todo' onAddTask && columnId === 'todo'
? () => onAddTask(false) ? () => onAddTask(false)
@ -517,7 +513,28 @@ export const KanbanBoard = memo(
{addButton} {addButton}
</> </>
); );
}; },
[
enableTaskSorting,
hasReviewers,
kanbanState,
memberColorMap,
onAddTask,
onApprove,
onCancelTask,
onCompleteTask,
onDeleteTask,
onMoveBackToDone,
onRequestChanges,
onRequestReview,
onScrollToTask,
onStartTask,
onTaskClick,
onViewChanges,
taskMap,
teamName,
]
);
const visibleColumns = useMemo( const visibleColumns = useMemo(
() => (filter.columns.size > 0 ? COLUMNS.filter((c) => filter.columns.has(c.id)) : COLUMNS), () => (filter.columns.size > 0 ? COLUMNS.filter((c) => filter.columns.has(c.id)) : COLUMNS),
@ -533,9 +550,7 @@ export const KanbanBoard = memo(
const columnModeSearchWidth = const columnModeSearchWidth =
primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256; primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256;
const toolbarLeftWidth = const toolbarLeftWidth =
viewMode === 'grid' viewMode === 'grid' ? (gridPrimaryColumnWidth ?? columnModeSearchWidth) : columnModeSearchWidth;
? (gridPrimaryColumnWidth ?? columnModeSearchWidth)
: columnModeSearchWidth;
const clearScheduledScrollRestore = useCallback(() => { const clearScheduledScrollRestore = useCallback(() => {
for (const timeoutId of scrollRestoreTimeoutsRef.current) { for (const timeoutId of scrollRestoreTimeoutsRef.current) {
@ -594,6 +609,29 @@ export const KanbanBoard = memo(
[scheduleScrollRestore, viewMode] [scheduleScrollRestore, viewMode]
); );
const gridColumns = useMemo(
() =>
visibleColumns.map((column) => {
const columnTasks = groupedOrdered.get(column.id) ?? [];
const accent = COLUMN_ACCENTS[column.id];
return {
id: column.id,
title: column.title,
count: columnTasks.length,
icon: accent.icon,
headerBg: accent.headerBg,
bodyBg: accent.bodyBg,
content: renderCards(column.id, columnTasks),
showAddButton: columnSupportsAddButton(column.id, onAddTask),
skeletonCards: columnTasks.map((task) => ({
key: task.id,
height: estimateGridSkeletonCardHeight(task, column.id, kanbanState, hasReviewers),
})),
};
}),
[visibleColumns, groupedOrdered, renderCards, onAddTask, kanbanState, hasReviewers]
);
const boardContent = ( const boardContent = (
<div ref={boardRef} className="min-w-0 max-w-full overflow-x-hidden"> <div ref={boardRef} className="min-w-0 max-w-full overflow-x-hidden">
<div <div
@ -685,30 +723,7 @@ export const KanbanBoard = memo(
primaryColumnId={primaryVisibleColumnId} primaryColumnId={primaryVisibleColumnId}
onPrimaryColumnWidthChange={setGridPrimaryColumnWidth} onPrimaryColumnWidthChange={setGridPrimaryColumnWidth}
skeletonDelayMs={gridSkeletonDelayMs} skeletonDelayMs={gridSkeletonDelayMs}
columns={visibleColumns.map((column) => { columns={gridColumns}
const columnTasks = groupedOrdered.get(column.id) ?? [];
const accent = COLUMN_ACCENTS[column.id];
return {
id: column.id,
title: column.title,
count: columnTasks.length,
icon: accent.icon,
headerBg: accent.headerBg,
bodyBg: accent.bodyBg,
content: renderCards(column.id, columnTasks),
showAddButton: columnSupportsAddButton(column.id, onAddTask),
skeletonCards: columnTasks.map((task) => ({
key: task.id,
height: estimateGridSkeletonCardHeight(
task,
column.id,
kanbanState,
hasReviewers
),
})),
};
})}
/> />
) : ( ) : (
<div className="w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden px-1 pb-6 pr-4 pt-2"> <div className="w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden px-1 pb-6 pr-4 pt-2">
@ -760,5 +775,4 @@ export const KanbanBoard = memo(
} }
return boardContent; return boardContent;
} });
);

View file

@ -91,8 +91,7 @@ function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): {
}; };
} }
export const MemberCard = memo( export const MemberCard = memo(function MemberCard({
({
member, member,
memberColor, memberColor,
runtimeSummary, runtimeSummary,
@ -120,7 +119,7 @@ export const MemberCard = memo(
onAssignTask, onAssignTask,
onRestartMember, onRestartMember,
onSkipMemberForLaunch, onSkipMemberForLaunch,
}: MemberCardProps): React.JSX.Element => { }: MemberCardProps): React.JSX.Element {
// NOTE: lead context display disabled — usage formula is inaccurate // NOTE: lead context display disabled — usage formula is inaccurate
// const teamName = useStore((s) => s.selectedTeamName); // const teamName = useStore((s) => s.selectedTeamName);
// const leadContext = useStore((s) => // const leadContext = useStore((s) =>
@ -278,9 +277,7 @@ export const MemberCard = memo(
const restartActionErrorFallback = canRelaunchOpenCode const restartActionErrorFallback = canRelaunchOpenCode
? 'Failed to relaunch OpenCode teammate' ? 'Failed to relaunch OpenCode teammate'
: 'Failed to retry teammate'; : 'Failed to retry teammate';
const handleRestartMember = async ( const handleRestartMember = async (event: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
event: React.MouseEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (!onRestartMember || retryingLaunch) { if (!onRestartMember || retryingLaunch) {
@ -473,9 +470,7 @@ export const MemberCard = memo(
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={ aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-amber-300 transition-colors hover:bg-amber-500/10 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60" className="rounded p-1 text-amber-300 transition-colors hover:bg-amber-500/10 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch} disabled={retryingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -544,9 +539,7 @@ export const MemberCard = memo(
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={ aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60" className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch || skippingLaunch} disabled={retryingLaunch || skippingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -588,9 +581,7 @@ export const MemberCard = memo(
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={ aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-zinc-300 transition-colors hover:bg-zinc-500/10 hover:text-zinc-100 disabled:cursor-not-allowed disabled:opacity-60" className="rounded p-1 text-zinc-300 transition-colors hover:bg-zinc-500/10 hover:text-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch} disabled={retryingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -718,5 +709,4 @@ export const MemberCard = memo(
</div> </div>
</div> </div>
); );
} });
);

View file

@ -19,13 +19,13 @@ import {
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { ScheduleEmptyState } from './ScheduleEmptyState'; import { ScheduleEmptyState } from './ScheduleEmptyState';
import { ScheduleRunLogDialog } from './ScheduleRunLogDialog';
import { ScheduleRunRow } from './ScheduleRunRow';
import { ScheduleStatusBadge } from './ScheduleStatusBadge';
const LaunchTeamDialog = lazy(() => const LaunchTeamDialog = lazy(() =>
import('../dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog })) import('../dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
); );
import { ScheduleRunLogDialog } from './ScheduleRunLogDialog';
import { ScheduleRunRow } from './ScheduleRunRow';
import { ScheduleStatusBadge } from './ScheduleStatusBadge';
import type { Schedule, ScheduleRun } from '@shared/types'; import type { Schedule, ScheduleRun } from '@shared/types';

View file

@ -14,7 +14,7 @@ interface TaskRowProps {
task: TeamTaskWithKanban; task: TeamTaskWithKanban;
} }
export const TaskRow = memo(({ task }: TaskRowProps): React.JSX.Element => { export const TaskRow = memo(function TaskRow({ task }: TaskRowProps): React.JSX.Element {
const blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? []; const blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? [];
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? []; const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
const kanbanColumn = getTaskKanbanColumn(task); const kanbanColumn = getTaskKanbanColumn(task);