mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add templates for epics, product brief, and requirements PRD
- Created a new directory structure for epics and stories with templates for individual epics and an index file. - Added a product brief template for generating product brief documents in Phase 2. - Introduced a requirements PRD template for generating a Product Requirements Document as a directory of individual requirement files in Phase 3. feat: implement V2PipelineTab component for Memory V2 management - Developed the V2PipelineTab component to manage extraction and consolidation processes. - Included ExtractionCard and ConsolidationCard components to handle respective functionalities. - Added JobsList component to display job statuses and allow filtering by job kind. feat: create hooks for Memory V2 pipeline - Implemented custom hooks for managing extraction and consolidation statuses, as well as job listings. - Added mutation hooks to trigger extraction and consolidation processes with automatic query invalidation on success.
This commit is contained in:
@@ -191,6 +191,41 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
trigger: 'SessionStart',
|
||||
command: 'ccw',
|
||||
args: ['hook', 'project-state', '--stdin']
|
||||
},
|
||||
// --- Memory V2 ---
|
||||
{
|
||||
id: 'memory-v2-extract',
|
||||
name: 'Memory V2 Extract',
|
||||
description: 'Trigger Phase 1 extraction when session ends (after idle period)',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'ccw',
|
||||
args: ['core-memory', 'extract', '--max-sessions', '10']
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-auto-consolidate',
|
||||
name: 'Memory V2 Auto Consolidate',
|
||||
description: 'Trigger Phase 2 consolidation after extraction jobs complete',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const r=cp.spawnSync("ccw",["core-memory","extract","--json"],{encoding:"utf8",shell:true});try{const d=JSON.parse(r.stdout);if(d&&d.total_stage1>=5){cp.spawnSync("ccw",["core-memory","consolidate"],{stdio:"inherit",shell:true})}}catch(e){}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'memory-sync-dashboard',
|
||||
name: 'Memory Sync Dashboard',
|
||||
description: 'Sync memory V2 status to dashboard on changes',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'core_memory',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const payload=JSON.stringify({type:"MEMORY_V2_STATUS_UPDATED",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
}
|
||||
] as const;
|
||||
|
||||
|
||||
351
ccw/frontend/src/components/memory/V2PipelineTab.tsx
Normal file
351
ccw/frontend/src/components/memory/V2PipelineTab.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
// ========================================
|
||||
// V2PipelineTab Component
|
||||
// ========================================
|
||||
// Memory V2 Pipeline management UI
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Zap,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
Play,
|
||||
Eye,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
FileText,
|
||||
Database,
|
||||
Activity,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import {
|
||||
useExtractionStatus,
|
||||
useConsolidationStatus,
|
||||
useV2Jobs,
|
||||
useTriggerExtraction,
|
||||
useTriggerConsolidation,
|
||||
} from '@/hooks/useMemoryV2';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Status Badge ==========
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
|
||||
idle: { color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300', icon: <Clock className="w-3 h-3" />, label: 'Idle' },
|
||||
running: { color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', icon: <Loader2 className="w-3 h-3 animate-spin" />, label: 'Running' },
|
||||
completed: { color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', icon: <CheckCircle className="w-3 h-3" />, label: 'Completed' },
|
||||
done: { color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', icon: <CheckCircle className="w-3 h-3" />, label: 'Done' },
|
||||
error: { color: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', icon: <AlertCircle className="w-3 h-3" />, label: 'Error' },
|
||||
pending: { color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', icon: <Clock className="w-3 h-3" />, label: 'Pending' },
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const config = STATUS_CONFIG[status] || STATUS_CONFIG.idle;
|
||||
return (
|
||||
<Badge className={cn('flex items-center gap-1', config.color)}>
|
||||
{config.icon}
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Extraction Card ==========
|
||||
|
||||
function ExtractionCard() {
|
||||
const intl = useIntl();
|
||||
const { data: status, isLoading, refetch } = useExtractionStatus();
|
||||
const trigger = useTriggerExtraction();
|
||||
const [maxSessions, setMaxSessions] = useState(10);
|
||||
|
||||
const handleTrigger = () => {
|
||||
trigger.mutate(maxSessions);
|
||||
};
|
||||
|
||||
// Check if any job is running
|
||||
const hasRunningJob = status?.jobs?.some(j => j.status === 'running');
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-yellow-500" />
|
||||
Phase 1: {intl.formatMessage({ id: 'memory.v2.extraction.title', defaultMessage: 'Extraction' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.description', defaultMessage: 'Extract structured memories from CLI sessions' })}
|
||||
</p>
|
||||
</div>
|
||||
{status && (
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">{status.total_stage1}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.extracted', defaultMessage: 'Extracted' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<input
|
||||
type="number"
|
||||
value={maxSessions}
|
||||
onChange={(e) => setMaxSessions(Math.max(1, parseInt(e.target.value) || 10))}
|
||||
className="w-20 px-2 py-1 text-sm border rounded bg-background"
|
||||
min={1}
|
||||
max={64}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">sessions max</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleTrigger}
|
||||
disabled={trigger.isPending || hasRunningJob}
|
||||
size="sm"
|
||||
>
|
||||
{trigger.isPending || hasRunningJob ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.extracting', defaultMessage: 'Extracting...' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.trigger', defaultMessage: 'Trigger Extraction' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{status?.jobs && status.jobs.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.recentJobs', defaultMessage: 'Recent Jobs' })}
|
||||
</div>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{status.jobs.slice(0, 5).map((job) => (
|
||||
<div key={job.job_key} className="flex items-center justify-between text-sm">
|
||||
<span className="font-mono text-xs truncate max-w-[150px]">{job.job_key}</span>
|
||||
<StatusBadge status={job.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Consolidation Card ==========
|
||||
|
||||
function ConsolidationCard() {
|
||||
const intl = useIntl();
|
||||
const { data: status, isLoading, refetch } = useConsolidationStatus();
|
||||
const trigger = useTriggerConsolidation();
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
const handleTrigger = () => {
|
||||
trigger.mutate();
|
||||
};
|
||||
|
||||
const isRunning = status?.status === 'running';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Database className="w-5 h-5 text-blue-500" />
|
||||
Phase 2: {intl.formatMessage({ id: 'memory.v2.consolidation.title', defaultMessage: 'Consolidation' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.description', defaultMessage: 'Merge extracted results into MEMORY.md' })}
|
||||
</p>
|
||||
</div>
|
||||
{status && <StatusBadge status={status.status} />}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="text-lg font-bold">
|
||||
{status?.memoryMdAvailable ? '✅' : '❌'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">MEMORY.md</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="text-lg font-bold">{status?.inputCount ?? '-'}</div>
|
||||
<div className="text-xs text-muted-foreground">Inputs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status?.lastError && (
|
||||
<div className="mb-4 p-2 bg-red-100 dark:bg-red-900/30 rounded text-xs text-red-800 dark:text-red-300">
|
||||
{status.lastError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleTrigger}
|
||||
disabled={trigger.isPending || isRunning}
|
||||
size="sm"
|
||||
>
|
||||
{trigger.isPending || isRunning ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.consolidating', defaultMessage: 'Consolidating...' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.trigger', defaultMessage: 'Trigger Consolidation' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{status?.memoryMdAvailable && (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowPreview(true)}>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.preview', defaultMessage: 'Preview' })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* MEMORY.md Preview Dialog */}
|
||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
MEMORY.md
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto flex-1">
|
||||
<pre className="text-sm whitespace-pre-wrap p-4 bg-muted rounded font-mono">
|
||||
{status?.memoryMdPreview || 'No content available'}
|
||||
</pre>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Jobs List ==========
|
||||
|
||||
function JobsList() {
|
||||
const intl = useIntl();
|
||||
const [kindFilter, setKindFilter] = useState<string>('');
|
||||
const { data, isLoading, refetch } = useV2Jobs(kindFilter ? { kind: kindFilter } : undefined);
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-purple-500" />
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.title', defaultMessage: 'Jobs' })}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={kindFilter}
|
||||
onChange={(e) => setKindFilter(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded bg-background"
|
||||
>
|
||||
<option value="">All Kinds</option>
|
||||
<option value="phase1_extraction">Extraction</option>
|
||||
<option value="memory_consolidate_global">Consolidation</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.jobs && data.jobs.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left p-2">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.kind', defaultMessage: 'Kind' })}
|
||||
</th>
|
||||
<th className="text-left p-2">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.key', defaultMessage: 'Key' })}
|
||||
</th>
|
||||
<th className="text-left p-2">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.status', defaultMessage: 'Status' })}
|
||||
</th>
|
||||
<th className="text-left p-2">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.error', defaultMessage: 'Error' })}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.jobs.map((job) => (
|
||||
<tr key={`${job.kind}-${job.job_key}`} className="border-b">
|
||||
<td className="p-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{job.kind === 'phase1_extraction' ? 'Extraction' :
|
||||
job.kind === 'memory_consolidate_global' ? 'Consolidation' : job.kind}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-2 font-mono text-xs truncate max-w-[150px]">{job.job_key}</td>
|
||||
<td className="p-2"><StatusBadge status={job.status} /></td>
|
||||
<td className="p-2 text-red-500 text-xs truncate max-w-[200px]">{job.last_error || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.noJobs', defaultMessage: 'No jobs found' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按状态统计 */}
|
||||
{data?.byStatus && Object.keys(data.byStatus).length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t flex items-center gap-4 text-sm flex-wrap">
|
||||
{Object.entries(data.byStatus).map(([status, count]) => (
|
||||
<span key={status} className="flex items-center gap-1">
|
||||
<StatusBadge status={status} />
|
||||
<span className="font-bold">{count}</span>
|
||||
</span>
|
||||
))}
|
||||
<span className="text-muted-foreground ml-auto">
|
||||
Total: {data.total}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function V2PipelineTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ExtractionCard />
|
||||
<ConsolidationCard />
|
||||
</div>
|
||||
<JobsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default V2PipelineTab;
|
||||
@@ -187,10 +187,7 @@ export function GlobalSettingsTab() {
|
||||
});
|
||||
|
||||
// Fetch project-tech stats
|
||||
const {
|
||||
data: projectTechStats,
|
||||
isLoading: isLoadingProjectTech,
|
||||
} = useQuery({
|
||||
const { data: projectTechStats } = useQuery({
|
||||
queryKey: settingsKeys.projectTech(),
|
||||
queryFn: fetchProjectTechStats,
|
||||
staleTime: 60000, // 1 minute
|
||||
@@ -490,7 +487,7 @@ export function GlobalSettingsTab() {
|
||||
)}
|
||||
onClick={() => localDevProgress.enabled && handleCategoryToggle(cat)}
|
||||
>
|
||||
{cat} ({projectTechStats?.categories[cat] || 0})
|
||||
{formatMessage({ id: `specs.devCategory.${cat}`, defaultMessage: cat })} ({projectTechStats?.categories[cat] || 0})
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
|
||||
import { getInjectionPreview } from '@/lib/api';
|
||||
import { getInjectionPreview, COMMAND_PREVIEWS, type CommandPreviewConfig } from '@/lib/api';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -209,6 +209,12 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<InjectionPreviewFile | null>(null);
|
||||
|
||||
// State for command preview
|
||||
const [selectedCommand, setSelectedCommand] = useState<CommandPreviewConfig>(COMMAND_PREVIEWS[0]);
|
||||
const [commandPreviewData, setCommandPreviewData] = useState<InjectionPreviewResponse | null>(null);
|
||||
const [commandPreviewLoading, setCommandPreviewLoading] = useState(false);
|
||||
const [commandPreviewDialogOpen, setCommandPreviewDialogOpen] = useState(false);
|
||||
|
||||
// Fetch stats
|
||||
const loadStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
@@ -264,6 +270,21 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
}
|
||||
}, [previewMode]);
|
||||
|
||||
// Load command preview content
|
||||
const loadCommandPreview = useCallback(async (command: CommandPreviewConfig) => {
|
||||
setCommandPreviewLoading(true);
|
||||
try {
|
||||
const data = await getInjectionPreview(command.mode, true, undefined, command.category);
|
||||
setCommandPreviewData(data);
|
||||
setSelectedCommand(command);
|
||||
setCommandPreviewDialogOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to load command preview:', err);
|
||||
} finally {
|
||||
setCommandPreviewLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
@@ -406,15 +427,20 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
return stats;
|
||||
}, [previewData]);
|
||||
|
||||
// Calculate progress and status
|
||||
// Calculate progress and status - use API's maxLength for consistency
|
||||
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
||||
const maxLength = settings.maxLength;
|
||||
const apiMaxLength = stats?.injectionLength?.maxLength || settings.maxLength;
|
||||
const maxLength = apiMaxLength; // Use API's maxLength for consistency
|
||||
const warnThreshold = settings.warnThreshold;
|
||||
const percentage = calculatePercentage(currentLength, maxLength);
|
||||
const isOverLimit = currentLength > maxLength;
|
||||
const isOverWarning = currentLength > warnThreshold;
|
||||
const remainingSpace = Math.max(0, maxLength - currentLength);
|
||||
|
||||
// Calculate approximate line count (assuming ~80 chars per line)
|
||||
const estimatedLineCount = Math.ceil(currentLength / 80);
|
||||
const maxLineCount = Math.ceil(maxLength / 80);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-6', className)}>
|
||||
{/* Recommended Hooks Section */}
|
||||
@@ -556,36 +582,40 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
>
|
||||
{formatNumber(currentLength)} / {formatNumber(maxLength)}{' '}
|
||||
{formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
<span className="text-muted-foreground ml-2">
|
||||
(~{formatNumber(estimatedLineCount)} / {formatNumber(maxLineCount)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={percentage}
|
||||
className={cn(
|
||||
'h-3',
|
||||
isOverLimit && 'bg-destructive/20',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-100 dark:bg-yellow-900/30'
|
||||
)}
|
||||
indicatorClassName={cn(
|
||||
isOverLimit && 'bg-destructive',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Warning threshold marker */}
|
||||
<div
|
||||
className="relative h-0"
|
||||
style={{
|
||||
left: `${Math.min(100, (warnThreshold / maxLength) * 100)}%`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute -top-5 transform -translate-x-1/2 flex flex-col items-center">
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-500" />
|
||||
<div className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{formatMessage({ id: 'specs.injection.warnThreshold', defaultMessage: 'Warn' })}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
|
||||
<span>{percentage}%</span>
|
||||
<span>{formatMessage({ id: 'specs.injection.maxLimit', defaultMessage: 'Max' })}: {formatNumber(maxLength)}</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Progress
|
||||
value={percentage}
|
||||
className={cn(
|
||||
'h-3',
|
||||
isOverLimit && 'bg-destructive/20',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-100 dark:bg-yellow-900/30'
|
||||
)}
|
||||
indicatorClassName={cn(
|
||||
isOverLimit && 'bg-destructive',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
{/* Warning threshold marker */}
|
||||
<div
|
||||
className="absolute top-0 h-3 flex flex-col items-center"
|
||||
style={{
|
||||
left: `${Math.min(100, (warnThreshold / maxLength) * 100)}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-500 -mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -791,6 +821,59 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Command Preview Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5" />
|
||||
{formatMessage({ id: 'specs.injection.commandPreview', defaultMessage: 'Command Injection Preview' })}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{formatMessage({
|
||||
id: 'specs.injection.commandPreviewDesc',
|
||||
defaultMessage: 'Preview the content that would be injected by different CLI commands',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
{COMMAND_PREVIEWS.map((cmd) => (
|
||||
<Button
|
||||
key={cmd.command}
|
||||
variant="outline"
|
||||
className="h-auto flex-col items-start py-3 px-4"
|
||||
onClick={() => loadCommandPreview(cmd)}
|
||||
disabled={commandPreviewLoading}
|
||||
>
|
||||
<div className="font-medium text-sm">
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${cmd.labelKey}.label`,
|
||||
defaultMessage: cmd.labelKey
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${cmd.descriptionKey}.description`,
|
||||
defaultMessage: cmd.descriptionKey
|
||||
})}
|
||||
</div>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded mt-2 w-full text-center">
|
||||
{cmd.command.replace('ccw spec load', '').trim() || 'default'}
|
||||
</code>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{commandPreviewLoading && (
|
||||
<div className="flex items-center justify-center py-4 mt-4 border rounded-lg">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.loadingPreview', defaultMessage: 'Loading preview...' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Settings Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -814,39 +897,83 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
{/* Max Injection Length */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxLength">
|
||||
{formatMessage({ id: 'specs.injection.maxLength', defaultMessage: 'Max Injection Length (characters)' })}
|
||||
{formatMessage({ id: 'specs.injection.maxLength', defaultMessage: 'Max Injection Length' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="maxLength"
|
||||
type="number"
|
||||
min={1000}
|
||||
max={50000}
|
||||
step={500}
|
||||
value={formData.maxLength}
|
||||
onChange={(e) => handleFieldChange('maxLength', Number(e.target.value))}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="maxLength"
|
||||
type="number"
|
||||
min={1000}
|
||||
max={50000}
|
||||
step={500}
|
||||
value={formData.maxLength}
|
||||
onChange={(e) => handleFieldChange('maxLength', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
(~{Math.ceil(formData.maxLength / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'specs.injection.maxLengthHelp',
|
||||
defaultMessage: 'Recommended: 4000-10000. Too large may consume too much context; too small may truncate important specs.',
|
||||
defaultMessage: 'Recommended: 4000-10000 characters (50-125 lines). Too large may consume too much context.',
|
||||
})}
|
||||
</p>
|
||||
{/* Quick presets */}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.quickPresets', defaultMessage: 'Quick presets:' })}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 4000)}
|
||||
>
|
||||
4000 (50 lines)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 8000)}
|
||||
>
|
||||
8000 (100 lines)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 12000)}
|
||||
>
|
||||
12000 (150 lines)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning Threshold */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="warnThreshold">
|
||||
{formatMessage({ id: 'specs.injection.warnThresholdLabel', defaultMessage: 'Warning Threshold (characters)' })}
|
||||
{formatMessage({ id: 'specs.injection.warnThresholdLabel', defaultMessage: 'Warning Threshold' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="warnThreshold"
|
||||
type="number"
|
||||
min={500}
|
||||
max={formData.maxLength - 1}
|
||||
step={500}
|
||||
value={formData.warnThreshold}
|
||||
onChange={(e) => handleFieldChange('warnThreshold', Number(e.target.value))}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="warnThreshold"
|
||||
type="number"
|
||||
min={500}
|
||||
max={formData.maxLength - 1}
|
||||
step={500}
|
||||
value={formData.warnThreshold}
|
||||
onChange={(e) => handleFieldChange('warnThreshold', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
(~{Math.ceil(formData.warnThreshold / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'specs.injection.warnThresholdHelp',
|
||||
@@ -916,6 +1043,84 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Command Preview Dialog */}
|
||||
<Dialog open={commandPreviewDialogOpen} onOpenChange={setCommandPreviewDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5" />
|
||||
{formatMessage({ id: 'specs.injection.previewTitle', defaultMessage: 'Injection Preview' })}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<code className="bg-muted px-2 py-1 rounded text-xs">{selectedCommand.command}</code>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${selectedCommand.descriptionKey}.description`,
|
||||
defaultMessage: selectedCommand.descriptionKey
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{/* Stats */}
|
||||
{commandPreviewData && (
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<Badge variant="secondary">
|
||||
{commandPreviewData.stats.count} {formatMessage({ id: 'specs.injection.files', defaultMessage: 'files' })}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
{formatNumber(commandPreviewData.stats.totalLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
~{Math.ceil(commandPreviewData.stats.totalLength / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{/* Preview Content */}
|
||||
<div className="flex-1 overflow-auto max-h-[60vh] border rounded-lg">
|
||||
{commandPreviewData?.files.length ? (
|
||||
<div className="space-y-4 p-4">
|
||||
{commandPreviewData.files.map((file, idx) => (
|
||||
<div key={file.file} className="space-y-2">
|
||||
{/* File Header */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{idx + 1}
|
||||
</Badge>
|
||||
<span className="font-medium">{file.title}</span>
|
||||
{file.category && (
|
||||
<Badge variant="outline" className={cn(
|
||||
'text-xs',
|
||||
CATEGORY_CONFIG[file.category as SpecCategory]?.bgColor,
|
||||
CATEGORY_CONFIG[file.category as SpecCategory]?.color
|
||||
)}>
|
||||
{file.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatNumber(file.contentLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* File Content */}
|
||||
<pre className="text-xs whitespace-pre-wrap p-3 bg-muted rounded border-l-2 border-primary/30">
|
||||
{file.content || formatMessage({ id: 'specs.content.noContent', defaultMessage: 'No content available' })}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.noFiles', defaultMessage: 'No files match this command' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,21 +7,26 @@ const Progress = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
indicatorClassName?: string;
|
||||
}
|
||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
>(({ className, value, indicatorClassName, ...props }, ref) => {
|
||||
// Ensure value is a valid number between 0-100
|
||||
const safeValue = Math.max(0, Math.min(100, Number(value) || 0));
|
||||
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||
style={{ transform: `translateX(-${100 - safeValue}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
});
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
|
||||
@@ -161,6 +161,21 @@ export type {
|
||||
UseReindexReturn,
|
||||
} from './useUnifiedSearch';
|
||||
|
||||
// ========== Memory V2 Pipeline ==========
|
||||
export {
|
||||
useExtractionStatus,
|
||||
useConsolidationStatus,
|
||||
useV2Jobs,
|
||||
useTriggerExtraction,
|
||||
useTriggerConsolidation,
|
||||
memoryV2Keys,
|
||||
} from './useMemoryV2';
|
||||
export type {
|
||||
ExtractionStatus,
|
||||
ConsolidationStatus,
|
||||
V2JobsResponse,
|
||||
} from './useMemoryV2';
|
||||
|
||||
// ========== MCP Servers ==========
|
||||
export {
|
||||
useMcpServers,
|
||||
|
||||
101
ccw/frontend/src/hooks/useMemoryV2.ts
Normal file
101
ccw/frontend/src/hooks/useMemoryV2.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// ========================================
|
||||
// useMemoryV2 Hook
|
||||
// ========================================
|
||||
// TanStack Query hooks for Memory V2 pipeline
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
triggerExtraction,
|
||||
getExtractionStatus,
|
||||
triggerConsolidation,
|
||||
getConsolidationStatus,
|
||||
getV2Jobs,
|
||||
type ExtractionStatus,
|
||||
type ConsolidationStatus,
|
||||
type V2JobsResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query keys
|
||||
export const memoryV2Keys = {
|
||||
all: ['memoryV2'] as const,
|
||||
extractionStatus: (path?: string) => [...memoryV2Keys.all, 'extraction', path] as const,
|
||||
consolidationStatus: (path?: string) => [...memoryV2Keys.all, 'consolidation', path] as const,
|
||||
jobs: (path?: string, filters?: { kind?: string; status_filter?: string }) =>
|
||||
[...memoryV2Keys.all, 'jobs', path, filters] as const,
|
||||
};
|
||||
|
||||
// Default stale time: 30 seconds (V2 status changes frequently)
|
||||
const STALE_TIME = 30 * 1000;
|
||||
|
||||
// Hook: 提取状态
|
||||
export function useExtractionStatus() {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
return useQuery({
|
||||
queryKey: memoryV2Keys.extractionStatus(projectPath),
|
||||
queryFn: () => getExtractionStatus(projectPath),
|
||||
enabled: !!projectPath,
|
||||
staleTime: STALE_TIME,
|
||||
refetchInterval: 5000, // 每 5 秒刷新
|
||||
});
|
||||
}
|
||||
|
||||
// Hook: 合并状态
|
||||
export function useConsolidationStatus() {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
return useQuery({
|
||||
queryKey: memoryV2Keys.consolidationStatus(projectPath),
|
||||
queryFn: () => getConsolidationStatus(projectPath),
|
||||
enabled: !!projectPath,
|
||||
staleTime: STALE_TIME,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
// Hook: V2 作业列表
|
||||
export function useV2Jobs(filters?: { kind?: string; status_filter?: string }) {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
return useQuery({
|
||||
queryKey: memoryV2Keys.jobs(projectPath, filters),
|
||||
queryFn: () => getV2Jobs(filters, projectPath),
|
||||
enabled: !!projectPath,
|
||||
staleTime: STALE_TIME,
|
||||
refetchInterval: 10000, // 每 10 秒刷新
|
||||
});
|
||||
}
|
||||
|
||||
// Hook: 触发提取
|
||||
export function useTriggerExtraction() {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (maxSessions?: number) => triggerExtraction(maxSessions, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: memoryV2Keys.extractionStatus(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: memoryV2Keys.jobs(projectPath) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Hook: 触发合并
|
||||
export function useTriggerConsolidation() {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => triggerConsolidation(projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: memoryV2Keys.consolidationStatus(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: memoryV2Keys.jobs(projectPath) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Export types
|
||||
export type { ExtractionStatus, ConsolidationStatus, V2JobsResponse };
|
||||
@@ -1638,6 +1638,111 @@ export async function unarchiveMemory(memoryId: string, projectPath?: string): P
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Memory V2 API ==========
|
||||
|
||||
export interface ExtractionStatus {
|
||||
total_stage1: number;
|
||||
jobs: Array<{
|
||||
job_key: string;
|
||||
status: string;
|
||||
last_error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ConsolidationStatus {
|
||||
status: 'idle' | 'running' | 'completed' | 'error';
|
||||
memoryMdAvailable: boolean;
|
||||
memoryMdPreview?: string;
|
||||
inputCount?: number;
|
||||
lastRun?: number;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface V2Job {
|
||||
kind: string;
|
||||
job_key: string;
|
||||
status: 'pending' | 'running' | 'done' | 'error';
|
||||
last_error?: string;
|
||||
worker_id?: string;
|
||||
started_at?: number;
|
||||
finished_at?: number;
|
||||
retry_remaining?: number;
|
||||
}
|
||||
|
||||
export interface V2JobsResponse {
|
||||
jobs: V2Job[];
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Phase 1 extraction for eligible CLI sessions
|
||||
*/
|
||||
export async function triggerExtraction(
|
||||
maxSessions?: number,
|
||||
projectPath?: string
|
||||
): Promise<{ triggered: boolean; jobIds: string[]; message: string }> {
|
||||
const params = new URLSearchParams();
|
||||
if (projectPath) params.set('path', projectPath);
|
||||
return fetchApi<{ triggered: boolean; jobIds: string[]; message: string }>(
|
||||
`/api/core-memory/extract?${params}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ max_sessions: maxSessions }),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Phase 1 extraction status
|
||||
*/
|
||||
export async function getExtractionStatus(
|
||||
projectPath?: string
|
||||
): Promise<ExtractionStatus> {
|
||||
const params = new URLSearchParams();
|
||||
if (projectPath) params.set('path', projectPath);
|
||||
return fetchApi<ExtractionStatus>(`/api/core-memory/extract/status?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Phase 2 consolidation to generate MEMORY.md
|
||||
*/
|
||||
export async function triggerConsolidation(
|
||||
projectPath?: string
|
||||
): Promise<{ triggered: boolean; message: string }> {
|
||||
const params = new URLSearchParams();
|
||||
if (projectPath) params.set('path', projectPath);
|
||||
return fetchApi<{ triggered: boolean; message: string }>(
|
||||
`/api/core-memory/consolidate?${params}`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Phase 2 consolidation status
|
||||
*/
|
||||
export async function getConsolidationStatus(
|
||||
projectPath?: string
|
||||
): Promise<ConsolidationStatus> {
|
||||
const params = new URLSearchParams();
|
||||
if (projectPath) params.set('path', projectPath);
|
||||
return fetchApi<ConsolidationStatus>(`/api/core-memory/consolidate/status?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get V2 pipeline jobs list
|
||||
*/
|
||||
export async function getV2Jobs(
|
||||
options?: { kind?: string; status_filter?: string },
|
||||
projectPath?: string
|
||||
): Promise<V2JobsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (projectPath) params.set('path', projectPath);
|
||||
if (options?.kind) params.set('kind', options.kind);
|
||||
if (options?.status_filter) params.set('status_filter', options.status_filter);
|
||||
return fetchApi<V2JobsResponse>(`/api/core-memory/jobs?${params}`);
|
||||
}
|
||||
|
||||
// ========== Project Overview API ==========
|
||||
|
||||
export interface TechnologyStack {
|
||||
@@ -7365,47 +7470,48 @@ export async function getInjectionPreview(
|
||||
*/
|
||||
export interface CommandPreviewConfig {
|
||||
command: string;
|
||||
label: string;
|
||||
description: string;
|
||||
labelKey: string; // i18n key for label
|
||||
descriptionKey: string; // i18n key for description
|
||||
category?: string;
|
||||
mode: 'required' | 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined command preview configurations
|
||||
* Labels and descriptions use i18n keys: commandPreview.{key}.label / commandPreview.{key}.description
|
||||
*/
|
||||
export const COMMAND_PREVIEWS: CommandPreviewConfig[] = [
|
||||
{
|
||||
command: 'ccw spec load',
|
||||
label: 'Default (All Categories)',
|
||||
description: 'Load all required specs without category filter',
|
||||
labelKey: 'default',
|
||||
descriptionKey: 'default',
|
||||
mode: 'required',
|
||||
},
|
||||
{
|
||||
command: 'ccw spec load --category exploration',
|
||||
label: 'Exploration',
|
||||
description: 'Specs for code exploration, analysis, debugging',
|
||||
labelKey: 'exploration',
|
||||
descriptionKey: 'exploration',
|
||||
category: 'exploration',
|
||||
mode: 'required',
|
||||
},
|
||||
{
|
||||
command: 'ccw spec load --category planning',
|
||||
label: 'Planning',
|
||||
description: 'Specs for task planning, requirements',
|
||||
labelKey: 'planning',
|
||||
descriptionKey: 'planning',
|
||||
category: 'planning',
|
||||
mode: 'required',
|
||||
},
|
||||
{
|
||||
command: 'ccw spec load --category execution',
|
||||
label: 'Execution',
|
||||
description: 'Specs for implementation, testing, deployment',
|
||||
labelKey: 'execution',
|
||||
descriptionKey: 'execution',
|
||||
category: 'execution',
|
||||
mode: 'required',
|
||||
},
|
||||
{
|
||||
command: 'ccw spec load --category general',
|
||||
label: 'General',
|
||||
description: 'Specs that apply to all stages',
|
||||
labelKey: 'general',
|
||||
descriptionKey: 'general',
|
||||
category: 'general',
|
||||
mode: 'required',
|
||||
},
|
||||
|
||||
@@ -158,6 +158,50 @@
|
||||
"session-state-watch": {
|
||||
"name": "Session State Watch",
|
||||
"description": "Watch for session metadata file changes (workflow-session.json)"
|
||||
},
|
||||
"stop-notify": {
|
||||
"name": "Stop Notify",
|
||||
"description": "Notify dashboard when Claude finishes responding"
|
||||
},
|
||||
"auto-format-on-write": {
|
||||
"name": "Auto Format on Write",
|
||||
"description": "Auto-format files after Claude writes or edits them"
|
||||
},
|
||||
"auto-lint-on-write": {
|
||||
"name": "Auto Lint on Write",
|
||||
"description": "Auto-lint files after Claude writes or edits them"
|
||||
},
|
||||
"block-sensitive-files": {
|
||||
"name": "Block Sensitive Files",
|
||||
"description": "Block modifications to sensitive files (.env, secrets, credentials)"
|
||||
},
|
||||
"git-auto-stage": {
|
||||
"name": "Git Auto Stage",
|
||||
"description": "Auto stage all modified files when Claude finishes responding"
|
||||
},
|
||||
"post-edit-index": {
|
||||
"name": "Post Edit Index",
|
||||
"description": "Notify indexing service when files are modified"
|
||||
},
|
||||
"session-end-summary": {
|
||||
"name": "Session End Summary",
|
||||
"description": "Send session summary to dashboard on session end"
|
||||
},
|
||||
"project-state-inject": {
|
||||
"name": "Project State Inject",
|
||||
"description": "Inject project guidelines and recent dev history at session start"
|
||||
},
|
||||
"memory-v2-extract": {
|
||||
"name": "Memory V2 Extract",
|
||||
"description": "Trigger Phase 1 extraction when session ends (after idle period)"
|
||||
},
|
||||
"memory-v2-auto-consolidate": {
|
||||
"name": "Memory V2 Auto Consolidate",
|
||||
"description": "Trigger Phase 2 consolidation after extraction jobs complete"
|
||||
},
|
||||
"memory-sync-dashboard": {
|
||||
"name": "Memory Sync Dashboard",
|
||||
"description": "Sync memory V2 status to dashboard on changes"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
|
||||
@@ -109,5 +109,50 @@
|
||||
"vectorRank": "Vector #{rank}",
|
||||
"ftsRank": "FTS #{rank}",
|
||||
"heatScore": "Heat: {score}"
|
||||
},
|
||||
"v2": {
|
||||
"title": "Memory V2 Pipeline",
|
||||
"extraction": {
|
||||
"title": "Extraction",
|
||||
"description": "Extract structured memories from CLI sessions",
|
||||
"trigger": "Trigger Extraction",
|
||||
"extracting": "Extracting...",
|
||||
"extracted": "Extracted",
|
||||
"recentJobs": "Recent Jobs",
|
||||
"triggered": "Extraction triggered",
|
||||
"triggerError": "Failed to trigger extraction"
|
||||
},
|
||||
"consolidation": {
|
||||
"title": "Consolidation",
|
||||
"description": "Consolidate extraction results into MEMORY.md",
|
||||
"trigger": "Trigger Consolidation",
|
||||
"consolidating": "Consolidating...",
|
||||
"preview": "Preview",
|
||||
"memoryMd": "MEMORY.md",
|
||||
"exists": "Exists",
|
||||
"notExists": "Not Exists",
|
||||
"inputs": "Inputs",
|
||||
"triggered": "Consolidation triggered",
|
||||
"triggerError": "Failed to trigger consolidation"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "Jobs",
|
||||
"kind": "Kind",
|
||||
"key": "Key",
|
||||
"status": "Status",
|
||||
"error": "Error",
|
||||
"noJobs": "No jobs found",
|
||||
"allKinds": "All Kinds",
|
||||
"extraction": "Extraction",
|
||||
"consolidation": "Consolidation"
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
"running": "Running",
|
||||
"completed": "Completed",
|
||||
"done": "Done",
|
||||
"error": "Error",
|
||||
"pending": "Pending"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +213,46 @@
|
||||
"warning": "Approaching Limit",
|
||||
"normal": "Normal",
|
||||
"characters": "characters",
|
||||
"lines": "lines",
|
||||
"maxLimit": "Max",
|
||||
"quickPresets": "Quick presets:",
|
||||
"statsInfo": "Statistics",
|
||||
"requiredLength": "Required specs length:",
|
||||
"matchedLength": "Keyword-matched length:",
|
||||
"remaining": "Remaining space:",
|
||||
"loadError": "Failed to load stats",
|
||||
"saveSuccess": "Settings saved successfully",
|
||||
"saveError": "Failed to save settings"
|
||||
"saveError": "Failed to save settings",
|
||||
"filesList": "Injection Files",
|
||||
"files": "files",
|
||||
"noFiles": "No files match this command",
|
||||
"loadingPreview": "Loading preview...",
|
||||
"commandPreview": "Command Injection Preview",
|
||||
"commandPreviewDesc": "Preview the content that would be injected by different CLI commands",
|
||||
"previewTitle": "Injection Preview"
|
||||
},
|
||||
|
||||
"commandPreview": {
|
||||
"default": {
|
||||
"label": "All Categories",
|
||||
"description": "Load all required specs without category filter"
|
||||
},
|
||||
"exploration": {
|
||||
"label": "Exploration",
|
||||
"description": "Specs for code exploration, analysis, debugging"
|
||||
},
|
||||
"planning": {
|
||||
"label": "Planning",
|
||||
"description": "Specs for task planning, requirements"
|
||||
},
|
||||
"execution": {
|
||||
"label": "Execution",
|
||||
"description": "Specs for implementation, testing, deployment"
|
||||
},
|
||||
"general": {
|
||||
"label": "General",
|
||||
"description": "Specs that apply to all stages"
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
@@ -231,9 +264,27 @@
|
||||
"defaultReadModeHelp": "The default read mode for newly created personal specs",
|
||||
"selectReadMode": "Select read mode",
|
||||
"autoEnable": "Auto Enable New Specs",
|
||||
"autoEnableDescription": "Automatically enable newly created personal specs",
|
||||
"autoEnableDescription": "New personal specs are set to required (readMode=required) by default and automatically included in context injection",
|
||||
"specStatistics": "Spec Statistics",
|
||||
"totalSpecs": "Total: {count} spec files"
|
||||
"totalSpecs": "Total: {count} spec files",
|
||||
"devProgressInjection": "Development Progress Injection",
|
||||
"devProgressInjectionDesc": "Control how development progress from project-tech.json is injected into AI context",
|
||||
"enableDevProgress": "Enable Injection",
|
||||
"enableDevProgressDesc": "Include development history in AI context",
|
||||
"maxEntries": "Max Entries per Category",
|
||||
"maxEntriesDesc": "Maximum number of entries to include per category (1-50)",
|
||||
"includeCategories": "Include Categories",
|
||||
"categoriesDesc": "Click to toggle category inclusion",
|
||||
"devProgressStats": "{total} entries from {sessions} sessions, last updated: {date}",
|
||||
"devProgressStatsNoDate": "{total} entries from {sessions} sessions"
|
||||
},
|
||||
|
||||
"devCategory": {
|
||||
"feature": "Feature",
|
||||
"enhancement": "Enhancement",
|
||||
"bugfix": "Bug Fix",
|
||||
"refactor": "Refactor",
|
||||
"docs": "Docs"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
|
||||
@@ -158,6 +158,50 @@
|
||||
"session-state-watch": {
|
||||
"name": "会话状态监控",
|
||||
"description": "监控会话元数据文件变更 (workflow-session.json)"
|
||||
},
|
||||
"stop-notify": {
|
||||
"name": "停止通知",
|
||||
"description": "当 Claude 完成响应时通知仪表盘"
|
||||
},
|
||||
"auto-format-on-write": {
|
||||
"name": "写入自动格式化",
|
||||
"description": "在 Claude 写入或编辑文件后自动格式化"
|
||||
},
|
||||
"auto-lint-on-write": {
|
||||
"name": "写入自动检查",
|
||||
"description": "在 Claude 写入或编辑文件后自动进行 Lint 检查"
|
||||
},
|
||||
"block-sensitive-files": {
|
||||
"name": "阻止敏感文件修改",
|
||||
"description": "阻止对敏感文件 (.env、密钥、凭据) 的修改"
|
||||
},
|
||||
"git-auto-stage": {
|
||||
"name": "Git 自动暂存",
|
||||
"description": "当 Claude 完成响应时自动暂存所有修改的文件"
|
||||
},
|
||||
"post-edit-index": {
|
||||
"name": "编辑后索引",
|
||||
"description": "文件修改时通知索引服务"
|
||||
},
|
||||
"session-end-summary": {
|
||||
"name": "会话结束摘要",
|
||||
"description": "会话结束时向仪表盘发送会话摘要"
|
||||
},
|
||||
"project-state-inject": {
|
||||
"name": "项目状态注入",
|
||||
"description": "会话开始时注入项目指南和最近开发历史"
|
||||
},
|
||||
"memory-v2-extract": {
|
||||
"name": "Memory V2 提取",
|
||||
"description": "会话结束时触发 Phase 1 提取(空闲期后)"
|
||||
},
|
||||
"memory-v2-auto-consolidate": {
|
||||
"name": "Memory V2 自动合并",
|
||||
"description": "提取作业完成后触发 Phase 2 合并"
|
||||
},
|
||||
"memory-sync-dashboard": {
|
||||
"name": "Memory 同步仪表盘",
|
||||
"description": "变更时将 Memory V2 状态同步到仪表盘"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
|
||||
@@ -109,5 +109,50 @@
|
||||
"vectorRank": "向量 #{rank}",
|
||||
"ftsRank": "全文 #{rank}",
|
||||
"heatScore": "热度: {score}"
|
||||
},
|
||||
"v2": {
|
||||
"title": "Memory V2 Pipeline",
|
||||
"extraction": {
|
||||
"title": "提取",
|
||||
"description": "从 CLI 会话中提取结构化记忆",
|
||||
"trigger": "触发提取",
|
||||
"extracting": "提取中...",
|
||||
"extracted": "已提取",
|
||||
"recentJobs": "最近作业",
|
||||
"triggered": "提取已触发",
|
||||
"triggerError": "触发提取失败"
|
||||
},
|
||||
"consolidation": {
|
||||
"title": "合并",
|
||||
"description": "合并提取结果生成 MEMORY.md",
|
||||
"trigger": "触发合并",
|
||||
"consolidating": "合并中...",
|
||||
"preview": "预览",
|
||||
"memoryMd": "MEMORY.md",
|
||||
"exists": "存在",
|
||||
"notExists": "不存在",
|
||||
"inputs": "输入",
|
||||
"triggered": "合并已触发",
|
||||
"triggerError": "触发合并失败"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "作业列表",
|
||||
"kind": "类型",
|
||||
"key": "Key",
|
||||
"status": "状态",
|
||||
"error": "错误",
|
||||
"noJobs": "暂无作业记录",
|
||||
"allKinds": "所有类型",
|
||||
"extraction": "提取",
|
||||
"consolidation": "合并"
|
||||
},
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
"running": "运行中",
|
||||
"completed": "已完成",
|
||||
"done": "完成",
|
||||
"error": "错误",
|
||||
"pending": "等待"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"normal": "正常",
|
||||
"characters": "字符",
|
||||
"chars": "字符",
|
||||
"lines": "行",
|
||||
"maxLimit": "最大",
|
||||
"quickPresets": "快速预设:",
|
||||
"statsInfo": "统计信息",
|
||||
"requiredLength": "必读规范长度:",
|
||||
"matchedLength": "关键词匹配长度:",
|
||||
@@ -222,7 +225,34 @@
|
||||
"saveSuccess": "设置已保存",
|
||||
"saveError": "保存设置失败",
|
||||
"filesList": "注入文件列表",
|
||||
"files": "个文件"
|
||||
"files": "个文件",
|
||||
"noFiles": "没有匹配此命令的文件",
|
||||
"loadingPreview": "加载预览中...",
|
||||
"commandPreview": "命令注入预览",
|
||||
"commandPreviewDesc": "预览不同 CLI 命令将注入的内容"
|
||||
},
|
||||
|
||||
"commandPreview": {
|
||||
"default": {
|
||||
"label": "全部类别",
|
||||
"description": "加载所有必读规范,不进行类别过滤"
|
||||
},
|
||||
"exploration": {
|
||||
"label": "探索",
|
||||
"description": "代码探索、分析、调试相关规范"
|
||||
},
|
||||
"planning": {
|
||||
"label": "规划",
|
||||
"description": "任务规划、需求相关规范"
|
||||
},
|
||||
"execution": {
|
||||
"label": "执行",
|
||||
"description": "实现、测试、部署相关规范"
|
||||
},
|
||||
"general": {
|
||||
"label": "通用",
|
||||
"description": "适用于所有阶段的规范"
|
||||
}
|
||||
},
|
||||
|
||||
"priority": {
|
||||
@@ -241,9 +271,27 @@
|
||||
"defaultReadModeHelp": "新创建的个人规范的默认读取模式",
|
||||
"selectReadMode": "选择读取模式",
|
||||
"autoEnable": "自动启用新规范",
|
||||
"autoEnableDescription": "自动启用新创建的个人规范",
|
||||
"autoEnableDescription": "新创建的个人规范默认设置为必读(readMode=required),自动加入注入上下文",
|
||||
"specStatistics": "规范统计",
|
||||
"totalSpecs": "总计:{count} 个规范文件"
|
||||
"totalSpecs": "总计:{count} 个规范文件",
|
||||
"devProgressInjection": "开发进度注入",
|
||||
"devProgressInjectionDesc": "控制如何将 project-tech.json 中的开发进度注入到 AI 上下文中",
|
||||
"enableDevProgress": "启用注入",
|
||||
"enableDevProgressDesc": "在 AI 上下文中包含开发历史记录",
|
||||
"maxEntries": "每类最大条目数",
|
||||
"maxEntriesDesc": "每个类别包含的最大条目数 (1-50)",
|
||||
"includeCategories": "包含的类别",
|
||||
"categoriesDesc": "点击切换类别包含状态",
|
||||
"devProgressStats": "共 {total} 条记录,来自 {sessions} 个会话,最后更新:{date}",
|
||||
"devProgressStatsNoDate": "共 {total} 条记录,来自 {sessions} 个会话"
|
||||
},
|
||||
|
||||
"devCategory": {
|
||||
"feature": "新功能",
|
||||
"enhancement": "增强",
|
||||
"bugfix": "修复",
|
||||
"refactor": "重构",
|
||||
"docs": "文档"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { Checkbox } from '@/components/ui/Checkbox';
|
||||
import { useMemory, useMemoryMutations, useUnifiedSearch, useUnifiedStats, useRecommendations, useReindex } from '@/hooks';
|
||||
import type { CoreMemory, UnifiedSearchResult } from '@/lib/api';
|
||||
import { cn, parseMemoryMetadata } from '@/lib/utils';
|
||||
import { V2PipelineTab } from '@/components/memory/V2PipelineTab';
|
||||
|
||||
// ========== Source Type Helpers ==========
|
||||
|
||||
@@ -624,7 +625,7 @@ export function MemoryPage() {
|
||||
const [isNewMemoryOpen, setIsNewMemoryOpen] = useState(false);
|
||||
const [editingMemory, setEditingMemory] = useState<CoreMemory | null>(null);
|
||||
const [viewingMemory, setViewingMemory] = useState<CoreMemory | null>(null);
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories');
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch' | 'v2pipeline'>('memories');
|
||||
const [unifiedQuery, setUnifiedQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||
@@ -866,6 +867,11 @@ export function MemoryPage() {
|
||||
label: formatMessage({ id: 'memory.tabs.unifiedSearch' }),
|
||||
icon: <Search className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'v2pipeline',
|
||||
label: 'V2 Pipeline',
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1062,7 +1068,10 @@ export function MemoryPage() {
|
||||
)}
|
||||
|
||||
{/* Content Area */}
|
||||
{isUnifiedTab ? (
|
||||
{currentTab === 'v2pipeline' ? (
|
||||
/* V2 Pipeline Tab */
|
||||
<V2PipelineTab />
|
||||
) : isUnifiedTab ? (
|
||||
/* Unified Search Results */
|
||||
unifiedLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
|
||||
@@ -376,5 +376,5 @@ export function run(argv: string[]): void {
|
||||
program.parse(argv);
|
||||
}
|
||||
|
||||
// Invoke CLI when run directly
|
||||
run(process.argv);
|
||||
// Note: run() is called by bin/ccw.js entry point
|
||||
// Do not call run() here to avoid duplicate execution
|
||||
|
||||
@@ -224,6 +224,7 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const mode = url.searchParams.get('mode') || 'required'; // required | all | keywords
|
||||
const preview = url.searchParams.get('preview') === 'true';
|
||||
const category = url.searchParams.get('category') || undefined; // optional category filter
|
||||
|
||||
try {
|
||||
const { getDimensionIndex, SPEC_DIMENSIONS } = await import(
|
||||
@@ -254,6 +255,11 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter by category if specified
|
||||
if (category && (entry.category || 'general') !== category) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileData: InjectionFile = {
|
||||
file: entry.file,
|
||||
title: entry.title,
|
||||
|
||||
@@ -444,28 +444,28 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
|
||||
|
||||
// API: Get project-tech stats for development progress injection
|
||||
if (pathname === '/api/project-tech/stats' && req.method === 'GET') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const techPath = join(resolvedPath, '.workflow', 'project-tech.json');
|
||||
|
||||
if (!existsSync(techPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
total_features: 0,
|
||||
total_sessions: 0,
|
||||
last_updated: null,
|
||||
categories: {
|
||||
feature: 0,
|
||||
enhancement: 0,
|
||||
bugfix: 0,
|
||||
refactor: 0,
|
||||
docs: 0
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const techPath = join(resolvedPath, '.workflow', 'project-tech.json');
|
||||
|
||||
if (!existsSync(techPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
total_features: 0,
|
||||
total_sessions: 0,
|
||||
last_updated: null,
|
||||
categories: {
|
||||
feature: 0,
|
||||
enhancement: 0,
|
||||
bugfix: 0,
|
||||
refactor: 0,
|
||||
docs: 0
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
const rawContent = readFileSync(techPath, 'utf-8');
|
||||
const tech = JSON.parse(rawContent) as {
|
||||
development_index?: {
|
||||
|
||||
@@ -626,13 +626,13 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleFilesRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// System routes (data, health, version, paths, shutdown, notify, storage, dialog, a2ui answer broker, system settings)
|
||||
// System routes (data, health, version, paths, shutdown, notify, storage, dialog, a2ui answer broker, system settings, project-tech)
|
||||
if (pathname === '/api/data' || pathname === '/api/health' ||
|
||||
pathname === '/api/version-check' || pathname === '/api/shutdown' ||
|
||||
pathname === '/api/recent-paths' || pathname === '/api/switch-path' ||
|
||||
pathname === '/api/remove-recent-path' || pathname === '/api/system/notify' ||
|
||||
pathname === '/api/system/settings' || pathname === '/api/system/hooks/install-recommended' ||
|
||||
pathname === '/api/a2ui/answer' ||
|
||||
pathname === '/api/a2ui/answer' || pathname === '/api/project-tech/stats' ||
|
||||
pathname.startsWith('/api/storage/') || pathname.startsWith('/api/dialog/')) {
|
||||
if (await handleSystemRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user