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:
catlog22
2026-02-14 12:54:08 +08:00
parent cdb240d2c2
commit 4d22ae4b2f
56 changed files with 4767 additions and 425 deletions

View File

@@ -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) => {

View File

@@ -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>
);