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:
catlog22
2026-02-14 21:49:31 +08:00
parent d535ab4749
commit 37d19ada75
15 changed files with 448 additions and 510 deletions

View File

@@ -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';
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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": "会话分组将在此显示",

View File

@@ -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">