feat(docs): add full documentation generation phase and related documentation generation phase

- Implement Phase 4: Full Documentation Generation with multi-layered strategy and tool fallback.
- Introduce Phase 5: Related Documentation Generation for incremental updates based on git changes.
- Create new utility components for displaying execution status in the terminal panel.
- Add helper functions for rendering execution status icons and formatting relative time.
- Establish a recent paths configuration for improved path resolution.
This commit is contained in:
catlog22
2026-02-13 21:46:28 +08:00
parent d750290f84
commit 25e27286b4
21 changed files with 692 additions and 498 deletions

View File

@@ -11,9 +11,7 @@ import {
Play,
CheckCircle,
XCircle,
Clock,
Terminal,
Loader2,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
@@ -23,50 +21,9 @@ import {
selectExecutionStats,
useTerminalPanelStore,
} from '@/stores';
import type { QueueExecution, QueueExecutionStatus } from '@/stores/queueExecutionStore';
import type { QueueExecution } from '@/stores/queueExecutionStore';
import { cn } from '@/lib/utils';
// ========== Helpers ==========
function statusBadgeVariant(status: QueueExecutionStatus): 'info' | 'success' | 'destructive' | 'secondary' {
switch (status) {
case 'running':
return 'info';
case 'completed':
return 'success';
case 'failed':
return 'destructive';
case 'pending':
default:
return 'secondary';
}
}
function statusIcon(status: QueueExecutionStatus) {
switch (status) {
case 'running':
return <Loader2 className="w-3.5 h-3.5 animate-spin" />;
case 'completed':
return <CheckCircle className="w-3.5 h-3.5" />;
case 'failed':
return <XCircle className="w-3.5 h-3.5" />;
case 'pending':
default:
return <Clock className="w-3.5 h-3.5" />;
}
}
function formatRelativeTime(isoString: string): string {
const diff = Date.now() - new Date(isoString).getTime();
const seconds = Math.floor(diff / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
import { statusIcon, statusBadgeVariant, formatRelativeTime } from '@/lib/execution-display-utils';
// ========== Empty State ==========

View File

@@ -38,6 +38,7 @@ import {
} from '@/components/shared/CliExecutionSettings';
import { buildQueueItemContext } from '@/lib/queue-prompt';
import { useQueueExecutionStore, type QueueExecution } from '@/stores/queueExecutionStore';
import type { Flow } from '@/types/flow';
// ---------------------------------------------------------------------------
// Types
@@ -70,7 +71,7 @@ export function QueueItemExecutor({ item, className }: QueueItemExecutorProps) {
// Resolve the parent issue for context building
const { issues } = useIssues();
const issue = useMemo(
() => issues.find((i) => i.id === item.issue_id) as any,
() => issues.find((i) => i.id === item.issue_id),
[issues, item.issue_id]
);
@@ -203,13 +204,13 @@ export function QueueItemExecutor({ item, className }: QueueItemExecutorProps) {
throw new Error('Failed to create flow');
}
// Hydrate Orchestrator stores
const flowDto = created.data as any;
// Hydrate Orchestrator stores -- convert OrchestratorFlowDto to Flow
const flowDto = created.data;
const parsedVersion = parseInt(String(flowDto.version ?? '1'), 10);
const flowForStore = {
const flowForStore: Flow = {
...flowDto,
version: Number.isFinite(parsedVersion) ? parsedVersion : 1,
} as any;
} as Flow;
useFlowStore.getState().setCurrentFlow(flowForStore);
// Execute the flow

View File

@@ -59,7 +59,7 @@ export interface CcwConfig {
enabledTools: string[];
projectRoot?: string;
allowedDirs?: string;
disableSandbox?: boolean;
enableSandbox?: boolean;
}
/**
@@ -75,7 +75,7 @@ export interface CcwToolsMcpCardProps {
/** Comma-separated list of allowed directories */
allowedDirs?: string;
/** Whether sandbox is disabled */
disableSandbox?: boolean;
enableSandbox?: boolean;
/** Callback when a tool is toggled */
onToggleTool: (tool: string, enabled: boolean) => void;
/** Callback when configuration is updated */
@@ -110,7 +110,7 @@ export function CcwToolsMcpCard({
enabledTools,
projectRoot,
allowedDirs,
disableSandbox,
enableSandbox,
onToggleTool,
onUpdateConfig,
onInstall,
@@ -123,7 +123,7 @@ export function CcwToolsMcpCard({
// Local state for config inputs
const [projectRootInput, setProjectRootInput] = useState(projectRoot || '');
const [allowedDirsInput, setAllowedDirsInput] = useState(allowedDirs || '');
const [disableSandboxInput, setDisableSandboxInput] = useState(disableSandbox || false);
const [enableSandboxInput, setEnableSandboxInput] = useState(enableSandbox || false);
const [isExpanded, setIsExpanded] = useState(false);
const [installScope, setInstallScope] = useState<'global' | 'project'>('global');
@@ -193,7 +193,7 @@ export function CcwToolsMcpCard({
updateConfigMutation.mutate({
projectRoot: projectRootInput || undefined,
allowedDirs: allowedDirsInput || undefined,
disableSandbox: disableSandboxInput,
enableSandbox: enableSandboxInput,
});
};
@@ -387,22 +387,22 @@ export function CcwToolsMcpCard({
</p>
</div>
{/* Disable Sandbox */}
{/* Enable Sandbox */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="ccw-disable-sandbox"
checked={disableSandboxInput}
onChange={(e) => setDisableSandboxInput(e.target.checked)}
id="ccw-enable-sandbox"
checked={enableSandboxInput}
onChange={(e) => setEnableSandboxInput(e.target.checked)}
disabled={!isInstalled}
className="w-4 h-4"
/>
<label
htmlFor="ccw-disable-sandbox"
htmlFor="ccw-enable-sandbox"
className="text-sm text-foreground flex items-center gap-1 cursor-pointer"
>
<Shield className="w-4 h-4" />
{formatMessage({ id: 'mcp.ccw.paths.disableSandbox' })}
{formatMessage({ id: 'mcp.ccw.paths.enableSandbox' })}
</label>
</div>

View File

@@ -0,0 +1,133 @@
// ========================================
// QueueExecutionListView Component
// ========================================
// Compact execution list for TerminalPanel queue view.
// Subscribes to queueExecutionStore and renders execution entries
// with status badges, tool/mode labels, and relative timestamps.
// Click navigates to terminal view (session) or orchestrator page.
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { ClipboardList } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import { useQueueExecutionStore } from '@/stores/queueExecutionStore';
import { useTerminalPanelStore } from '@/stores/terminalPanelStore';
import type { QueueExecution } from '@/stores/queueExecutionStore';
import { ROUTES } from '@/router';
import { statusIcon, statusBadgeVariant, formatRelativeTime } from '@/lib/execution-display-utils';
// ========== Execution Item ==========
function QueueExecutionItem({
execution,
onClick,
}: {
execution: QueueExecution;
onClick: () => void;
}) {
const { formatMessage } = useIntl();
const typeLabel = execution.type === 'session'
? formatMessage({ id: 'home.terminalPanel.queueView.session' })
: formatMessage({ id: 'home.terminalPanel.queueView.orchestrator' });
return (
<button
type="button"
className={cn(
'w-full text-left px-3 py-2.5 rounded-md transition-colors',
'hover:bg-muted/60 focus:outline-none focus:ring-1 focus:ring-primary/30'
)}
onClick={onClick}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
{statusIcon(execution.status)}
<span className="text-sm font-medium font-mono text-foreground truncate">
{execution.queueItemId}
</span>
</div>
<Badge variant={statusBadgeVariant(execution.status)} className="shrink-0 text-[10px] px-1.5 py-0">
{formatMessage({ id: `home.terminalPanel.status.${execution.status}` })}
</Badge>
</div>
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-mono">{execution.tool}</span>
<span className="text-border">|</span>
<span>{execution.mode}</span>
<span className="text-border">|</span>
<span>{typeLabel}</span>
<span className="ml-auto shrink-0">{formatRelativeTime(execution.startedAt)}</span>
</div>
</button>
);
}
// ========== Empty State ==========
function QueueEmptyState() {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<ClipboardList className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-sm">{formatMessage({ id: 'home.terminalPanel.queueView.emptyTitle' })}</p>
<p className="text-xs mt-1">{formatMessage({ id: 'home.terminalPanel.queueView.emptyDesc' })}</p>
</div>
</div>
);
}
// ========== Main Component ==========
export function QueueExecutionListView() {
const navigate = useNavigate();
const executions = useQueueExecutionStore((s) => s.executions);
const setPanelView = useTerminalPanelStore((s) => s.setPanelView);
const openTerminal = useTerminalPanelStore((s) => s.openTerminal);
// Sort: running first, then pending, then failed, then completed; within same status by startedAt desc
const sortedExecutions = useMemo(() => {
const all = Object.values(executions);
const statusOrder: Record<string, number> = {
running: 0,
pending: 1,
failed: 2,
completed: 3,
};
return all.sort((a, b) => {
const sa = statusOrder[a.status] ?? 4;
const sb = statusOrder[b.status] ?? 4;
if (sa !== sb) return sa - sb;
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
});
}, [executions]);
const handleClick = (exec: QueueExecution) => {
if (exec.type === 'session' && exec.sessionKey) {
setPanelView('terminal');
openTerminal(exec.sessionKey);
} else {
navigate(ROUTES.ORCHESTRATOR);
}
};
if (sortedExecutions.length === 0) {
return <QueueEmptyState />;
}
return (
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto p-2 space-y-0.5">
{sortedExecutions.map((exec) => (
<QueueExecutionItem
key={exec.id}
execution={exec}
onClick={() => handleClick(exec)}
/>
))}
</div>
);
}

View File

@@ -201,8 +201,9 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
category: 'user',
}, projectPath || undefined);
setPrompt('');
} catch {
// Error shown in terminal output
} catch (err) {
// Error shown in terminal output; log for DevTools debugging
console.error('[TerminalMainArea] executeInCliSession failed:', err);
} finally {
setIsExecuting(false);
}