mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
Add benchmark results for fast3 and fast4, implement KeepAliveLspBridge, and add tests for staged strategies
- Added new benchmark result files: compare_2026-02-09_score_fast3.json and compare_2026-02-09_score_fast4.json. - Implemented KeepAliveLspBridge to maintain a persistent LSP connection across multiple queries, improving performance. - Created unit tests for staged clustering strategies in test_staged_stage3_fast_strategies.py, ensuring correct behavior of score and dir_rr strategies.
This commit is contained in:
@@ -33,6 +33,7 @@ import type { IssueQueue, QueueItem } from '@/lib/api';
|
||||
|
||||
export interface QueueActionsProps {
|
||||
queue: IssueQueue;
|
||||
queueId?: string;
|
||||
isActive?: boolean;
|
||||
onActivate?: (queueId: string) => void;
|
||||
onDeactivate?: () => void;
|
||||
@@ -50,6 +51,7 @@ export interface QueueActionsProps {
|
||||
|
||||
export function QueueActions({
|
||||
queue,
|
||||
queueId: queueIdProp,
|
||||
isActive = false,
|
||||
onActivate,
|
||||
onDeactivate,
|
||||
@@ -69,20 +71,20 @@ export function QueueActions({
|
||||
const [mergeTargetId, setMergeTargetId] = useState('');
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
|
||||
|
||||
// Use "current" as the queue ID for single-queue model
|
||||
// This matches the API pattern where deactivate works on the current queue
|
||||
const queueId = 'current';
|
||||
const queueId = queueIdProp;
|
||||
|
||||
// Get all items from grouped_items for split dialog
|
||||
const allItems: QueueItem[] = Object.values(queue.grouped_items || {}).flat();
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!queueId) return;
|
||||
onDelete?.(queueId);
|
||||
setIsDeleteOpen(false);
|
||||
};
|
||||
|
||||
const handleMerge = () => {
|
||||
if (mergeTargetId.trim()) {
|
||||
if (!queueId) return;
|
||||
onMerge?.(queueId, mergeTargetId.trim());
|
||||
setIsMergeOpen(false);
|
||||
setMergeTargetId('');
|
||||
@@ -91,6 +93,7 @@ export function QueueActions({
|
||||
|
||||
const handleSplit = () => {
|
||||
if (selectedItemIds.length > 0 && selectedItemIds.length < allItems.length) {
|
||||
if (!queueId) return;
|
||||
onSplit?.(queueId, selectedItemIds);
|
||||
setIsSplitOpen(false);
|
||||
setSelectedItemIds([]);
|
||||
@@ -128,7 +131,7 @@ export function QueueActions({
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => onActivate(queueId)}
|
||||
disabled={isActivating}
|
||||
disabled={isActivating || !queueId}
|
||||
title={formatMessage({ id: 'issues.queue.actions.activate' })}
|
||||
>
|
||||
{isActivating ? (
|
||||
@@ -161,7 +164,7 @@ export function QueueActions({
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setIsMergeOpen(true)}
|
||||
disabled={isMerging}
|
||||
disabled={isMerging || !queueId}
|
||||
title={formatMessage({ id: 'issues.queue.actions.merge' })}
|
||||
>
|
||||
{isMerging ? (
|
||||
@@ -178,7 +181,7 @@ export function QueueActions({
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setIsSplitOpen(true)}
|
||||
disabled={isSplitting}
|
||||
disabled={isSplitting || !queueId}
|
||||
title={formatMessage({ id: 'issues.queue.actions.split' })}
|
||||
>
|
||||
{isSplitting ? (
|
||||
@@ -195,7 +198,7 @@ export function QueueActions({
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setIsDeleteOpen(true)}
|
||||
disabled={isDeleting}
|
||||
disabled={isDeleting || !queueId}
|
||||
title={formatMessage({ id: 'issues.queue.actions.delete' })}
|
||||
>
|
||||
{isDeleting ? (
|
||||
|
||||
171
ccw/frontend/src/components/issue/queue/QueueBoard.tsx
Normal file
171
ccw/frontend/src/components/issue/queue/QueueBoard.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
// ========================================
|
||||
// QueueBoard
|
||||
// ========================================
|
||||
// Kanban-style view of queue execution groups with DnD reordering/moving.
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { KanbanBoard, type KanbanColumn, type KanbanItem } from '@/components/shared/KanbanBoard';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useQueueMutations } from '@/hooks';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import type { IssueQueue, QueueItem } from '@/lib/api';
|
||||
|
||||
type QueueBoardItem = QueueItem & KanbanItem;
|
||||
|
||||
function groupSortKey(groupId: string): [number, string] {
|
||||
const n = parseInt(groupId.match(/\d+/)?.[0] || '999');
|
||||
return [Number.isFinite(n) ? n : 999, groupId];
|
||||
}
|
||||
|
||||
function buildColumns(queue: IssueQueue): KanbanColumn<QueueBoardItem>[] {
|
||||
const entries = Object.entries(queue.grouped_items || {});
|
||||
entries.sort(([a], [b]) => {
|
||||
const [an, aid] = groupSortKey(a);
|
||||
const [bn, bid] = groupSortKey(b);
|
||||
if (an !== bn) return an - bn;
|
||||
return aid.localeCompare(bid);
|
||||
});
|
||||
|
||||
return entries.map(([groupId, items]) => {
|
||||
const sorted = [...(items || [])].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0));
|
||||
const mapped = sorted.map((it) => ({
|
||||
...it,
|
||||
id: it.item_id,
|
||||
title: `${it.issue_id} · ${it.solution_id}`,
|
||||
status: it.status,
|
||||
}));
|
||||
return {
|
||||
id: groupId,
|
||||
title: groupId,
|
||||
items: mapped,
|
||||
icon: <LayoutGrid className="w-4 h-4" />,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function applyDrag(columns: KanbanColumn<QueueBoardItem>[], result: DropResult): KanbanColumn<QueueBoardItem>[] {
|
||||
if (!result.destination) return columns;
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
const next = columns.map((c) => ({ ...c, items: [...c.items] }));
|
||||
const src = next.find((c) => c.id === source.droppableId);
|
||||
const dst = next.find((c) => c.id === destination.droppableId);
|
||||
if (!src || !dst) return columns;
|
||||
|
||||
const srcIndex = src.items.findIndex((i) => i.id === draggableId);
|
||||
if (srcIndex === -1) return columns;
|
||||
|
||||
const [moved] = src.items.splice(srcIndex, 1);
|
||||
if (!moved) return columns;
|
||||
|
||||
dst.items.splice(destination.index, 0, moved);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function QueueBoard({
|
||||
queue,
|
||||
onItemClick,
|
||||
className,
|
||||
}: {
|
||||
queue: IssueQueue;
|
||||
onItemClick?: (item: QueueItem) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const { reorderQueueGroup, moveQueueItem, isReordering, isMoving } = useQueueMutations();
|
||||
|
||||
const baseColumns = useMemo(() => buildColumns(queue), [queue]);
|
||||
const [columns, setColumns] = useState<KanbanColumn<QueueBoardItem>[]>(baseColumns);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(baseColumns);
|
||||
}, [baseColumns]);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
async (result: DropResult, sourceColumn: string, destColumn: string) => {
|
||||
if (!projectPath) return;
|
||||
if (!result.destination) return;
|
||||
if (sourceColumn === destColumn && result.source.index === result.destination.index) return;
|
||||
|
||||
try {
|
||||
const nextColumns = applyDrag(columns, result);
|
||||
setColumns(nextColumns);
|
||||
|
||||
const itemId = result.draggableId;
|
||||
|
||||
if (sourceColumn === destColumn) {
|
||||
const column = nextColumns.find((c) => c.id === sourceColumn);
|
||||
const nextOrder = (column?.items ?? []).map((i) => i.item_id);
|
||||
await reorderQueueGroup(sourceColumn, nextOrder);
|
||||
} else {
|
||||
await moveQueueItem(itemId, destColumn, result.destination.index);
|
||||
}
|
||||
} catch (e) {
|
||||
// Revert by resetting to server-derived columns
|
||||
setColumns(baseColumns);
|
||||
}
|
||||
},
|
||||
[baseColumns, columns, moveQueueItem, projectPath, reorderQueueGroup]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
{formatMessage({ id: 'issues.queue.stats.executionGroups' })}
|
||||
</Badge>
|
||||
{(isReordering || isMoving) && (
|
||||
<span>
|
||||
{formatMessage({ id: 'common.status.running' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<KanbanBoard<QueueBoardItem>
|
||||
columns={columns}
|
||||
onDragEnd={handleDragEnd}
|
||||
onItemClick={(item) => onItemClick?.(item as unknown as QueueItem)}
|
||||
emptyColumnMessage={formatMessage({ id: 'issues.queue.empty' })}
|
||||
renderItem={(item, provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={() => onItemClick?.(item as unknown as QueueItem)}
|
||||
className={cn(
|
||||
'p-3 bg-card border border-border rounded-lg shadow-sm cursor-pointer',
|
||||
'hover:shadow-md hover:border-primary/50 transition-all',
|
||||
item.status === 'blocked' && 'border-destructive/50 bg-destructive/5',
|
||||
item.status === 'failed' && 'border-destructive/50 bg-destructive/5',
|
||||
item.status === 'executing' && 'border-primary/40'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-mono text-muted-foreground">{item.item_id}</div>
|
||||
<div className="text-sm font-medium text-foreground truncate">
|
||||
{item.issue_id} · {item.solution_id}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{formatMessage({ id: `issues.queue.status.${item.status}` })}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.depends_on?.length ? (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'issues.solution.overview.dependencies' })}: {item.depends_on.length}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueBoard;
|
||||
@@ -51,8 +51,7 @@ export function QueueCard({
|
||||
}: QueueCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// Use "current" for queue ID display
|
||||
const queueId = 'current';
|
||||
const queueId = queue.id;
|
||||
|
||||
// Calculate item counts
|
||||
const taskCount = queue.tasks?.length || 0;
|
||||
@@ -88,7 +87,7 @@ export function QueueCard({
|
||||
{formatMessage({ id: 'issues.queue.title' })}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{queueId.substring(0, 20)}{queueId.length > 20 ? '...' : ''}
|
||||
{(queueId || 'legacy').substring(0, 20)}{(queueId || 'legacy').length > 20 ? '...' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,6 +101,7 @@ export function QueueCard({
|
||||
|
||||
<QueueActions
|
||||
queue={queue}
|
||||
queueId={queueId}
|
||||
isActive={isActive}
|
||||
onActivate={onActivate}
|
||||
onDeactivate={onDeactivate}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
// ========================================
|
||||
// QueueExecuteInSession
|
||||
// ========================================
|
||||
// Minimal “execution plane” for queue items:
|
||||
// pick/create a PTY session and submit a generated prompt to it.
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Plus, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useIssues } from '@/hooks';
|
||||
import {
|
||||
createCliSession,
|
||||
executeInCliSession,
|
||||
fetchCliSessions,
|
||||
type CliSession,
|
||||
type QueueItem,
|
||||
} from '@/lib/api';
|
||||
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
||||
|
||||
type ToolName = 'claude' | 'codex' | 'gemini';
|
||||
type ResumeStrategy = 'nativeResume' | 'promptConcat';
|
||||
|
||||
function buildQueueItemPrompt(item: QueueItem, issue: any | undefined): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`Queue Item: ${item.item_id}`);
|
||||
lines.push(`Issue: ${item.issue_id}`);
|
||||
lines.push(`Solution: ${item.solution_id}`);
|
||||
if (item.task_id) lines.push(`Task: ${item.task_id}`);
|
||||
lines.push('');
|
||||
|
||||
if (issue) {
|
||||
if (issue.title) lines.push(`Title: ${issue.title}`);
|
||||
if (issue.context) {
|
||||
lines.push('');
|
||||
lines.push('Context:');
|
||||
lines.push(String(issue.context));
|
||||
}
|
||||
|
||||
const solution = Array.isArray(issue.solutions)
|
||||
? issue.solutions.find((s: any) => s?.id === item.solution_id)
|
||||
: undefined;
|
||||
if (solution) {
|
||||
lines.push('');
|
||||
lines.push('Solution Description:');
|
||||
if (solution.description) lines.push(String(solution.description));
|
||||
if (solution.approach) {
|
||||
lines.push('');
|
||||
lines.push('Approach:');
|
||||
lines.push(String(solution.approach));
|
||||
}
|
||||
|
||||
// Best-effort: if the solution has embedded tasks, include the matched task.
|
||||
const tasks = Array.isArray(solution.tasks) ? solution.tasks : [];
|
||||
const task = item.task_id ? tasks.find((t: any) => t?.id === item.task_id) : undefined;
|
||||
if (task) {
|
||||
lines.push('');
|
||||
lines.push('Task:');
|
||||
if (task.title) lines.push(`- ${task.title}`);
|
||||
if (task.description) lines.push(String(task.description));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('Instruction:');
|
||||
lines.push(
|
||||
'Implement the above queue item in this repository. Prefer small, testable changes; run relevant tests; report blockers if any.'
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function QueueExecuteInSession({ item, className }: { item: QueueItem; className?: string }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const { issues } = useIssues();
|
||||
const issue = useMemo(() => issues.find((i) => i.id === item.issue_id) as any, [issues, item.issue_id]);
|
||||
|
||||
const sessionsByKey = useCliSessionStore((s) => s.sessions);
|
||||
const setSessions = useCliSessionStore((s) => s.setSessions);
|
||||
const upsertSession = useCliSessionStore((s) => s.upsertSession);
|
||||
|
||||
const sessions = useMemo(
|
||||
() => Object.values(sessionsByKey).sort((a, b) => a.createdAt.localeCompare(b.createdAt)),
|
||||
[sessionsByKey]
|
||||
);
|
||||
|
||||
const [selectedSessionKey, setSelectedSessionKey] = useState<string>('');
|
||||
const [tool, setTool] = useState<ToolName>('claude');
|
||||
const [mode, setMode] = useState<'analysis' | 'write'>('write');
|
||||
const [resumeStrategy, setResumeStrategy] = useState<ResumeStrategy>('nativeResume');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastExecution, setLastExecution] = useState<{ executionId: string; command: string } | null>(null);
|
||||
|
||||
const refreshSessions = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const r = await fetchCliSessions();
|
||||
setSessions(r.sessions as unknown as CliSession[]);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void refreshSessions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSessionKey) return;
|
||||
if (sessions.length === 0) return;
|
||||
setSelectedSessionKey(sessions[sessions.length - 1]?.sessionKey ?? '');
|
||||
}, [sessions, selectedSessionKey]);
|
||||
|
||||
const ensureSession = async (): Promise<string> => {
|
||||
if (selectedSessionKey) return selectedSessionKey;
|
||||
if (!projectPath) throw new Error('No project path selected');
|
||||
const created = await createCliSession({
|
||||
workingDir: projectPath,
|
||||
preferredShell: 'bash',
|
||||
resumeKey: item.issue_id,
|
||||
});
|
||||
upsertSession(created.session as unknown as CliSession);
|
||||
setSelectedSessionKey(created.session.sessionKey);
|
||||
return created.session.sessionKey;
|
||||
};
|
||||
|
||||
const handleCreateSession = async () => {
|
||||
setError(null);
|
||||
try {
|
||||
if (!projectPath) throw new Error('No project path selected');
|
||||
const created = await createCliSession({
|
||||
workingDir: projectPath,
|
||||
preferredShell: 'bash',
|
||||
resumeKey: item.issue_id,
|
||||
});
|
||||
upsertSession(created.session as unknown as CliSession);
|
||||
setSelectedSessionKey(created.session.sessionKey);
|
||||
await refreshSessions();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async () => {
|
||||
setIsExecuting(true);
|
||||
setError(null);
|
||||
setLastExecution(null);
|
||||
try {
|
||||
const sessionKey = await ensureSession();
|
||||
const prompt = buildQueueItemPrompt(item, issue);
|
||||
const result = await executeInCliSession(sessionKey, {
|
||||
tool,
|
||||
prompt,
|
||||
mode,
|
||||
workingDir: projectPath,
|
||||
category: 'user',
|
||||
resumeKey: item.issue_id,
|
||||
resumeStrategy,
|
||||
});
|
||||
setLastExecution({ executionId: result.executionId, command: result.command });
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{formatMessage({ id: 'issues.queue.exec.title' })}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshSessions}
|
||||
disabled={isLoading}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
||||
{formatMessage({ id: 'issues.terminal.session.refresh' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleCreateSession} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
{formatMessage({ id: 'issues.terminal.session.new' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
||||
{formatMessage({ id: 'issues.terminal.session.select' })}
|
||||
</label>
|
||||
<Select value={selectedSessionKey} onValueChange={(v) => setSelectedSessionKey(v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={formatMessage({ id: 'issues.terminal.session.none' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sessions.length === 0 ? (
|
||||
<SelectItem value="" disabled>
|
||||
{formatMessage({ id: 'issues.terminal.session.none' })}
|
||||
</SelectItem>
|
||||
) : (
|
||||
sessions.map((s) => (
|
||||
<SelectItem key={s.sessionKey} value={s.sessionKey}>
|
||||
{(s.tool || 'cli') + ' · ' + s.sessionKey}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
||||
{formatMessage({ id: 'issues.terminal.exec.tool' })}
|
||||
</label>
|
||||
<Select value={tool} onValueChange={(v) => setTool(v as ToolName)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude">claude</SelectItem>
|
||||
<SelectItem value="codex">codex</SelectItem>
|
||||
<SelectItem value="gemini">gemini</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
||||
{formatMessage({ id: 'issues.terminal.exec.mode' })}
|
||||
</label>
|
||||
<Select value={mode} onValueChange={(v) => setMode(v as 'analysis' | 'write')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="analysis">analysis</SelectItem>
|
||||
<SelectItem value="write">write</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
||||
{formatMessage({ id: 'issues.terminal.exec.resumeStrategy' })}
|
||||
</label>
|
||||
<Select value={resumeStrategy} onValueChange={(v) => setResumeStrategy(v as ResumeStrategy)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="nativeResume">nativeResume</SelectItem>
|
||||
<SelectItem value="promptConcat">promptConcat</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-sm text-destructive">{error}</div>}
|
||||
{lastExecution && (
|
||||
<div className="text-xs text-muted-foreground font-mono break-all">
|
||||
{lastExecution.executionId}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Button onClick={handleExecute} disabled={isExecuting || !projectPath} className="gap-2">
|
||||
{formatMessage({ id: 'issues.terminal.exec.run' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { X, FileText, CheckCircle, Circle, Loader2, XCircle, Clock, AlertTriangle } from 'lucide-react';
|
||||
import { X, FileText, CheckCircle, Circle, Loader2, XCircle, Clock, AlertTriangle, Terminal } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
||||
import { QueueExecuteInSession } from '@/components/issue/queue/QueueExecuteInSession';
|
||||
import { IssueTerminalTab } from '@/components/issue/hub/IssueTerminalTab';
|
||||
import { useIssueQueue } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { QueueItem } from '@/lib/api';
|
||||
@@ -20,7 +22,7 @@ export interface SolutionDrawerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type TabValue = 'overview' | 'tasks' | 'json';
|
||||
type TabValue = 'overview' | 'tasks' | 'terminal' | 'json';
|
||||
|
||||
// ========== Status Configuration ==========
|
||||
const statusConfig: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info'; icon: React.ComponentType<{ className?: string }> }> = {
|
||||
@@ -134,6 +136,10 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) {
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'issues.solution.tabs.tasks' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="terminal" className="flex-1">
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'issues.solution.tabs.terminal' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="json" className="flex-1">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'issues.solution.tabs.json' })}
|
||||
@@ -170,6 +176,9 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execute in Session */}
|
||||
<QueueExecuteInSession item={item} />
|
||||
|
||||
{/* Dependencies */}
|
||||
{item.depends_on && item.depends_on.length > 0 && (
|
||||
<div>
|
||||
@@ -244,6 +253,11 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) {
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Terminal Tab */}
|
||||
<TabsContent value="terminal" className="mt-4 pb-6 focus-visible:outline-none">
|
||||
<IssueTerminalTab issueId={issueId} />
|
||||
</TabsContent>
|
||||
|
||||
{/* JSON Tab */}
|
||||
<TabsContent value="json" className="mt-4 pb-6 focus-visible:outline-none">
|
||||
<pre className="p-4 bg-muted rounded-md overflow-x-auto text-xs">
|
||||
|
||||
Reference in New Issue
Block a user