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 {
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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 “{data.config.name}”? This action is irreversible.
|
Delete team “{data.config.name}”? 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()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue