feat(workflow): add lightweight interactive planning workflow with in-memory execution and code exploration

- Introduced `lite-plan` command for intelligent task analysis and planning.
- Implemented dynamic exploration and clarification phases based on task complexity.
- Added support for auto mode and forced exploration flags.
- Defined output artifacts and session structure for planning results.
- Enhanced execution process with context handoff to `lite-execute`.

chore(temp): create temporary memory content and import script

- Added `.temp-memory-content.txt` to store session details and execution plan.
- Implemented `temp-import-memory.cjs` to handle memory import using core-memory command.
- Ensured cleanup of temporary files after execution.
This commit is contained in:
catlog22
2026-02-27 11:43:44 +08:00
parent 07452e57b7
commit 4d755ff9b4
48 changed files with 5659 additions and 82 deletions

View File

@@ -324,9 +324,16 @@ export function CcwToolsMcpCard({
{/* Tool Checkboxes */}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase">
{formatMessage({ id: 'mcp.ccw.tools.label' })}
</p>
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground uppercase">
{formatMessage({ id: 'mcp.ccw.tools.label' })}
</p>
{!isInstalled && (
<p className="text-xs text-amber-600 dark:text-amber-500">
{formatMessage({ id: 'mcp.ccw.tools.hint' })}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-2">
{CCW_MCP_TOOLS.map((tool) => {
const isEnabled = enabledTools.includes(tool.name);

View File

@@ -181,9 +181,10 @@ export function GlobalSettingsTab() {
updateMutation.mutate({ personalSpecDefaults: newDefaults });
};
// Calculate totals
// Calculate totals - Only include specs and personal dimensions
const dimensions = stats?.dimensions || {};
const dimensionEntries = Object.entries(dimensions) as [
const dimensionEntries = Object.entries(dimensions)
.filter(([dim]) => dim === 'specs' || dim === 'personal') as [
keyof typeof dimensions,
SpecDimensionStats
][];
@@ -304,8 +305,10 @@ export function GlobalSettingsTab() {
</div>
) : (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{dimensionEntries.map(([dim, data]) => (
<div className="grid grid-cols-2 gap-4">
{dimensionEntries
.filter(([entry]) => entry[0] === 'specs' || entry[1] === 'personal')
.map(([dim, data]) => (
<div
key={dim}
className="text-center p-4 rounded-lg bg-muted/50 hover:bg-muted transition-colors"

View File

@@ -43,6 +43,8 @@ import {
Folder,
ChevronDown,
ChevronRight,
Layers,
Filter,
} from 'lucide-react';
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
@@ -83,6 +85,19 @@ export interface InjectionControlTabProps {
className?: string;
}
// ========== Category Configuration ==========
type SpecCategory = 'general' | 'exploration' | 'planning' | 'execution';
const SPEC_CATEGORIES: SpecCategory[] = ['general', 'exploration', 'planning', 'execution'];
const CATEGORY_CONFIG: Record<SpecCategory, { color: string; bgColor: string }> = {
general: { color: 'text-gray-600', bgColor: 'bg-gray-100 dark:bg-gray-800' },
exploration: { color: 'text-blue-600', bgColor: 'bg-blue-100 dark:bg-blue-900/30' },
planning: { color: 'text-purple-600', bgColor: 'bg-purple-100 dark:bg-purple-900/30' },
execution: { color: 'text-green-600', bgColor: 'bg-green-100 dark:bg-green-900/30' },
};
// ========== Recommended Hooks Configuration ==========
const RECOMMENDED_HOOKS = [
@@ -184,6 +199,7 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
// State for injection preview
const [previewMode, setPreviewMode] = useState<'required' | 'all'>('required');
const [categoryFilter, setCategoryFilter] = useState<SpecCategory | 'all'>('all');
const [previewData, setPreviewData] = useState<InjectionPreviewResponse | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
const [expandedDimensions, setExpandedDimensions] = useState<Record<string, boolean>>({
@@ -358,17 +374,36 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
}
}, [installedHookIds, installHooksMutation, formatMessage]);
// Group files by dimension
// Group files by dimension and filter by category
const filesByDimension = useMemo(() => {
if (!previewData) return {};
const grouped: Record<string, InjectionPreviewFile[]> = {};
for (const file of previewData.files) {
// Apply category filter
if (categoryFilter !== 'all' && file.category !== categoryFilter) {
continue;
}
if (!grouped[file.dimension]) {
grouped[file.dimension] = [];
}
grouped[file.dimension].push(file);
}
return grouped;
}, [previewData, categoryFilter]);
// Calculate category statistics
const categoryStats = useMemo(() => {
if (!previewData) {
return { general: 0, exploration: 0, planning: 0, execution: 0 };
}
const stats: Record<SpecCategory, number> = { general: 0, exploration: 0, planning: 0, execution: 0 };
for (const file of previewData.files) {
const cat = (file.category as SpecCategory) || 'general';
if (stats[cat] !== undefined) {
stats[cat]++;
}
}
return stats;
}, [previewData]);
// Calculate progress and status
@@ -641,7 +676,37 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
)}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="space-y-4">
{/* Category Filter */}
<div className="flex items-center gap-2 flex-wrap">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.filterByCategory', defaultMessage: 'Category:' })}
</span>
<div className="flex gap-2 flex-wrap">
<Button
variant={categoryFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter('all')}
>
{formatMessage({ id: 'specs.category.all', defaultMessage: 'All' })}
</Button>
{SPEC_CATEGORIES.map(cat => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
className="gap-1"
>
<Layers className="h-3 w-3" />
{formatMessage({ id: `specs.category.${cat}`, defaultMessage: cat })} ({categoryStats[cat]})
</Button>
))}
</div>
</div>
{/* Files List */}
{previewLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@@ -669,45 +734,53 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
</Badge>
</div>
<span className="text-sm text-muted-foreground">
{formatNumber(files.reduce((sum, f) => sum + f.contentLength, 0))} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'chars' })}
{formatNumber(files.reduce((sum, f) => sum + f.contentLength, 0))} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
</span>
</button>
{expandedDimensions[dim] && (
<div className="border-t">
{files.map((file) => (
<div
key={file.file}
className="flex items-center justify-between p-3 border-b last:border-b-0 hover:bg-muted/30"
>
<div className="flex items-center gap-3 min-w-0">
{file.scope === 'global' ? (
<Globe className="h-4 w-4 text-blue-500 flex-shrink-0" />
) : (
<Folder className="h-4 w-4 text-green-500 flex-shrink-0" />
)}
<div className="min-w-0">
<div className="font-medium truncate">{file.title}</div>
<div className="text-xs text-muted-foreground truncate">{file.file}</div>
{files.map((file) => {
const fileCategory = (file.category as SpecCategory) || 'general';
const categoryStyle = CATEGORY_CONFIG[fileCategory] || CATEGORY_CONFIG.general;
return (
<div
key={file.file}
className="flex items-center justify-between p-3 border-b last:border-b-0 hover:bg-muted/30"
>
<div className="flex items-center gap-3 min-w-0">
{file.scope === 'global' ? (
<Globe className="h-4 w-4 text-blue-500 flex-shrink-0" />
) : (
<Folder className="h-4 w-4 text-green-500 flex-shrink-0" />
)}
<div className="min-w-0">
<div className="font-medium truncate">{file.title}</div>
<div className="text-xs text-muted-foreground truncate">{file.file}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={cn('text-xs gap-1', categoryStyle.bgColor, categoryStyle.color)}>
<Layers className="h-3 w-3" />
{formatMessage({ id: `specs.category.${fileCategory}`, defaultMessage: fileCategory })}
</Badge>
<Badge variant="outline" className="text-xs">
{formatMessage({ id: `specs.priority.${file.priority}`, defaultMessage: file.priority })}
</Badge>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatNumber(file.contentLength)}
</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadFilePreview(file)}
>
<Eye className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{formatMessage({ id: `specs.priority.${file.priority}`, defaultMessage: file.priority })}
</Badge>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatNumber(file.contentLength)}
</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadFilePreview(file)}
>
<Eye className="h-3.5 w-3.5" />
</Button>
</div>
</div>
))}
);
})}
</div>
)}
</div>

View File

@@ -73,11 +73,14 @@ function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
function StatusDot({ status }: { status: Issue['status'] }) {
const colorMap: Record<Issue['status'], string> = {
open: 'text-info',
in_progress: 'text-warning',
resolved: 'text-success',
closed: 'text-muted-foreground',
registered: 'text-info',
planning: 'text-blue-500',
planned: 'text-blue-600',
queued: 'text-yellow-500',
executing: 'text-warning',
completed: 'text-success',
failed: 'text-destructive',
paused: 'text-muted-foreground',
};
return <CircleDot className={cn('w-3 h-3 shrink-0', colorMap[status] ?? 'text-muted-foreground')} />;
}

View File

@@ -112,6 +112,7 @@
},
"tools": {
"label": "Available Tools",
"hint": "Install CCW MCP first to select tools",
"core": "Core",
"write_file": {
"name": "write_file",

View File

@@ -112,6 +112,7 @@
},
"tools": {
"label": "可用工具",
"hint": "请先安装 CCW MCP 后再选择工具",
"core": "核心",
"write_file": {
"name": "write_file",

View File

@@ -138,7 +138,7 @@ function EditIssueDialog({ open, onOpenChange, issue, onSubmit, isUpdating }: Ed
const [title, setTitle] = useState('');
const [context, setContext] = useState('');
const [priority, setPriority] = useState<Issue['priority']>('medium');
const [status, setStatus] = useState<Issue['status']>('open');
const [status, setStatus] = useState<Issue['status']>('registered');
// Reset form when dialog opens or issue changes
useEffect(() => {
@@ -146,12 +146,12 @@ function EditIssueDialog({ open, onOpenChange, issue, onSubmit, isUpdating }: Ed
setTitle(issue.title ?? '');
setContext(issue.context ?? '');
setPriority(issue.priority ?? 'medium');
setStatus(issue.status ?? 'open');
setStatus(issue.status ?? 'registered');
} else if (!open) {
setTitle('');
setContext('');
setPriority('medium');
setStatus('open');
setStatus('registered');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, issue?.id]);
@@ -217,10 +217,11 @@ function EditIssueDialog({ open, onOpenChange, issue, onSubmit, isUpdating }: Ed
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">{formatMessage({ id: 'issues.status.open' })}</SelectItem>
<SelectItem value="in_progress">{formatMessage({ id: 'issues.status.inProgress' })}</SelectItem>
<SelectItem value="resolved">{formatMessage({ id: 'issues.status.resolved' })}</SelectItem>
<SelectItem value="closed">{formatMessage({ id: 'issues.status.closed' })}</SelectItem>
<SelectItem value="registered">{formatMessage({ id: 'issues.status.registered', defaultMessage: 'Registered' })}</SelectItem>
<SelectItem value="planning">{formatMessage({ id: 'issues.status.planning', defaultMessage: 'Planning' })}</SelectItem>
<SelectItem value="planned">{formatMessage({ id: 'issues.status.planned', defaultMessage: 'Planned' })}</SelectItem>
<SelectItem value="queued">{formatMessage({ id: 'issues.status.queued', defaultMessage: 'Queued' })}</SelectItem>
<SelectItem value="executing">{formatMessage({ id: 'issues.status.executing', defaultMessage: 'Executing' })}</SelectItem>
<SelectItem value="completed">{formatMessage({ id: 'issues.status.completed' })}</SelectItem>
</SelectContent>
</Select>
@@ -341,11 +342,14 @@ export function IssueManagerPage() {
// Filter counts
const statusCounts = useMemo(() => ({
all: issues.length,
open: issuesByStatus.open?.length || 0,
in_progress: issuesByStatus.in_progress?.length || 0,
resolved: issuesByStatus.resolved?.length || 0,
closed: issuesByStatus.closed?.length || 0,
registered: issuesByStatus.registered?.length || 0,
planning: issuesByStatus.planning?.length || 0,
planned: issuesByStatus.planned?.length || 0,
queued: issuesByStatus.queued?.length || 0,
executing: issuesByStatus.executing?.length || 0,
completed: issuesByStatus.completed?.length || 0,
failed: issuesByStatus.failed?.length || 0,
paused: issuesByStatus.paused?.length || 0,
}), [issues, issuesByStatus]);
const handleCreateIssue = async (data: { title: string; context?: string; priority?: Issue['priority'] }) => {
@@ -440,9 +444,9 @@ export function IssueManagerPage() {
<Card className="p-4">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-warning" />
<span className="text-2xl font-bold">{issuesByStatus.in_progress?.length || 0}</span>
<span className="text-2xl font-bold">{issuesByStatus.executing?.length || 0}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.inProgress' })}</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.executing', defaultMessage: 'Executing' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
@@ -454,9 +458,9 @@ export function IssueManagerPage() {
<Card className="p-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-success" />
<span className="text-2xl font-bold">{issuesByStatus.resolved?.length || 0}</span>
<span className="text-2xl font-bold">{issuesByStatus.completed?.length || 0}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.resolved' })}</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.completed' })}</p>
</Card>
</div>
@@ -478,10 +482,12 @@ export function IssueManagerPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{formatMessage({ id: 'issues.filters.all' })}</SelectItem>
<SelectItem value="open">{formatMessage({ id: 'issues.status.open' })}</SelectItem>
<SelectItem value="in_progress">{formatMessage({ id: 'issues.status.inProgress' })}</SelectItem>
<SelectItem value="resolved">{formatMessage({ id: 'issues.status.resolved' })}</SelectItem>
<SelectItem value="closed">{formatMessage({ id: 'issues.status.closed' })}</SelectItem>
<SelectItem value="registered">{formatMessage({ id: 'issues.status.registered', defaultMessage: 'Registered' })}</SelectItem>
<SelectItem value="planning">{formatMessage({ id: 'issues.status.planning', defaultMessage: 'Planning' })}</SelectItem>
<SelectItem value="planned">{formatMessage({ id: 'issues.status.planned', defaultMessage: 'Planned' })}</SelectItem>
<SelectItem value="queued">{formatMessage({ id: 'issues.status.queued', defaultMessage: 'Queued' })}</SelectItem>
<SelectItem value="executing">{formatMessage({ id: 'issues.status.executing', defaultMessage: 'Executing' })}</SelectItem>
<SelectItem value="completed">{formatMessage({ id: 'issues.status.completed' })}</SelectItem>
</SelectContent>
</Select>
<Select value={priorityFilter} onValueChange={(v) => setPriorityFilter(v as PriorityFilter)}>
@@ -509,20 +515,20 @@ export function IssueManagerPage() {
{formatMessage({ id: 'issues.filters.all' })} ({statusCounts.all})
</Button>
<Button
variant={statusFilter === 'open' ? 'default' : 'outline'}
variant={statusFilter === 'registered' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('open')}
onClick={() => setStatusFilter('registered')}
>
<Badge variant="info" className="mr-2">{statusCounts.open}</Badge>
{formatMessage({ id: 'issues.status.open' })}
<Badge variant="info" className="mr-2">{statusCounts.registered}</Badge>
{formatMessage({ id: 'issues.status.registered', defaultMessage: 'Registered' })}
</Button>
<Button
variant={statusFilter === 'in_progress' ? 'default' : 'outline'}
variant={statusFilter === 'executing' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('in_progress')}
onClick={() => setStatusFilter('executing')}
>
<Badge variant="warning" className="mr-2">{statusCounts.in_progress}</Badge>
{formatMessage({ id: 'issues.status.inProgress' })}
<Badge variant="warning" className="mr-2">{statusCounts.executing}</Badge>
{formatMessage({ id: 'issues.status.executing', defaultMessage: 'Executing' })}
</Button>
<Button
variant={priorityFilter === 'critical' ? 'destructive' : 'outline'}

View File

@@ -260,10 +260,12 @@ export function SpecsSettingsPage() {
</div>
</div>
{/* Stats Summary */}
{/* Stats Summary - Only show specs and personal dimensions */}
{statsData?.dimensions && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(statsData.dimensions).map(([dim, data]) => (
<div className="grid grid-cols-2 gap-4">
{Object.entries(statsData.dimensions)
.filter(([dim]) => dim === 'specs' || dim === 'personal')
.map(([dim, data]) => (
<Card key={dim}>
<CardContent className="pt-4">
<div className="text-sm text-muted-foreground">

View File

@@ -3,6 +3,10 @@ import { launchBrowser } from '../utils/browser-launcher.js';
import { validatePath } from '../utils/path-resolver.js';
import { startReactFrontend, stopReactFrontend } from '../utils/react-frontend.js';
import chalk from 'chalk';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
interface ServeOptions {
port?: number;
@@ -11,6 +15,36 @@ interface ServeOptions {
browser?: boolean;
}
/**
* Check if a port is in use
* @param port - Port number to check
* @returns Promise<boolean> - true if port is in use
*/
async function isPortInUse(port: number): Promise<boolean> {
try {
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 4) continue;
const proto = parts[0]?.toUpperCase();
const localAddress = parts[1] || '';
const state = parts[3]?.toUpperCase();
// Check if this is a TCP connection in LISTENING state on our port
if (proto === 'TCP' && localAddress.endsWith(`:${port}`) && state === 'LISTENING') {
return true;
}
}
return false;
} catch {
// If netstat fails or no matches found, assume port is free
return false;
}
}
/**
* Serve command handler - starts dashboard server with live path switching
* @param {Object} options - Command options
@@ -33,13 +67,36 @@ export async function serveCommand(options: ServeOptions): Promise<void> {
initialPath = pathValidation.path;
}
const startupId = Math.random().toString(36).substring(7);
console.log(chalk.blue.bold('\n CCW Dashboard Server\n'));
console.log(chalk.gray(` Startup ID: ${startupId}`));
console.log(chalk.gray(` Initial project: ${initialPath}`));
console.log(chalk.gray(` Host: ${host}`));
console.log(chalk.gray(` Port: ${port}\n`));
// Start React frontend
// Calculate React frontend port
const reactPort = port + 1;
// Check if ports are already in use
const mainPortInUse = await isPortInUse(port);
const reactPortInUse = await isPortInUse(reactPort);
if (mainPortInUse) {
console.error(chalk.red(`\n Error: Port ${port} is already in use.`));
console.error(chalk.yellow(` Another CCW server may be running.`));
console.error(chalk.gray(` Try stopping it first: ccw stop`));
console.error(chalk.gray(` Or use a different port: ccw serve --port ${port + 2}\n`));
process.exit(1);
}
if (reactPortInUse) {
console.error(chalk.red(`\n Error: Port ${reactPort} (React frontend) is already in use.`));
console.error(chalk.yellow(` Another process may be using this port.`));
console.error(chalk.gray(` Try using a different port: ccw serve --port ${port + 2}\n`));
process.exit(1);
}
// Start React frontend
try {
await startReactFrontend(reactPort);
} catch (error) {

View File

@@ -32,7 +32,8 @@ async function findProcessOnPort(port: number): Promise<string | null> {
if (proto !== 'TCP') continue;
if (!localAddress.endsWith(`:${port}`)) continue;
if (!/^\d+$/.test(pidCandidate)) continue;
// Reject PID 0 (System Idle Process) and non-numeric PIDs
if (!/^[1-9]\d*$/.test(pidCandidate)) continue;
return pidCandidate; // PID is the last column
}
@@ -43,7 +44,8 @@ async function findProcessOnPort(port: number): Promise<string | null> {
}
async function getProcessCommandLine(pid: string): Promise<string | null> {
if (!/^\d+$/.test(pid)) return null;
// Reject PID 0 (System Idle Process) and non-numeric PIDs
if (!/^[1-9]\d*$/.test(pid)) return null;
try {
const probeCommand =
@@ -78,7 +80,8 @@ function isLikelyViteCommandLine(commandLine: string, port: number): boolean {
* @returns {Promise<boolean>} Success status
*/
async function killProcess(pid: string): Promise<boolean> {
if (!/^\d+$/.test(pid)) return false;
// Reject PID 0 (System Idle Process) and non-numeric PIDs
if (!/^[1-9]\d*$/.test(pid)) return false;
try {
// Prefer taskkill to terminate the entire process tree on Windows (npm/cmd wrappers can orphan children).