mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
feat: Add Phase 6 for Post-Implementation Review and enhance workflow execution
- Introduced Phase 6: Post-Implementation Review with detailed steps for specialized reviews (quality, security, architecture, action items). - Updated SKILL.md to reflect new phase and its execution lifecycle. - Enhanced Flowchart component to conditionally display step statuses based on task tracking. - Modified TaskDrawer to pass status tracking prop to Flowchart. - Improved AgentList and other terminal dashboard components for better UI consistency and responsiveness. - Removed GlobalKpiBar component as part of UI cleanup. - Added issue detail preview in TerminalWorkbench for better user experience when no terminal is active. - Updated localization files for new strings related to the terminal dashboard and workbench. - Enhanced TaskListTab to conditionally render task stats and status dropdown based on task status tracking.
This commit is contained in:
@@ -54,8 +54,9 @@ const StatusIcon: React.FC<{ status?: string; className?: string }> = ({ status,
|
||||
const CustomNode: React.FC<{ data: FlowchartNodeData }> = ({ data }) => {
|
||||
const isPreAnalysis = data.type === 'pre-analysis';
|
||||
const isSection = data.type === 'section';
|
||||
const isCompleted = data.status === 'completed';
|
||||
const isInProgress = data.status === 'in_progress';
|
||||
const showStatus = data.showStepStatus !== false;
|
||||
const isCompleted = showStatus && data.status === 'completed';
|
||||
const isInProgress = showStatus && data.status === 'in_progress';
|
||||
|
||||
if (isSection) {
|
||||
return (
|
||||
@@ -101,14 +102,14 @@ const CustomNode: React.FC<{ data: FlowchartNodeData }> = ({ data }) => {
|
||||
<span
|
||||
className={`flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${stepBgClass}`}
|
||||
>
|
||||
{isCompleted ? <CheckCircle className="h-4 w-4" /> : data.step}
|
||||
{isCompleted && showStatus ? <CheckCircle className="h-4 w-4" /> : data.step}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-semibold ${isCompleted ? 'text-green-700 dark:text-green-400' : 'text-foreground'}`}>
|
||||
{data.label}
|
||||
</span>
|
||||
{data.status && data.status !== 'pending' && (
|
||||
{showStatus && data.status && data.status !== 'pending' && (
|
||||
<StatusIcon status={data.status} className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
@@ -141,12 +142,13 @@ const nodeTypes: NodeTypes = {
|
||||
export interface FlowchartProps {
|
||||
flowControl: FlowControl;
|
||||
className?: string;
|
||||
showStepStatus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flowchart component for visualizing implementation approach
|
||||
*/
|
||||
export function Flowchart({ flowControl, className = '' }: FlowchartProps) {
|
||||
export function Flowchart({ flowControl, className = '', showStepStatus = true }: FlowchartProps) {
|
||||
const preAnalysis = flowControl.pre_analysis || [];
|
||||
const implSteps = flowControl.implementation_approach || [];
|
||||
|
||||
@@ -185,6 +187,7 @@ export function Flowchart({ flowControl, className = '' }: FlowchartProps) {
|
||||
step: `P${idx + 1}`,
|
||||
output: step.output_to,
|
||||
type: 'pre-analysis' as const,
|
||||
showStepStatus,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -308,6 +311,7 @@ export function Flowchart({ flowControl, className = '' }: FlowchartProps) {
|
||||
type: 'implementation' as const,
|
||||
dependsOn,
|
||||
status: stepStatus,
|
||||
showStepStatus,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -411,10 +415,12 @@ export function Flowchart({ flowControl, className = '' }: FlowchartProps) {
|
||||
nodeColor={(node) => {
|
||||
const data = node.data as FlowchartNodeData;
|
||||
if (data.type === 'section') return '#9ca3af';
|
||||
// Status-based colors
|
||||
if (data.status === 'completed') return '#22c55e'; // green-500
|
||||
if (data.status === 'in_progress') return '#f59e0b'; // amber-500
|
||||
if (data.status === 'blocked') return '#ef4444'; // red-500
|
||||
// Status-based colors (only when status tracking is enabled)
|
||||
if (data.showStepStatus !== false) {
|
||||
if (data.status === 'completed') return '#22c55e'; // green-500
|
||||
if (data.status === 'in_progress') return '#f59e0b'; // amber-500
|
||||
if (data.status === 'blocked') return '#ef4444'; // red-500
|
||||
}
|
||||
if (data.type === 'pre-analysis') return '#f59e0b';
|
||||
return '#3b82f6';
|
||||
}}
|
||||
|
||||
@@ -382,7 +382,7 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
|
||||
{/* Flowchart Tab */}
|
||||
{hasFlowchart && (
|
||||
<TabsContent value="flowchart" className="mt-4 pb-6">
|
||||
<Flowchart flowControl={flowControl!} className="min-h-[400px]" />
|
||||
<Flowchart flowControl={flowControl!} className="min-h-[400px]" showStepStatus={hasStatusTracking} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
|
||||
@@ -95,8 +95,8 @@ export function AgentList() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-t border-border shrink-0">
|
||||
{/* Section header with visual separation */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-t border-border bg-muted/20 shrink-0">
|
||||
<Bot className="w-4 h-4 text-muted-foreground" />
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
{formatMessage({ id: 'terminalDashboard.agentList.title' })}
|
||||
@@ -110,8 +110,8 @@ export function AgentList() {
|
||||
|
||||
{/* Plan list or empty state */}
|
||||
{planEntries.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-4 px-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-center py-3 px-3">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{formatMessage({ id: 'terminalDashboard.agentList.noAgents' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
// ========================================
|
||||
// GlobalKpiBar Component
|
||||
// ========================================
|
||||
// Top bar showing 3 KPI metrics spanning the full page width.
|
||||
// Metrics:
|
||||
// 1. Active Sessions - count from sessionManagerStore (wraps cliSessionStore)
|
||||
// 2. Queue Size - pending/ready items count from useIssueQueue React Query hook
|
||||
// 3. Alert Count - total alerts from all terminalMetas
|
||||
//
|
||||
// Per design spec (V-001): consumes sessionManagerStore, NOT cliSessionStore directly.
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Activity, ListChecks, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
useSessionManagerStore,
|
||||
selectGroups,
|
||||
selectTerminalMetas,
|
||||
} from '@/stores/sessionManagerStore';
|
||||
import { useIssueQueue } from '@/hooks/useIssues';
|
||||
import type { TerminalStatus } from '@/types/terminal-dashboard';
|
||||
|
||||
// ========== KPI Item ==========
|
||||
|
||||
function KpiItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
variant = 'default',
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: number;
|
||||
variant?: 'default' | 'primary' | 'warning' | 'destructive';
|
||||
}) {
|
||||
const variantStyles = {
|
||||
default: 'text-muted-foreground',
|
||||
primary: 'text-primary',
|
||||
warning: 'text-warning',
|
||||
destructive: 'text-destructive',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={cn('w-4 h-4', variantStyles[variant])} />
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<span className={cn('text-sm font-semibold tabular-nums', variantStyles[variant])}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function GlobalKpiBar() {
|
||||
const { formatMessage } = useIntl();
|
||||
const groups = useSessionManagerStore(selectGroups);
|
||||
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
||||
const queueQuery = useIssueQueue();
|
||||
|
||||
// Derive active session count from sessionManagerStore groups
|
||||
const sessionCount = useMemo(() => {
|
||||
const allSessionIds = groups.flatMap((g) => g.sessionIds);
|
||||
// Count sessions that have 'active' status in terminalMetas
|
||||
let activeCount = 0;
|
||||
for (const sid of allSessionIds) {
|
||||
const meta = terminalMetas[sid];
|
||||
const status: TerminalStatus = meta?.status ?? 'idle';
|
||||
if (status === 'active') {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
// If no sessions are managed in groups, return total unique session IDs
|
||||
// This ensures the KPI shows meaningful data even before grouping
|
||||
return activeCount > 0 ? activeCount : allSessionIds.length;
|
||||
}, [groups, terminalMetas]);
|
||||
|
||||
// Derive queue pending count from useIssueQueue data
|
||||
const queuePendingCount = useMemo(() => {
|
||||
const queue = queueQuery.data;
|
||||
if (!queue) return 0;
|
||||
// Count all items across grouped_items
|
||||
let count = 0;
|
||||
if (queue.grouped_items) {
|
||||
for (const items of Object.values(queue.grouped_items)) {
|
||||
count += items.length;
|
||||
}
|
||||
}
|
||||
// Also count ungrouped tasks and solutions
|
||||
if (queue.tasks) count += queue.tasks.length;
|
||||
if (queue.solutions) count += queue.solutions.length;
|
||||
return count;
|
||||
}, [queueQuery.data]);
|
||||
|
||||
// Derive total alert count from all terminalMetas
|
||||
const totalAlerts = useMemo(() => {
|
||||
let count = 0;
|
||||
for (const meta of Object.values(terminalMetas)) {
|
||||
count += meta.alertCount;
|
||||
}
|
||||
return count;
|
||||
}, [terminalMetas]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-6 px-4 py-2 border-b border-border bg-muted/30 shrink-0">
|
||||
<KpiItem
|
||||
icon={Activity}
|
||||
label={formatMessage({ id: 'terminalDashboard.kpi.activeSessions' })}
|
||||
value={sessionCount}
|
||||
variant="primary"
|
||||
/>
|
||||
|
||||
<div className="w-px h-4 bg-border" />
|
||||
|
||||
<KpiItem
|
||||
icon={ListChecks}
|
||||
label={formatMessage({ id: 'terminalDashboard.kpi.queueSize' })}
|
||||
value={queuePendingCount}
|
||||
variant={queuePendingCount > 0 ? 'warning' : 'default'}
|
||||
/>
|
||||
|
||||
<div className="w-px h-4 bg-border" />
|
||||
|
||||
<KpiItem
|
||||
icon={AlertTriangle}
|
||||
label={formatMessage({ id: 'terminalDashboard.kpi.alertCount' })}
|
||||
value={totalAlerts}
|
||||
variant={totalAlerts > 0 ? 'destructive' : 'default'}
|
||||
/>
|
||||
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{formatMessage({ id: 'terminalDashboard.page.title' })}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ function IssueItem({
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'w-full text-left px-3 py-2 rounded-md transition-colors',
|
||||
'w-full text-left px-2.5 py-1.5 rounded-md transition-colors',
|
||||
'hover:bg-muted/60 focus:outline-none focus:ring-1 focus:ring-primary/30',
|
||||
isSelected && 'bg-primary/10 ring-1 ring-primary/30',
|
||||
isHighlighted && !isSelected && 'bg-accent/50'
|
||||
@@ -120,7 +120,7 @@ function IssueItem({
|
||||
{issue.context}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground pl-5">
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[10px] text-muted-foreground pl-5">
|
||||
<span className="font-mono">{issue.id}</span>
|
||||
{issue.labels && issue.labels.length > 0 && (
|
||||
<>
|
||||
@@ -140,7 +140,7 @@ function IssueEmptyState() {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground p-4">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="h-10 w-10 mx-auto mb-3 opacity-40" />
|
||||
<AlertCircle className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
|
||||
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.issuePanel.noIssues' })}</p>
|
||||
<p className="text-xs mt-1 opacity-70">
|
||||
{formatMessage({ id: 'terminalDashboard.issuePanel.noIssuesDesc' })}
|
||||
@@ -157,7 +157,7 @@ function IssueErrorState({ error }: { error: Error }) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-destructive p-4">
|
||||
<div className="text-center">
|
||||
<AlertTriangle className="h-10 w-10 mx-auto mb-3 opacity-60" />
|
||||
<AlertTriangle className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
|
||||
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.issuePanel.error' })}</p>
|
||||
<p className="text-xs mt-1 opacity-70">{error.message}</p>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,7 @@ function QueueErrorState({ error }: { error: Error }) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-destructive p-4">
|
||||
<div className="text-center">
|
||||
<AlertTriangle className="h-10 w-10 mx-auto mb-3 opacity-60" />
|
||||
<AlertTriangle className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
|
||||
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.error' })}</p>
|
||||
<p className="text-xs mt-1 opacity-70">{error.message}</p>
|
||||
</div>
|
||||
|
||||
@@ -96,8 +96,8 @@ export function SessionGroupTree() {
|
||||
{formatMessage({ id: 'terminalDashboard.sessionTree.createGroup' })}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-2 text-muted-foreground p-4">
|
||||
<Folder className="w-8 h-8 opacity-50" />
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-1.5 text-muted-foreground p-4">
|
||||
<Folder className="w-6 h-6 opacity-30" />
|
||||
<p className="text-xs text-center">
|
||||
{formatMessage({ id: 'terminalDashboard.sessionTree.noGroups' })}
|
||||
</p>
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
// Horizontal tab strip for terminal sessions in the Terminal Dashboard.
|
||||
// Renders tabs from sessionManagerStore groups with status indicators and alert badges.
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { Terminal, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
useSessionManagerStore,
|
||||
@@ -35,6 +36,15 @@ export function TerminalTabBar() {
|
||||
// Flatten all sessionIds from all groups
|
||||
const allSessionIds = groups.flatMap((g) => g.sessionIds);
|
||||
|
||||
// Total alerts across all terminals
|
||||
const totalAlerts = useMemo(() => {
|
||||
let count = 0;
|
||||
for (const meta of Object.values(terminalMetas)) {
|
||||
count += meta.alertCount;
|
||||
}
|
||||
return count;
|
||||
}, [terminalMetas]);
|
||||
|
||||
if (allSessionIds.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-muted/30 min-h-[40px]">
|
||||
@@ -88,6 +98,16 @@ export function TerminalTabBar() {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Total alerts indicator at right end */}
|
||||
{totalAlerts > 0 && (
|
||||
<div className="ml-auto flex items-center gap-1 px-3 py-2 shrink-0 text-destructive">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span className="text-[10px] font-semibold tabular-nums">
|
||||
{totalAlerts > 99 ? '99+' : totalAlerts}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,23 +2,139 @@
|
||||
// TerminalWorkbench Component
|
||||
// ========================================
|
||||
// Container for the right panel of the Terminal Dashboard.
|
||||
// Combines TerminalTabBar (tab switching) and TerminalInstance (xterm.js)
|
||||
// in a flex-col layout. MVP scope: single terminal view (1x1 grid).
|
||||
// Combines TerminalTabBar (tab switching) and TerminalInstance (xterm.js).
|
||||
// When no terminal is active, shows selected issue detail preview
|
||||
// or a compact empty state with action hints.
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import {
|
||||
Terminal,
|
||||
CircleDot,
|
||||
Tag,
|
||||
Clock,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
useSessionManagerStore,
|
||||
selectSessionManagerActiveTerminalId,
|
||||
} from '@/stores/sessionManagerStore';
|
||||
import {
|
||||
useIssueQueueIntegrationStore,
|
||||
selectSelectedIssueId,
|
||||
} from '@/stores/issueQueueIntegrationStore';
|
||||
import { useIssues } from '@/hooks/useIssues';
|
||||
import type { Issue } from '@/lib/api';
|
||||
import { TerminalTabBar } from './TerminalTabBar';
|
||||
import { TerminalInstance } from './TerminalInstance';
|
||||
|
||||
// ========== Priority Styles ==========
|
||||
|
||||
const PRIORITY_VARIANT: Record<Issue['priority'], 'destructive' | 'warning' | 'info' | 'secondary'> = {
|
||||
critical: 'destructive',
|
||||
high: 'warning',
|
||||
medium: 'info',
|
||||
low: 'secondary',
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<Issue['status'], string> = {
|
||||
open: 'text-info',
|
||||
in_progress: 'text-warning',
|
||||
resolved: 'text-success',
|
||||
closed: 'text-muted-foreground',
|
||||
completed: 'text-success',
|
||||
};
|
||||
|
||||
// ========== Issue Detail Preview ==========
|
||||
|
||||
function IssueDetailPreview({ issue }: { issue: Issue }) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-lg mx-auto space-y-4">
|
||||
{/* Header hint */}
|
||||
<p className="text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
{formatMessage({ id: 'terminalDashboard.workbench.issuePreview' })}
|
||||
</p>
|
||||
|
||||
{/* Title + Status */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<CircleDot className={cn('w-4 h-4 shrink-0 mt-0.5', STATUS_COLORS[issue.status] ?? 'text-muted-foreground')} />
|
||||
<h3 className="text-base font-semibold text-foreground leading-snug">
|
||||
{issue.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-6">
|
||||
<Badge variant={PRIORITY_VARIANT[issue.priority]} className="text-[10px] px-1.5 py-0">
|
||||
{issue.priority}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">{issue.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Context / Description */}
|
||||
{issue.context && (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-3">
|
||||
<p className="text-xs text-foreground/80 leading-relaxed whitespace-pre-wrap">
|
||||
{issue.context}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata rows */}
|
||||
<div className="space-y-1.5 text-xs text-muted-foreground">
|
||||
{issue.labels && issue.labels.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-3.5 h-3.5 shrink-0" />
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{issue.labels.map((label) => (
|
||||
<span key={label} className="px-1.5 py-0.5 rounded bg-muted text-[10px]">
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issue.assignee && (
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>{issue.assignee}</span>
|
||||
</div>
|
||||
)}
|
||||
{issue.createdAt && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>{new Date(issue.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hint */}
|
||||
<p className="text-[10px] text-muted-foreground/60 pt-2">
|
||||
{formatMessage({ id: 'terminalDashboard.workbench.issuePreviewHint' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export function TerminalWorkbench() {
|
||||
const { formatMessage } = useIntl();
|
||||
const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId);
|
||||
const selectedIssueId = useIssueQueueIntegrationStore(selectSelectedIssueId);
|
||||
const { issues } = useIssues();
|
||||
|
||||
// Find selected issue for preview
|
||||
const selectedIssue = useMemo(() => {
|
||||
if (!selectedIssueId) return null;
|
||||
return issues.find((i) => i.id === selectedIssueId) ?? null;
|
||||
}, [selectedIssueId, issues]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -30,11 +146,14 @@ export function TerminalWorkbench() {
|
||||
<div className="flex-1 min-h-0">
|
||||
<TerminalInstance sessionId={activeTerminalId} />
|
||||
</div>
|
||||
) : selectedIssue ? (
|
||||
/* Issue detail preview when no terminal but issue is selected */
|
||||
<IssueDetailPreview issue={selectedIssue} />
|
||||
) : (
|
||||
/* Empty state when no terminal is selected */
|
||||
/* Compact empty state */
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Terminal className="h-10 w-10 mx-auto mb-3 opacity-50" />
|
||||
<Terminal className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
|
||||
<p className="text-sm font-medium">
|
||||
{formatMessage({ id: 'terminalDashboard.workbench.noTerminal' })}
|
||||
</p>
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
"noSelection": "Select an item to view details",
|
||||
"associationChain": "Association Chain"
|
||||
},
|
||||
"bottomPanel": {
|
||||
"queueTab": "Queue",
|
||||
"inspectorTab": "Inspector",
|
||||
"collapse": "Collapse panel",
|
||||
"expand": "Expand panel"
|
||||
},
|
||||
"sessionTree": {
|
||||
"createGroup": "New Group",
|
||||
"groupNamePrompt": "Enter group name",
|
||||
@@ -67,7 +73,9 @@
|
||||
},
|
||||
"workbench": {
|
||||
"noTerminal": "No terminal selected",
|
||||
"noTerminalHint": "Select a session from the tab bar or create a new one"
|
||||
"noTerminalHint": "Select a session from the tab bar or create a new one",
|
||||
"issuePreview": "Issue Preview",
|
||||
"issuePreviewHint": "Select a terminal session or send this issue to the queue to begin work"
|
||||
},
|
||||
"placeholder": {
|
||||
"sessionTree": "Session groups will appear here",
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
"noSelection": "选择一个项目以查看详情",
|
||||
"associationChain": "关联链路"
|
||||
},
|
||||
"bottomPanel": {
|
||||
"queueTab": "队列",
|
||||
"inspectorTab": "检查器",
|
||||
"collapse": "折叠面板",
|
||||
"expand": "展开面板"
|
||||
},
|
||||
"sessionTree": {
|
||||
"createGroup": "新建分组",
|
||||
"groupNamePrompt": "输入分组名称",
|
||||
@@ -67,7 +73,9 @@
|
||||
},
|
||||
"workbench": {
|
||||
"noTerminal": "未选择终端",
|
||||
"noTerminalHint": "从标签栏选择会话或创建新会话"
|
||||
"noTerminalHint": "从标签栏选择会话或创建新会话",
|
||||
"issuePreview": "问题预览",
|
||||
"issuePreviewHint": "选择终端会话或将此问题发送到队列以开始工作"
|
||||
},
|
||||
"placeholder": {
|
||||
"sessionTree": "会话分组将在此显示",
|
||||
|
||||
@@ -44,6 +44,14 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const tasks = session.tasks || [];
|
||||
|
||||
// Detect if session tasks support status tracking (new format has explicit status/status_history in raw data)
|
||||
const hasStatusTracking = tasks.some((t) => {
|
||||
const raw = (t as unknown as Record<string, unknown>)._raw as Record<string, unknown> | undefined;
|
||||
const source = (raw?._raw as Record<string, unknown>) || raw;
|
||||
return source ? (source.status !== undefined || source.status_history !== undefined) : false;
|
||||
});
|
||||
|
||||
const completed = tasks.filter((t) => t.status === 'completed').length;
|
||||
const inProgress = tasks.filter((t) => t.status === 'in_progress').length;
|
||||
const pending = tasks.filter((t) => t.status === 'pending').length;
|
||||
@@ -165,18 +173,20 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats Bar with Bulk Actions */}
|
||||
<TaskStatsBar
|
||||
completed={completed}
|
||||
inProgress={inProgress}
|
||||
pending={pending}
|
||||
onMarkAllPending={handleMarkAllPending}
|
||||
onMarkAllInProgress={handleMarkAllInProgress}
|
||||
onMarkAllCompleted={handleMarkAllCompleted}
|
||||
isLoadingPending={isLoadingPending}
|
||||
isLoadingInProgress={isLoadingInProgress}
|
||||
isLoadingCompleted={isLoadingCompleted}
|
||||
/>
|
||||
{/* Stats Bar with Bulk Actions (only for tasks with status tracking) */}
|
||||
{hasStatusTracking && (
|
||||
<TaskStatsBar
|
||||
completed={completed}
|
||||
inProgress={inProgress}
|
||||
pending={pending}
|
||||
onMarkAllPending={handleMarkAllPending}
|
||||
onMarkAllInProgress={handleMarkAllInProgress}
|
||||
onMarkAllCompleted={handleMarkAllCompleted}
|
||||
isLoadingPending={isLoadingPending}
|
||||
isLoadingInProgress={isLoadingInProgress}
|
||||
isLoadingCompleted={isLoadingCompleted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tasks List */}
|
||||
{localTasks.length === 0 ? (
|
||||
@@ -236,12 +246,14 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
||||
|
||||
{/* Right: Status and Meta info */}
|
||||
<div className="flex flex-col items-end gap-2 flex-shrink-0">
|
||||
{/* Row 1: Status dropdown */}
|
||||
<TaskStatusDropdown
|
||||
currentStatus={task.status as TaskStatus}
|
||||
onStatusChange={(newStatus) => handleTaskStatusChange(task.task_id, newStatus)}
|
||||
size="sm"
|
||||
/>
|
||||
{/* Row 1: Status dropdown (only for tasks with status tracking) */}
|
||||
{hasStatusTracking && (
|
||||
<TaskStatusDropdown
|
||||
currentStatus={task.status as TaskStatus}
|
||||
onStatusChange={(newStatus) => handleTaskStatusChange(task.task_id, newStatus)}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Row 2: Meta info */}
|
||||
<div className="flex items-center gap-3 flex-wrap justify-end text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user