agent-ecosystem/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx
iliya 1ccc1432fc feat: enhance task change handling and improve plugin catalog integration
- Added a 'source' field to PluginCatalogItem to distinguish between official and third-party plugins.
- Refactored ChangeExtractorService to improve caching mechanisms and normalize file paths for better consistency.
- Updated TaskBoundaryParser to support task IDs with underscores, enhancing task identification.
- Enhanced TeamMcpConfigBuilder to merge user-defined MCP configurations with generated ones, improving configuration management.
- Improved UI components to display source information for plugins and MCP servers, enhancing user experience and clarity.
2026-03-11 18:16:40 +02:00

148 lines
4.9 KiB
TypeScript

import { useCallback, useEffect, useMemo } from 'react';
import { useStore } from '@renderer/store';
import { buildTaskChangeRequestOptions } from '@renderer/utils/taskChangeRequest';
import { ExternalLink } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { TaskDetailDialog } from './TaskDetailDialog';
import type { GlobalTask, TeamTaskWithKanban } from '@shared/types';
/**
* Global wrapper around TaskDetailDialog.
* Mounted at layout level so it can be opened from anywhere (e.g. sidebar)
* without navigating to the team page first.
*/
export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
const {
globalTaskDetail,
closeGlobalTaskDetail,
selectedTeamName,
selectedTeamData,
selectedTeamLoading,
selectTeam,
openTeamTab,
setPendingReviewRequest,
globalTasks,
} = useStore(
useShallow((s) => ({
globalTaskDetail: s.globalTaskDetail,
closeGlobalTaskDetail: s.closeGlobalTaskDetail,
selectedTeamName: s.selectedTeamName,
selectedTeamData: s.selectedTeamData,
selectedTeamLoading: s.selectedTeamLoading,
selectTeam: s.selectTeam,
openTeamTab: s.openTeamTab,
setPendingReviewRequest: s.setPendingReviewRequest,
globalTasks: s.globalTasks,
}))
);
const teamName = globalTaskDetail?.teamName ?? '';
const taskId = globalTaskDetail?.taskId ?? '';
// Load full team data in the background to enable "as before" details (logs/changes/members).
useEffect(() => {
if (!globalTaskDetail) return;
if (!teamName) return;
// Avoid re-triggering selectTeam in a loop while the fetch is in flight.
// selectedTeamName is set immediately by selectTeam(), but selectedTeamData
// remains null until IPC resolves.
if (selectedTeamName === teamName) {
if (selectedTeamData || selectedTeamLoading) return;
// Retry once if we are on the right team but have no data and not loading (e.g. prior error).
}
void selectTeam(teamName, { skipProjectAutoSelect: true });
}, [
globalTaskDetail,
selectTeam,
selectedTeamData,
selectedTeamLoading,
selectedTeamName,
teamName,
]);
const isFullTeamLoaded = selectedTeamName === teamName && !!selectedTeamData;
// Team data is still loading when:
// - selectTeam() hasn't updated selectedTeamName yet (team switch pending)
// - selectedTeamName matches but IPC fetch is still in flight
const isThisTeamLoading =
selectedTeamName !== teamName ||
(selectedTeamName === teamName && selectedTeamLoading && !selectedTeamData);
const taskMap = useMemo(() => {
const map = new Map<string, TeamTaskWithKanban>();
if (!globalTaskDetail) return map;
if (isFullTeamLoaded && selectedTeamData) {
for (const t of selectedTeamData.tasks) map.set(t.id, t);
return map;
}
for (const t of globalTasks) {
if (t.teamName === globalTaskDetail.teamName) {
map.set(t.id, t);
}
}
return map;
}, [globalTaskDetail, globalTasks, isFullTeamLoaded, selectedTeamData]);
const activeMembers = useMemo(
() => (isFullTeamLoaded ? (selectedTeamData?.members.filter((m) => !m.removedAt) ?? []) : []),
[isFullTeamLoaded, selectedTeamData]
);
const handleOpenTeam = useCallback((): void => {
closeGlobalTaskDetail();
openTeamTab(teamName, undefined, taskId);
}, [closeGlobalTaskDetail, openTeamTab, teamName, taskId]);
const handleViewChanges = useCallback(
(viewTaskId: string, filePath?: string) => {
const targetTask = taskMap.get(viewTaskId);
if (!targetTask) return;
setPendingReviewRequest({
taskId: viewTaskId,
filePath,
requestOptions: buildTaskChangeRequestOptions(targetTask),
});
closeGlobalTaskDetail();
openTeamTab(teamName);
},
[closeGlobalTaskDetail, openTeamTab, setPendingReviewRequest, taskMap, teamName]
);
if (!globalTaskDetail) return null;
const task = (taskMap.get(taskId) as GlobalTask | undefined) ?? null;
const kanbanTaskState = isFullTeamLoaded
? selectedTeamData?.kanbanState.tasks[taskId]
: undefined;
return (
<TaskDetailDialog
open
variant={isFullTeamLoaded ? 'team' : 'global'}
loading={!isFullTeamLoaded && isThisTeamLoading}
task={task}
teamName={teamName}
kanbanTaskState={kanbanTaskState}
taskMap={taskMap}
members={activeMembers}
onClose={closeGlobalTaskDetail}
onOwnerChange={undefined}
onViewChanges={isFullTeamLoaded ? handleViewChanges : undefined}
headerExtra={
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border)] px-3 py-1.5 text-xs text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={handleOpenTeam}
>
<ExternalLink size={12} />
Open team
</button>
}
/>
);
};