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:
parent
49006ee589
commit
053caed8b6
12 changed files with 4537 additions and 4501 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
CODE_BG,
|
||||
|
|
@ -65,9 +65,6 @@ function buildItemMetaTooltip(
|
|||
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 {
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
|
|
@ -75,25 +72,63 @@ function truncateText(text: string, maxLength: number): string {
|
|||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
({
|
||||
items,
|
||||
function getItemKey(item: AIGroupDisplayItem, index: number): string {
|
||||
switch (item.type) {
|
||||
case 'thinking':
|
||||
return `thinking-${index}`;
|
||||
case 'output':
|
||||
return `output-${index}`;
|
||||
case 'tool':
|
||||
return `tool-${item.tool.id}-${index}`;
|
||||
case 'subagent':
|
||||
return `subagent-${item.subagent.id}-${index}`;
|
||||
case 'slash':
|
||||
return `slash-${item.slash.name}-${index}`;
|
||||
case 'teammate_message':
|
||||
return `teammate-${item.teammateMessage.id}-${index}`;
|
||||
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,
|
||||
expandedItemIds,
|
||||
onReplyHover,
|
||||
aiGroupId,
|
||||
order = 'chronological',
|
||||
searchQueryOverride,
|
||||
highlightToolUseId,
|
||||
highlightColor,
|
||||
|
|
@ -102,47 +137,13 @@ export const DisplayItemList = React.memo(
|
|||
previewMaxLength,
|
||||
timestampFormat,
|
||||
showItemMetaTooltip = false,
|
||||
}: Readonly<DisplayItemListProps>): React.JSX.Element => {
|
||||
// Reply-link highlight: when hovering a reply badge, dim everything except the linked pair
|
||||
const [replyLinkToolId, setReplyLinkToolId] = useState<string | null>(null);
|
||||
}: DisplayItemRowProps): React.JSX.Element | null {
|
||||
const handleClick = useCallback(() => onItemClick(itemKey), [onItemClick, itemKey]);
|
||||
|
||||
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;
|
||||
|
||||
switch (item.type) {
|
||||
case 'thinking': {
|
||||
itemKey = `thinking-${index}`;
|
||||
const thinkingStep = {
|
||||
id: itemKey,
|
||||
type: 'thinking' as const,
|
||||
|
|
@ -157,8 +158,8 @@ export const DisplayItemList = React.memo(
|
|||
<ThinkingItem
|
||||
step={thinkingStep}
|
||||
preview={truncateText(item.content, previewMaxLength ?? 150)}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
timestamp={item.timestamp}
|
||||
timestampFormat={timestampFormat}
|
||||
titleText={
|
||||
|
|
@ -174,7 +175,6 @@ export const DisplayItemList = React.memo(
|
|||
}
|
||||
|
||||
case 'output': {
|
||||
itemKey = `output-${index}`;
|
||||
const textStep = {
|
||||
id: itemKey,
|
||||
type: 'output' as const,
|
||||
|
|
@ -189,8 +189,8 @@ export const DisplayItemList = React.memo(
|
|||
<TextItem
|
||||
step={textStep}
|
||||
preview={truncateText(item.content, previewMaxLength ?? 150)}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
timestamp={item.timestamp}
|
||||
timestampFormat={timestampFormat}
|
||||
titleText={
|
||||
|
|
@ -206,37 +206,29 @@ export const DisplayItemList = React.memo(
|
|||
}
|
||||
|
||||
case 'tool': {
|
||||
itemKey = `tool-${item.tool.id}-${index}`;
|
||||
element = (
|
||||
<LinkedToolItem
|
||||
linkedTool={item.tool}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
timestamp={item.tool.startTime}
|
||||
timestampFormat={timestampFormat}
|
||||
titleText={
|
||||
showItemMetaTooltip
|
||||
? buildItemMetaTooltip(
|
||||
item.tool.startTime,
|
||||
getToolContextTokens(item.tool),
|
||||
'tokens'
|
||||
)
|
||||
? buildItemMetaTooltip(item.tool.startTime, getToolContextTokens(item.tool), 'tokens')
|
||||
: undefined
|
||||
}
|
||||
searchQueryOverride={searchQueryOverride}
|
||||
isHighlighted={highlightToolUseId === item.tool.id}
|
||||
highlightColor={highlightColor}
|
||||
notificationDotColor={notificationColorMap?.get(item.tool.id)}
|
||||
registerRef={
|
||||
registerToolRef ? (el) => registerToolRef(item.tool.id, el) : undefined
|
||||
}
|
||||
registerRef={registerToolRef ? (el) => registerToolRef(item.tool.id, el) : undefined}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subagent': {
|
||||
itemKey = `subagent-${item.subagent.id}-${index}`;
|
||||
const subagentStep = {
|
||||
id: itemKey,
|
||||
type: 'subagent' as const,
|
||||
|
|
@ -254,8 +246,8 @@ export const DisplayItemList = React.memo(
|
|||
<SubagentItem
|
||||
step={subagentStep}
|
||||
subagent={item.subagent}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
aiGroupId={aiGroupId}
|
||||
highlightToolUseId={highlightToolUseId}
|
||||
highlightColor={highlightColor}
|
||||
|
|
@ -267,12 +259,11 @@ export const DisplayItemList = React.memo(
|
|||
}
|
||||
|
||||
case 'slash': {
|
||||
itemKey = `slash-${item.slash.name}-${index}`;
|
||||
element = (
|
||||
<SlashItem
|
||||
slash={item.slash}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
timestamp={item.slash.timestamp}
|
||||
timestampFormat={timestampFormat}
|
||||
titleText={
|
||||
|
|
@ -290,20 +281,18 @@ export const DisplayItemList = React.memo(
|
|||
}
|
||||
|
||||
case 'teammate_message': {
|
||||
itemKey = `teammate-${item.teammateMessage.id}-${index}`;
|
||||
element = (
|
||||
<TeammateMessageItem
|
||||
teammateMessage={item.teammateMessage}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onReplyHover={handleReplyHover}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
onReplyHover={onReplyHover}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subagent_input': {
|
||||
itemKey = `input-${index}`;
|
||||
const inputContent = item.content;
|
||||
const inputTokenCount = item.tokenCount;
|
||||
element = (
|
||||
|
|
@ -319,8 +308,8 @@ export const DisplayItemList = React.memo(
|
|||
? buildItemMetaTooltip(item.timestamp, inputTokenCount, 'tokens')
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onItemClick(itemKey)}
|
||||
isExpanded={expandedItemIds.has(itemKey)}
|
||||
onClick={handleClick}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<MarkdownViewer
|
||||
content={inputContent}
|
||||
|
|
@ -334,34 +323,26 @@ export const DisplayItemList = React.memo(
|
|||
}
|
||||
|
||||
case 'compact_boundary': {
|
||||
itemKey = `compact-${index}`;
|
||||
const compactContent = item.content;
|
||||
const compactExpanded = expandedItemIds.has(itemKey);
|
||||
element = (
|
||||
<div>
|
||||
<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"
|
||||
style={{
|
||||
backgroundColor: TOOL_CALL_BG,
|
||||
border: `1px solid ${TOOL_CALL_BORDER}`,
|
||||
}}
|
||||
aria-expanded={compactExpanded}
|
||||
>
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-1.5"
|
||||
style={{ color: TOOL_CALL_TEXT }}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-1.5" style={{ color: TOOL_CALL_TEXT }}>
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className={`transition-transform duration-200 ${compactExpanded ? 'rotate-90' : ''}`}
|
||||
className={`transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
<Layers size={14} />
|
||||
</div>
|
||||
<span
|
||||
className="shrink-0 text-xs font-medium"
|
||||
style={{ color: TOOL_CALL_TEXT }}
|
||||
>
|
||||
<span className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }}>
|
||||
Compacted
|
||||
</span>
|
||||
{item.tokenDelta && (
|
||||
|
|
@ -386,14 +367,11 @@ export const DisplayItemList = React.memo(
|
|||
>
|
||||
Phase {item.phaseNumber}
|
||||
</span>
|
||||
<span
|
||||
className="ml-auto shrink-0 text-[11px]"
|
||||
style={{ color: COLOR_TEXT_MUTED }}
|
||||
>
|
||||
<span className="ml-auto shrink-0 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
|
||||
{format(new Date(item.timestamp), 'h:mm:ss a')}
|
||||
</span>
|
||||
</button>
|
||||
{compactExpanded && compactContent && (
|
||||
{isExpanded && compactContent && (
|
||||
<div
|
||||
className="mt-1 overflow-hidden rounded-lg"
|
||||
style={{
|
||||
|
|
@ -418,22 +396,102 @@ export const DisplayItemList = React.memo(
|
|||
return null;
|
||||
}
|
||||
|
||||
// Apply reply-link spotlight: dim items not in the highlighted pair
|
||||
const isDimmed = replyLinkToolId !== null && !isItemInReplyLink(item);
|
||||
return (
|
||||
<div
|
||||
key={itemKey}
|
||||
style={
|
||||
replyLinkToolId !== null
|
||||
? { opacity: isDimmed ? 0.2 : 1, transition: 'opacity 150ms ease' }
|
||||
: undefined
|
||||
hasReplyLink ? { opacity: isDimmed ? 0.2 : 1, transition: 'opacity 150ms ease' } : undefined
|
||||
}
|
||||
>
|
||||
{element}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
maxHeight = 'max-h-96',
|
||||
className = '',
|
||||
|
|
@ -959,7 +958,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
|
|||
baseDir,
|
||||
teamColorByName: providedTeamColorByName,
|
||||
onTeamClick: providedOnTeamClick,
|
||||
}) => {
|
||||
}) {
|
||||
const [showRaw, setShowRaw] = React.useState(false);
|
||||
const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS);
|
||||
const { isLight } = useTheme();
|
||||
|
|
@ -1063,8 +1062,8 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
|
|||
|
||||
{isTooLarge && (
|
||||
<div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
|
||||
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview
|
||||
to keep the UI responsive.
|
||||
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to
|
||||
keep the UI responsive.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1196,5 +1195,4 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import {
|
|||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog';
|
||||
import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow';
|
||||
import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge';
|
||||
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
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';
|
||||
|
||||
|
|
|
|||
|
|
@ -173,14 +173,13 @@ function applyProjectFilter(tasks: GlobalTask[], projectPath: string | null): Gl
|
|||
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
|
||||
}
|
||||
|
||||
export const GlobalTaskList = memo(
|
||||
({
|
||||
export const GlobalTaskList = memo(function GlobalTaskList({
|
||||
hideHeader = false,
|
||||
filters: externalFilters,
|
||||
onFiltersChange: externalOnFiltersChange,
|
||||
filtersPopoverOpen: externalFiltersPopoverOpen,
|
||||
onFiltersPopoverOpenChange: externalOnFiltersPopoverOpenChange,
|
||||
}: GlobalTaskListProps = {}): React.JSX.Element => {
|
||||
}: GlobalTaskListProps = {}): React.JSX.Element {
|
||||
const {
|
||||
globalTasks,
|
||||
globalTasksLoading,
|
||||
|
|
@ -210,8 +209,7 @@ export const GlobalTaskList = memo(
|
|||
const filters = externalFilters ?? internalFilters;
|
||||
const setFilters = externalOnFiltersChange ?? setInternalFilters;
|
||||
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
|
||||
const setFiltersPopoverOpen =
|
||||
externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
||||
const setFiltersPopoverOpen = externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
|
||||
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
|
||||
|
|
@ -273,16 +271,20 @@ export const GlobalTaskList = memo(
|
|||
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);
|
||||
setRenamingTaskKey(null);
|
||||
};
|
||||
},
|
||||
[taskLocalState]
|
||||
);
|
||||
|
||||
const handleRenameCancel = (): void => {
|
||||
const handleRenameCancel = useCallback((): void => {
|
||||
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({
|
||||
title: 'Delete task',
|
||||
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
|
||||
// duplicate IPC calls when the centralized init chain is already fetching.
|
||||
|
|
@ -418,10 +422,7 @@ export const GlobalTaskList = memo(
|
|||
);
|
||||
const projectVisibleCountByKey = useMemo(
|
||||
() =>
|
||||
syncProjectGroupVisibleCountByKey(
|
||||
projectRequestedVisibleCountByKey,
|
||||
projectGroupVisibility
|
||||
),
|
||||
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
|
||||
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
||||
);
|
||||
|
||||
|
|
@ -578,9 +579,7 @@ export const GlobalTaskList = memo(
|
|||
onClick={() => setGroupingMode(mode)}
|
||||
className={cn(
|
||||
'rounded px-1.5 py-0.5 transition-colors',
|
||||
groupingMode === mode
|
||||
? 'text-text'
|
||||
: 'text-text-muted hover:text-text-secondary'
|
||||
groupingMode === mode ? 'text-text' : 'text-text-muted hover:text-text-secondary'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
|
|
@ -857,5 +856,4 @@ export const GlobalTaskList = memo(
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,15 +155,14 @@ const SessionRuntimeBadge = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const SessionItem = memo(
|
||||
({
|
||||
export const SessionItem = memo(function SessionItem({
|
||||
session,
|
||||
isActive,
|
||||
isPinned,
|
||||
isHidden,
|
||||
multiSelectActive,
|
||||
isSelected,
|
||||
}: Readonly<SessionItemProps>): React.JSX.Element => {
|
||||
}: Readonly<SessionItemProps>): React.JSX.Element {
|
||||
const {
|
||||
openTab,
|
||||
activeProjectId,
|
||||
|
|
@ -346,9 +345,7 @@ export const SessionItem = memo(
|
|||
{session.messageCount}
|
||||
</span>
|
||||
<span style={{ opacity: 0.5 }}>·</span>
|
||||
<span className="tabular-nums">
|
||||
{formatShortTime(new Date(session.createdAt))}
|
||||
</span>
|
||||
<span className="tabular-nums">{formatShortTime(new Date(session.createdAt))}</span>
|
||||
{session.model && (
|
||||
<>
|
||||
<span style={{ opacity: 0.5 }}>·</span>
|
||||
|
|
@ -393,5 +390,4 @@ export const SessionItem = memo(
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,8 +69,7 @@ interface SidebarTaskItemProps {
|
|||
getDisplaySubject?: (task: GlobalTask) => string | undefined;
|
||||
}
|
||||
|
||||
export const SidebarTaskItem = memo(
|
||||
({
|
||||
export const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||
task,
|
||||
hideTeamName,
|
||||
showTeamName,
|
||||
|
|
@ -78,7 +77,7 @@ export const SidebarTaskItem = memo(
|
|||
onRenameComplete,
|
||||
onRenameCancel,
|
||||
getDisplaySubject,
|
||||
}: SidebarTaskItemProps): React.JSX.Element => {
|
||||
}: SidebarTaskItemProps): React.JSX.Element {
|
||||
const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail);
|
||||
const teamMembers = useStore(useShallow((s) => s.teamByName[task.teamName]?.members));
|
||||
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
|
||||
|
|
@ -284,5 +283,4 @@ export const SidebarTaskItem = memo(
|
|||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ import { useShallow } from 'zustand/react/shallow';
|
|||
|
||||
import { AddMemberDialog } from './dialogs/AddMemberDialog';
|
||||
import { EditTeamDialog } from './dialogs/EditTeamDialog';
|
||||
import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
|
||||
import { ReviewDialog } from './dialogs/ReviewDialog';
|
||||
import { executeTeamRelaunch } from './dialogs/teamRelaunchFlow';
|
||||
import { KanbanBoard } from './kanban/KanbanBoard';
|
||||
|
|
@ -90,6 +89,7 @@ import { MemberDetailDialog } from './members/MemberDetailDialog';
|
|||
import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes';
|
||||
|
||||
import type { AddMemberEntry } from './dialogs/AddMemberDialog';
|
||||
import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
|
||||
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
||||
import type { ComponentProps, CSSProperties } from 'react';
|
||||
|
||||
|
|
@ -958,8 +958,10 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
|||
);
|
||||
});
|
||||
|
||||
export const TeamDetailView = memo(
|
||||
({ teamName, isPaneFocused = false }: TeamDetailViewProps): React.JSX.Element => {
|
||||
export const TeamDetailView = memo(function TeamDetailView({
|
||||
teamName,
|
||||
isPaneFocused = false,
|
||||
}: TeamDetailViewProps): React.JSX.Element {
|
||||
const { isLight } = useTheme();
|
||||
const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
|
||||
|
|
@ -968,8 +970,8 @@ export const TeamDetailView = memo(
|
|||
initialTab?: MemberDetailTab;
|
||||
initialActivityFilter?: MemberActivityFilter;
|
||||
} | null>(null);
|
||||
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(
|
||||
() => getTeamPendingRepliesState(teamName)
|
||||
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(() =>
|
||||
getTeamPendingRepliesState(teamName)
|
||||
);
|
||||
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
|
||||
open: false,
|
||||
|
|
@ -1201,9 +1203,7 @@ export const TeamDetailView = memo(
|
|||
const [stoppingTeam, setStoppingTeam] = useState(false);
|
||||
const [trashOpen, setTrashOpen] = useState(false);
|
||||
const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined);
|
||||
const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(undefined);
|
||||
const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
|
@ -1364,8 +1364,7 @@ export const TeamDetailView = memo(
|
|||
maxWidth: 600,
|
||||
side: 'left',
|
||||
});
|
||||
const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } =
|
||||
useResizablePanel({
|
||||
const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } = useResizablePanel({
|
||||
height: sidebarLogsHeight,
|
||||
onHeightChange: setSidebarLogsHeight,
|
||||
minHeight: 120,
|
||||
|
|
@ -1715,17 +1714,11 @@ export const TeamDetailView = memo(
|
|||
}, [activeMembers, data?.config.members, launchParams?.providerId]);
|
||||
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
|
||||
|
||||
const taskMap = useMemo(
|
||||
() => new Map((data?.tasks ?? []).map((t) => [t.id, t])),
|
||||
[data?.tasks]
|
||||
);
|
||||
const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]);
|
||||
const taskMapRef = useRef(taskMap);
|
||||
taskMapRef.current = taskMap;
|
||||
|
||||
const memberTaskCounts = useMemo(
|
||||
() => buildTaskCountsByOwner(data?.tasks ?? []),
|
||||
[data?.tasks]
|
||||
);
|
||||
const memberTaskCounts = useMemo(() => buildTaskCountsByOwner(data?.tasks ?? []), [data?.tasks]);
|
||||
|
||||
const openCreateTaskDialog = useCallback(
|
||||
(subject = '', description = '', owner = '', startImmediately?: boolean): void => {
|
||||
|
|
@ -2029,13 +2022,7 @@ export const TeamDetailView = memo(
|
|||
startImmediately,
|
||||
});
|
||||
|
||||
if (
|
||||
prompt &&
|
||||
owner &&
|
||||
data?.isAlive &&
|
||||
!isTeamProvisioning &&
|
||||
startImmediately !== false
|
||||
) {
|
||||
if (prompt && owner && data?.isAlive && !isTeamProvisioning && startImmediately !== false) {
|
||||
const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`;
|
||||
try {
|
||||
await api.teams.processSend(teamName, msg);
|
||||
|
|
@ -2187,6 +2174,7 @@ export const TeamDetailView = memo(
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{launchDialogOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
|
|
@ -2201,6 +2189,7 @@ export const TeamDetailView = memo(
|
|||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
</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">
|
||||
{data.config.projectPath && (
|
||||
<span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]">
|
||||
<FolderOpen
|
||||
size={11}
|
||||
className="shrink-0 text-[var(--color-text-muted)]"
|
||||
/>
|
||||
<FolderOpen size={11} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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)]"
|
||||
title={leadBranch}
|
||||
>
|
||||
<GitBranch
|
||||
size={11}
|
||||
className="shrink-0 text-[var(--color-text-muted)]"
|
||||
/>
|
||||
<GitBranch size={11} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="max-w-32 truncate">{leadBranch}</span>
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -2447,9 +2430,7 @@ export const TeamDetailView = memo(
|
|||
</div>
|
||||
{(() => {
|
||||
const currentPath = data.config.projectPath;
|
||||
const history = data.config.projectPathHistory?.filter(
|
||||
(p) => p !== currentPath
|
||||
);
|
||||
const history = data.config.projectPathHistory?.filter((p) => p !== currentPath);
|
||||
if (!history || history.length === 0) return null;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -2542,9 +2523,7 @@ export const TeamDetailView = memo(
|
|||
sessionsError={sessionsError}
|
||||
leadSessionId={data.config.leadSessionId}
|
||||
selectedSessionId={kanbanFilter.sessionId}
|
||||
onSelectSession={(id) =>
|
||||
setKanbanFilter((prev) => ({ ...prev, sessionId: id }))
|
||||
}
|
||||
onSelectSession={(id) => setKanbanFilter((prev) => ({ ...prev, sessionId: id }))}
|
||||
projectPath={data.config.projectPath}
|
||||
/>
|
||||
</CollapsibleTeamSection>
|
||||
|
|
@ -2865,6 +2844,7 @@ export const TeamDetailView = memo(
|
|||
}}
|
||||
/>
|
||||
|
||||
{createTaskDialog.open && (
|
||||
<Suspense fallback={null}>
|
||||
<CreateTaskDialog
|
||||
open={createTaskDialog.open}
|
||||
|
|
@ -2882,6 +2862,7 @@ export const TeamDetailView = memo(
|
|||
submitting={creatingTask}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<EditTeamDialog
|
||||
open={editDialogOpen}
|
||||
|
|
@ -2948,11 +2929,7 @@ export const TeamDetailView = memo(
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setRemoveMemberConfirm(null)}
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={() => setRemoveMemberConfirm(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
|
|
@ -2976,8 +2953,8 @@ export const TeamDetailView = memo(
|
|||
<DialogHeader>
|
||||
<DialogTitle>Delete team</DialogTitle>
|
||||
<DialogDescription>
|
||||
Delete team “{data.config.name}”? This action is irreversible.
|
||||
All team data and tasks will be deleted.
|
||||
Delete team “{data.config.name}”? This action is irreversible. All
|
||||
team data and tasks will be deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
|
@ -2991,6 +2968,7 @@ export const TeamDetailView = memo(
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{launchDialogOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
|
|
@ -3006,7 +2984,9 @@ export const TeamDetailView = memo(
|
|||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{sendDialogOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<SendMessageDialog
|
||||
open={sendDialogOpen}
|
||||
|
|
@ -3064,7 +3044,9 @@ export const TeamDetailView = memo(
|
|||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{selectedTask !== null && (
|
||||
<Suspense fallback={null}>
|
||||
<TaskDetailDialog
|
||||
open={selectedTask !== null}
|
||||
|
|
@ -3108,6 +3090,7 @@ export const TeamDetailView = memo(
|
|||
onDeleteTask={handleDeleteTask}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<TrashDialog
|
||||
open={trashOpen}
|
||||
|
|
@ -3124,6 +3107,7 @@ export const TeamDetailView = memo(
|
|||
}}
|
||||
/>
|
||||
|
||||
{reviewDialogState.open && (
|
||||
<Suspense fallback={null}>
|
||||
<ChangeReviewDialog
|
||||
open={reviewDialogState.open}
|
||||
|
|
@ -3146,6 +3130,7 @@ export const TeamDetailView = memo(
|
|||
onEditorAction={handleEditorAction}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={setMessagesPanelMountPoint}
|
||||
|
|
@ -3213,5 +3198,4 @@ export const TeamDetailView = memo(
|
|||
{renderBody()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
|
||||
import { TeamEmptyState } from './TeamEmptyState';
|
||||
import { EMPTY_TEAM_FILTER, TeamListFilterPopover } from './TeamListFilterPopover';
|
||||
import {
|
||||
|
|
@ -54,13 +53,7 @@ import {
|
|||
teamMatchesProjectSelection,
|
||||
} from './teamProjectSelection';
|
||||
|
||||
const CreateTeamDialog = lazy(() =>
|
||||
import('./dialogs/CreateTeamDialog').then((m) => ({ default: m.CreateTeamDialog }))
|
||||
);
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
|
||||
);
|
||||
|
||||
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
|
||||
import type { TeamListFilterState } from './TeamListFilterPopover';
|
||||
import type { TeamStatus } from '@renderer/utils/teamListStatus';
|
||||
import type {
|
||||
|
|
@ -72,6 +65,13 @@ import type {
|
|||
TeamSummaryMember,
|
||||
} 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 {
|
||||
const base = sourceName.replace(/-\d+$/, '');
|
||||
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 electronMode = isElectronMode();
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
|
|
|||
|
|
@ -311,8 +311,7 @@ const SortableKanbanTaskCard = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const KanbanBoard = memo(
|
||||
({
|
||||
export const KanbanBoard = memo(function KanbanBoard({
|
||||
tasks,
|
||||
teamName,
|
||||
kanbanState,
|
||||
|
|
@ -339,7 +338,7 @@ export const KanbanBoard = memo(
|
|||
onDeleteTask,
|
||||
deletedTaskCount,
|
||||
onOpenTrash,
|
||||
}: KanbanBoardProps): React.JSX.Element => {
|
||||
}: KanbanBoardProps): React.JSX.Element {
|
||||
const boardRef = useRef<HTMLDivElement>(null);
|
||||
const scrollRestoreTimeoutsRef = useRef<number[]>([]);
|
||||
const [viewMode, setViewMode] = useState<KanbanViewMode>('grid');
|
||||
|
|
@ -423,11 +422,8 @@ export const KanbanBoard = memo(
|
|||
[onColumnOrderChange, groupedOrdered]
|
||||
);
|
||||
|
||||
const renderCards = (
|
||||
columnId: KanbanColumnId,
|
||||
columnTasks: TeamTask[],
|
||||
compact?: boolean
|
||||
): React.JSX.Element => {
|
||||
const renderCards = useCallback(
|
||||
(columnId: KanbanColumnId, columnTasks: TeamTask[], compact?: boolean): React.JSX.Element => {
|
||||
const addHandler =
|
||||
onAddTask && columnId === 'todo'
|
||||
? () => onAddTask(false)
|
||||
|
|
@ -517,7 +513,28 @@ export const KanbanBoard = memo(
|
|||
{addButton}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
[
|
||||
enableTaskSorting,
|
||||
hasReviewers,
|
||||
kanbanState,
|
||||
memberColorMap,
|
||||
onAddTask,
|
||||
onApprove,
|
||||
onCancelTask,
|
||||
onCompleteTask,
|
||||
onDeleteTask,
|
||||
onMoveBackToDone,
|
||||
onRequestChanges,
|
||||
onRequestReview,
|
||||
onScrollToTask,
|
||||
onStartTask,
|
||||
onTaskClick,
|
||||
onViewChanges,
|
||||
taskMap,
|
||||
teamName,
|
||||
]
|
||||
);
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() => (filter.columns.size > 0 ? COLUMNS.filter((c) => filter.columns.has(c.id)) : COLUMNS),
|
||||
|
|
@ -533,9 +550,7 @@ export const KanbanBoard = memo(
|
|||
const columnModeSearchWidth =
|
||||
primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256;
|
||||
const toolbarLeftWidth =
|
||||
viewMode === 'grid'
|
||||
? (gridPrimaryColumnWidth ?? columnModeSearchWidth)
|
||||
: columnModeSearchWidth;
|
||||
viewMode === 'grid' ? (gridPrimaryColumnWidth ?? columnModeSearchWidth) : columnModeSearchWidth;
|
||||
|
||||
const clearScheduledScrollRestore = useCallback(() => {
|
||||
for (const timeoutId of scrollRestoreTimeoutsRef.current) {
|
||||
|
|
@ -594,6 +609,29 @@ export const KanbanBoard = memo(
|
|||
[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 = (
|
||||
<div ref={boardRef} className="min-w-0 max-w-full overflow-x-hidden">
|
||||
<div
|
||||
|
|
@ -685,30 +723,7 @@ export const KanbanBoard = memo(
|
|||
primaryColumnId={primaryVisibleColumnId}
|
||||
onPrimaryColumnWidthChange={setGridPrimaryColumnWidth}
|
||||
skeletonDelayMs={gridSkeletonDelayMs}
|
||||
columns={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
|
||||
),
|
||||
})),
|
||||
};
|
||||
})}
|
||||
columns={gridColumns}
|
||||
/>
|
||||
) : (
|
||||
<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;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): {
|
|||
};
|
||||
}
|
||||
|
||||
export const MemberCard = memo(
|
||||
({
|
||||
export const MemberCard = memo(function MemberCard({
|
||||
member,
|
||||
memberColor,
|
||||
runtimeSummary,
|
||||
|
|
@ -120,7 +119,7 @@ export const MemberCard = memo(
|
|||
onAssignTask,
|
||||
onRestartMember,
|
||||
onSkipMemberForLaunch,
|
||||
}: MemberCardProps): React.JSX.Element => {
|
||||
}: MemberCardProps): React.JSX.Element {
|
||||
// NOTE: lead context display disabled — usage formula is inaccurate
|
||||
// const teamName = useStore((s) => s.selectedTeamName);
|
||||
// const leadContext = useStore((s) =>
|
||||
|
|
@ -278,9 +277,7 @@ export const MemberCard = memo(
|
|||
const restartActionErrorFallback = canRelaunchOpenCode
|
||||
? 'Failed to relaunch OpenCode teammate'
|
||||
: 'Failed to retry teammate';
|
||||
const handleRestartMember = async (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
): Promise<void> => {
|
||||
const handleRestartMember = async (event: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!onRestartMember || retryingLaunch) {
|
||||
|
|
@ -473,9 +470,7 @@ export const MemberCard = memo(
|
|||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
|
||||
}
|
||||
aria-label={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"
|
||||
disabled={retryingLaunch}
|
||||
onClick={handleRestartMember}
|
||||
|
|
@ -544,9 +539,7 @@ export const MemberCard = memo(
|
|||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
|
||||
}
|
||||
aria-label={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"
|
||||
disabled={retryingLaunch || skippingLaunch}
|
||||
onClick={handleRestartMember}
|
||||
|
|
@ -588,9 +581,7 @@ export const MemberCard = memo(
|
|||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
|
||||
}
|
||||
aria-label={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"
|
||||
disabled={retryingLaunch}
|
||||
onClick={handleRestartMember}
|
||||
|
|
@ -718,5 +709,4 @@ export const MemberCard = memo(
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ import {
|
|||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { ScheduleEmptyState } from './ScheduleEmptyState';
|
||||
import { ScheduleRunLogDialog } from './ScheduleRunLogDialog';
|
||||
import { ScheduleRunRow } from './ScheduleRunRow';
|
||||
import { ScheduleStatusBadge } from './ScheduleStatusBadge';
|
||||
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
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';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface TaskRowProps {
|
|||
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 blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
|
||||
const kanbanColumn = getTaskKanbanColumn(task);
|
||||
|
|
|
|||
Loading…
Reference in a new issue