Skip to content

Commit ad5b35a

Browse files
author
catlog22
committed
feat: unify queue execution handling with QueueItemExecutor and CLI execution settings
- Removed ad-hoc test scripts and temp files from project root - Added QueueItemExecutor component to handle both session and orchestrator executions - Created CliExecutionSettings component for shared execution parameter controls - Introduced useCliSessionCore hook for managing CLI session lifecycle - Merged buildQueueItemPrompt and buildQueueItemInstruction into a single function for context building - Implemented Zustand store for queue execution state management - Updated localization files for new execution features
1 parent af90069 commit ad5b35a

17 files changed

Lines changed: 1466 additions & 36 deletions

.claude/settings.json

Lines changed: 0 additions & 22 deletions
This file was deleted.

assets/wechat-group-qr.png

245 Bytes
Loading
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
// ========================================
2+
// Execution Panel
3+
// ========================================
4+
// Content panel for Executions tab in IssueHub.
5+
// Shows queue execution state from queueExecutionStore
6+
// with split-view: execution list (left) + detail view (right).
7+
8+
import { useState, useMemo } from 'react';
9+
import { useIntl } from 'react-intl';
10+
import {
11+
Play,
12+
CheckCircle,
13+
XCircle,
14+
Clock,
15+
Terminal,
16+
Loader2,
17+
} from 'lucide-react';
18+
import { Card } from '@/components/ui/Card';
19+
import { Badge } from '@/components/ui/Badge';
20+
import { Button } from '@/components/ui/Button';
21+
import {
22+
useQueueExecutionStore,
23+
selectExecutionStats,
24+
useTerminalPanelStore,
25+
} from '@/stores';
26+
import type { QueueExecution, QueueExecutionStatus } from '@/stores/queueExecutionStore';
27+
import { cn } from '@/lib/utils';
28+
29+
// ========== Helpers ==========
30+
31+
function statusBadgeVariant(status: QueueExecutionStatus): 'info' | 'success' | 'destructive' | 'secondary' {
32+
switch (status) {
33+
case 'running':
34+
return 'info';
35+
case 'completed':
36+
return 'success';
37+
case 'failed':
38+
return 'destructive';
39+
case 'pending':
40+
default:
41+
return 'secondary';
42+
}
43+
}
44+
45+
function statusIcon(status: QueueExecutionStatus) {
46+
switch (status) {
47+
case 'running':
48+
return <Loader2 className="w-3.5 h-3.5 animate-spin" />;
49+
case 'completed':
50+
return <CheckCircle className="w-3.5 h-3.5" />;
51+
case 'failed':
52+
return <XCircle className="w-3.5 h-3.5" />;
53+
case 'pending':
54+
default:
55+
return <Clock className="w-3.5 h-3.5" />;
56+
}
57+
}
58+
59+
function formatRelativeTime(isoString: string): string {
60+
const diff = Date.now() - new Date(isoString).getTime();
61+
const seconds = Math.floor(diff / 1000);
62+
if (seconds < 60) return `${seconds}s ago`;
63+
const minutes = Math.floor(seconds / 60);
64+
if (minutes < 60) return `${minutes}m ago`;
65+
const hours = Math.floor(minutes / 60);
66+
if (hours < 24) return `${hours}h ago`;
67+
const days = Math.floor(hours / 24);
68+
return `${days}d ago`;
69+
}
70+
71+
// ========== Empty State ==========
72+
73+
function ExecutionEmptyState() {
74+
const { formatMessage } = useIntl();
75+
76+
return (
77+
<Card className="p-12 text-center">
78+
<Terminal className="w-16 h-16 mx-auto text-muted-foreground/50" />
79+
<h3 className="mt-4 text-lg font-medium text-foreground">
80+
{formatMessage({ id: 'issues.executions.emptyState.title' })}
81+
</h3>
82+
<p className="mt-2 text-muted-foreground">
83+
{formatMessage({ id: 'issues.executions.emptyState.description' })}
84+
</p>
85+
</Card>
86+
);
87+
}
88+
89+
// ========== Stats Cards ==========
90+
91+
function ExecutionStatsCards() {
92+
const { formatMessage } = useIntl();
93+
const stats = useQueueExecutionStore(selectExecutionStats);
94+
95+
return (
96+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
97+
<Card className="p-4">
98+
<div className="flex items-center gap-2">
99+
<Play className="w-5 h-5 text-info" />
100+
<span className="text-2xl font-bold">{stats.running}</span>
101+
</div>
102+
<p className="text-sm text-muted-foreground mt-1">
103+
{formatMessage({ id: 'issues.executions.stats.running' })}
104+
</p>
105+
</Card>
106+
<Card className="p-4">
107+
<div className="flex items-center gap-2">
108+
<CheckCircle className="w-5 h-5 text-success" />
109+
<span className="text-2xl font-bold">{stats.completed}</span>
110+
</div>
111+
<p className="text-sm text-muted-foreground mt-1">
112+
{formatMessage({ id: 'issues.executions.stats.completed' })}
113+
</p>
114+
</Card>
115+
<Card className="p-4">
116+
<div className="flex items-center gap-2">
117+
<XCircle className="w-5 h-5 text-destructive" />
118+
<span className="text-2xl font-bold">{stats.failed}</span>
119+
</div>
120+
<p className="text-sm text-muted-foreground mt-1">
121+
{formatMessage({ id: 'issues.executions.stats.failed' })}
122+
</p>
123+
</Card>
124+
<Card className="p-4">
125+
<div className="flex items-center gap-2">
126+
<Terminal className="w-5 h-5 text-muted-foreground" />
127+
<span className="text-2xl font-bold">{stats.total}</span>
128+
</div>
129+
<p className="text-sm text-muted-foreground mt-1">
130+
{formatMessage({ id: 'issues.executions.stats.total' })}
131+
</p>
132+
</Card>
133+
</div>
134+
);
135+
}
136+
137+
// ========== Execution List Item ==========
138+
139+
function ExecutionListItem({
140+
execution,
141+
isSelected,
142+
onSelect,
143+
}: {
144+
execution: QueueExecution;
145+
isSelected: boolean;
146+
onSelect: () => void;
147+
}) {
148+
return (
149+
<button
150+
type="button"
151+
className={cn(
152+
'w-full text-left p-3 rounded-md transition-colors',
153+
'hover:bg-muted/60',
154+
isSelected && 'bg-muted ring-1 ring-primary/30'
155+
)}
156+
onClick={onSelect}
157+
>
158+
<div className="flex items-center justify-between gap-2">
159+
<div className="flex items-center gap-2 min-w-0">
160+
{statusIcon(execution.status)}
161+
<span className="text-sm font-medium text-foreground truncate">
162+
{execution.id}
163+
</span>
164+
</div>
165+
<Badge variant={statusBadgeVariant(execution.status)} className="gap-1 shrink-0">
166+
{execution.status}
167+
</Badge>
168+
</div>
169+
<div className="mt-1.5 flex items-center gap-3 text-xs text-muted-foreground">
170+
<span className="font-mono">{execution.tool}</span>
171+
<span>{execution.mode}</span>
172+
<span>{execution.type}</span>
173+
<span className="ml-auto">{formatRelativeTime(execution.startedAt)}</span>
174+
</div>
175+
</button>
176+
);
177+
}
178+
179+
// ========== Execution Detail View ==========
180+
181+
function ExecutionDetailView({ execution }: { execution: QueueExecution | null }) {
182+
const { formatMessage } = useIntl();
183+
const openTerminal = useTerminalPanelStore((s) => s.openTerminal);
184+
185+
if (!execution) {
186+
return (
187+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
188+
{formatMessage({ id: 'issues.executions.detail.selectExecution' })}
189+
</div>
190+
);
191+
}
192+
193+
const detailRows: Array<{ label: string; value: string | undefined }> = [
194+
{ label: formatMessage({ id: 'issues.executions.detail.id' }), value: execution.id },
195+
{ label: formatMessage({ id: 'issues.executions.detail.queueItemId' }), value: execution.queueItemId },
196+
{ label: formatMessage({ id: 'issues.executions.detail.issueId' }), value: execution.issueId },
197+
{ label: formatMessage({ id: 'issues.executions.detail.solutionId' }), value: execution.solutionId },
198+
{ label: formatMessage({ id: 'issues.executions.detail.type' }), value: execution.type },
199+
{ label: formatMessage({ id: 'issues.executions.detail.tool' }), value: execution.tool },
200+
{ label: formatMessage({ id: 'issues.executions.detail.mode' }), value: execution.mode },
201+
{ label: formatMessage({ id: 'issues.executions.detail.status' }), value: execution.status },
202+
{ label: formatMessage({ id: 'issues.executions.detail.startedAt' }), value: execution.startedAt },
203+
{ label: formatMessage({ id: 'issues.executions.detail.completedAt' }), value: execution.completedAt || '-' },
204+
];
205+
206+
if (execution.sessionKey) {
207+
detailRows.push({
208+
label: formatMessage({ id: 'issues.executions.detail.sessionKey' }),
209+
value: execution.sessionKey,
210+
});
211+
}
212+
if (execution.flowId) {
213+
detailRows.push({
214+
label: formatMessage({ id: 'issues.executions.detail.flowId' }),
215+
value: execution.flowId,
216+
});
217+
}
218+
if (execution.execId) {
219+
detailRows.push({
220+
label: formatMessage({ id: 'issues.executions.detail.execId' }),
221+
value: execution.execId,
222+
});
223+
}
224+
225+
return (
226+
<div className="space-y-4">
227+
{/* Header */}
228+
<div className="flex items-center justify-between">
229+
<div className="flex items-center gap-2">
230+
{statusIcon(execution.status)}
231+
<span className="text-sm font-semibold text-foreground">{execution.id}</span>
232+
<Badge variant={statusBadgeVariant(execution.status)}>{execution.status}</Badge>
233+
</div>
234+
{execution.type === 'session' && execution.sessionKey && (
235+
<Button
236+
variant="outline"
237+
size="sm"
238+
className="gap-1.5"
239+
onClick={() => openTerminal(execution.sessionKey!)}
240+
>
241+
<Terminal className="w-3.5 h-3.5" />
242+
{formatMessage({ id: 'issues.executions.detail.openInTerminal' })}
243+
</Button>
244+
)}
245+
</div>
246+
247+
{/* Error Banner */}
248+
{execution.error && (
249+
<Card className="p-3 border-destructive/50 bg-destructive/5">
250+
<div className="flex items-start gap-2">
251+
<XCircle className="w-4 h-4 text-destructive shrink-0 mt-0.5" />
252+
<p className="text-sm text-destructive break-all">{execution.error}</p>
253+
</div>
254+
</Card>
255+
)}
256+
257+
{/* Detail Table */}
258+
<Card className="p-0 overflow-hidden">
259+
<div className="divide-y divide-border">
260+
{detailRows.map((row) => (
261+
<div key={row.label} className="grid grid-cols-3 gap-2 px-4 py-2.5">
262+
<span className="text-xs font-medium text-muted-foreground">{row.label}</span>
263+
<span className="col-span-2 text-xs font-mono text-foreground break-all">
264+
{row.value || '-'}
265+
</span>
266+
</div>
267+
))}
268+
</div>
269+
</Card>
270+
</div>
271+
);
272+
}
273+
274+
// ========== Main Panel Component ==========
275+
276+
export function ExecutionPanel() {
277+
const { formatMessage } = useIntl();
278+
const executions = useQueueExecutionStore((s) => s.executions);
279+
const clearCompleted = useQueueExecutionStore((s) => s.clearCompleted);
280+
const [selectedId, setSelectedId] = useState<string | null>(null);
281+
282+
// Sort executions: running first, then pending, then by startedAt descending
283+
const sortedExecutions = useMemo(() => {
284+
const all = Object.values(executions);
285+
const statusOrder: Record<string, number> = {
286+
running: 0,
287+
pending: 1,
288+
failed: 2,
289+
completed: 3,
290+
};
291+
return all.sort((a, b) => {
292+
const sa = statusOrder[a.status] ?? 4;
293+
const sb = statusOrder[b.status] ?? 4;
294+
if (sa !== sb) return sa - sb;
295+
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
296+
});
297+
}, [executions]);
298+
299+
const selectedExecution = selectedId ? executions[selectedId] ?? null : null;
300+
const hasCompletedOrFailed = sortedExecutions.some(
301+
(e) => e.status === 'completed' || e.status === 'failed'
302+
);
303+
304+
if (sortedExecutions.length === 0) {
305+
return (
306+
<div className="space-y-6">
307+
<ExecutionStatsCards />
308+
<ExecutionEmptyState />
309+
</div>
310+
);
311+
}
312+
313+
return (
314+
<div className="space-y-6">
315+
{/* Stats Cards */}
316+
<ExecutionStatsCards />
317+
318+
{/* Split View */}
319+
<div className="grid grid-cols-[3fr_2fr] gap-4 min-h-[400px]">
320+
{/* Left: Execution List */}
321+
<Card className="p-3 overflow-hidden flex flex-col">
322+
<div className="flex items-center justify-between mb-3">
323+
<h3 className="text-sm font-semibold text-foreground">
324+
{formatMessage({ id: 'issues.executions.list.title' })}
325+
</h3>
326+
{hasCompletedOrFailed && (
327+
<Button variant="outline" size="sm" onClick={clearCompleted}>
328+
{formatMessage({ id: 'issues.executions.list.clearCompleted' })}
329+
</Button>
330+
)}
331+
</div>
332+
<div className="flex-1 overflow-y-auto space-y-1">
333+
{sortedExecutions.map((exec) => (
334+
<ExecutionListItem
335+
key={exec.id}
336+
execution={exec}
337+
isSelected={selectedId === exec.id}
338+
onSelect={() => setSelectedId(exec.id)}
339+
/>
340+
))}
341+
</div>
342+
</Card>
343+
344+
{/* Right: Detail View */}
345+
<Card className="p-4 overflow-y-auto">
346+
<ExecutionDetailView execution={selectedExecution} />
347+
</Card>
348+
</div>
349+
</div>
350+
);
351+
}
352+
353+
export default ExecutionPanel;

0 commit comments

Comments
 (0)