mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
Add orchestrator types and error handling configurations
- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`. - Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management. - Added a new PNG image file for visual representation. - Created a placeholder file named 'nul' for future use.
This commit is contained in:
@@ -79,41 +79,65 @@ function FixProgressCarousel({ sessionId }: { sessionId: string }) {
|
||||
const [currentSlide, setCurrentSlide] = React.useState(0);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
// Fetch fix progress data
|
||||
const fetchFixProgress = React.useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/fix-progress?sessionId=${encodeURIComponent(sessionId)}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
setFixProgressData(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
setFixProgressData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch fix progress:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
// Poll for fix progress updates
|
||||
// Sequential polling with AbortController — no concurrent requests possible
|
||||
React.useEffect(() => {
|
||||
fetchFixProgress();
|
||||
const abortController = new AbortController();
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let stopped = false;
|
||||
let errorCount = 0;
|
||||
|
||||
// Stop polling if phase is completion
|
||||
if (fixProgressData?.phase === 'completion') {
|
||||
return;
|
||||
}
|
||||
const poll = async () => {
|
||||
if (stopped) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchFixProgress();
|
||||
}, 5000);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/fix-progress?sessionId=${encodeURIComponent(sessionId)}`,
|
||||
{ signal: abortController.signal }
|
||||
);
|
||||
if (!response.ok) {
|
||||
errorCount += 1;
|
||||
if (response.status === 404 || errorCount >= 3) {
|
||||
stopped = true;
|
||||
setFixProgressData(null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
errorCount = 0;
|
||||
const data = await response.json();
|
||||
setFixProgressData(data);
|
||||
if (data?.phase === 'completion') {
|
||||
stopped = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (abortController.signal.aborted) return;
|
||||
errorCount += 1;
|
||||
if (errorCount >= 3) {
|
||||
stopped = true;
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (!abortController.signal.aborted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchFixProgress, fixProgressData?.phase]);
|
||||
// Schedule next poll only after current request completes
|
||||
if (!stopped) {
|
||||
timeoutId = setTimeout(poll, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
abortController.abort();
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
// Navigate carousel
|
||||
const navigateSlide = (direction: 'prev' | 'next' | number) => {
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
// ========================================
|
||||
// TeamPage
|
||||
// ========================================
|
||||
// Main page for team execution visualization
|
||||
// Main page for team execution - list/detail dual view with tabbed detail
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Users } from 'lucide-react';
|
||||
import { Package, MessageSquare } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||
import { useTeamStore } from '@/stores/teamStore';
|
||||
import { useTeams, useTeamMessages, useTeamStatus } from '@/hooks/useTeamData';
|
||||
import { TeamEmptyState } from '@/components/team/TeamEmptyState';
|
||||
import type { TeamDetailTab } from '@/stores/teamStore';
|
||||
import { useTeamMessages, useTeamStatus } from '@/hooks/useTeamData';
|
||||
import { TeamHeader } from '@/components/team/TeamHeader';
|
||||
import { TeamPipeline } from '@/components/team/TeamPipeline';
|
||||
import { TeamMembersPanel } from '@/components/team/TeamMembersPanel';
|
||||
import { TeamMessageFeed } from '@/components/team/TeamMessageFeed';
|
||||
import { TeamArtifacts } from '@/components/team/TeamArtifacts';
|
||||
import { TeamListView } from '@/components/team/TeamListView';
|
||||
|
||||
export function TeamPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
viewMode,
|
||||
autoRefresh,
|
||||
toggleAutoRefresh,
|
||||
messageFilter,
|
||||
@@ -27,89 +29,92 @@ export function TeamPage() {
|
||||
clearMessageFilter,
|
||||
timelineExpanded,
|
||||
setTimelineExpanded,
|
||||
detailTab,
|
||||
setDetailTab,
|
||||
backToList,
|
||||
} = useTeamStore();
|
||||
|
||||
// Data hooks
|
||||
const { teams, isLoading: teamsLoading } = useTeams();
|
||||
// Data hooks (only active in detail mode)
|
||||
const { messages, total: messageTotal } = useTeamMessages(
|
||||
selectedTeam,
|
||||
viewMode === 'detail' ? selectedTeam : null,
|
||||
messageFilter
|
||||
);
|
||||
const { members, totalMessages } = useTeamStatus(selectedTeam);
|
||||
const { members, totalMessages } = useTeamStatus(
|
||||
viewMode === 'detail' ? selectedTeam : null
|
||||
);
|
||||
|
||||
// Auto-select first team if none selected
|
||||
useEffect(() => {
|
||||
if (!selectedTeam && teams.length > 0) {
|
||||
setSelectedTeam(teams[0].name);
|
||||
}
|
||||
}, [selectedTeam, teams, setSelectedTeam]);
|
||||
|
||||
// Show empty state when no teams exist
|
||||
if (!teamsLoading && teams.length === 0) {
|
||||
// List view
|
||||
if (viewMode === 'list' || !selectedTeam) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Users className="w-5 h-5" />
|
||||
<h1 className="text-xl font-semibold">{formatMessage({ id: 'team.title' })}</h1>
|
||||
</div>
|
||||
<TeamEmptyState />
|
||||
<TeamListView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
value: 'artifacts',
|
||||
label: formatMessage({ id: 'team.tabs.artifacts' }),
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'messages',
|
||||
label: formatMessage({ id: 'team.tabs.messages' }),
|
||||
icon: <MessageSquare className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
// Detail view
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
<h1 className="text-xl font-semibold">{formatMessage({ id: 'team.title' })}</h1>
|
||||
</div>
|
||||
|
||||
{/* Team Header: selector + stats + controls */}
|
||||
{/* Detail Header: back button + team name + stats + controls */}
|
||||
<TeamHeader
|
||||
teams={teams}
|
||||
selectedTeam={selectedTeam}
|
||||
onSelectTeam={setSelectedTeam}
|
||||
onBack={backToList}
|
||||
members={members}
|
||||
totalMessages={totalMessages}
|
||||
autoRefresh={autoRefresh}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
/>
|
||||
|
||||
{selectedTeam ? (
|
||||
<>
|
||||
{/* Main content grid: Pipeline (left) + Members (right) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Pipeline visualization */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardContent className="p-4">
|
||||
<TeamPipeline messages={messages} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Overview: Pipeline + Members (always visible) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2 flex flex-col">
|
||||
<CardContent className="p-4 flex-1">
|
||||
<TeamPipeline messages={messages} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<TeamMembersPanel members={members} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Members panel */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<TeamMembersPanel members={members} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Tab Navigation: Artifacts / Messages */}
|
||||
<TabsNavigation
|
||||
value={detailTab}
|
||||
onValueChange={(v) => setDetailTab(v as TeamDetailTab)}
|
||||
tabs={tabs}
|
||||
/>
|
||||
|
||||
{/* Message timeline */}
|
||||
<TeamMessageFeed
|
||||
messages={messages}
|
||||
total={messageTotal}
|
||||
filter={messageFilter}
|
||||
onFilterChange={setMessageFilter}
|
||||
onClearFilter={clearMessageFilter}
|
||||
expanded={timelineExpanded}
|
||||
onExpandedChange={setTimelineExpanded}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
{formatMessage({ id: 'team.noTeamSelected' })}
|
||||
</div>
|
||||
{/* Artifacts Tab */}
|
||||
{detailTab === 'artifacts' && (
|
||||
<TeamArtifacts messages={messages} />
|
||||
)}
|
||||
|
||||
{/* Messages Tab */}
|
||||
{detailTab === 'messages' && (
|
||||
<TeamMessageFeed
|
||||
messages={messages}
|
||||
total={messageTotal}
|
||||
filter={messageFilter}
|
||||
onFilterChange={setMessageFilter}
|
||||
onClearFilter={clearMessageFilter}
|
||||
expanded={timelineExpanded}
|
||||
onExpandedChange={setTimelineExpanded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user