mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Enhance team lifecycle roles with checkpoint handling and inner loop execution
- Added checkpoint gate handling to the coordinator role, defining behavior based on quality gate results. - Updated planner role to utilize inner loop pattern for structured implementation planning and reporting. - Revised writer role to implement inner loop for document generation, delegating CLI execution to a subagent. - Introduced a new doc-generation subagent for isolated CLI calls and document generation strategies. - Enhanced UI components in the frontend to display job statuses, last run times, and improved error handling. - Updated localization files to include new strings for job details and status banners. - Improved CSS styles for markdown previews to enhance readability and presentation.
This commit is contained in:
@@ -53,10 +53,10 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
Zap,
|
||||
CheckCircle,
|
||||
@@ -17,11 +18,17 @@ import {
|
||||
FileText,
|
||||
Database,
|
||||
Activity,
|
||||
Copy,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
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 'highlight.js/styles/github-dark.css';
|
||||
import {
|
||||
useExtractionStatus,
|
||||
useConsolidationStatus,
|
||||
@@ -31,6 +38,24 @@ import {
|
||||
} from '@/hooks/useMemoryV2';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Format a timestamp to relative time string
|
||||
*/
|
||||
function formatRelativeTime(timestamp: number | string | undefined): string | null {
|
||||
if (!timestamp) return null;
|
||||
|
||||
try {
|
||||
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp);
|
||||
if (isNaN(date.getTime())) return null;
|
||||
|
||||
return formatDistanceToNow(date, { addSuffix: true });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Status Badge ==========
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
|
||||
@@ -66,6 +91,7 @@ function ExtractionCard() {
|
||||
|
||||
// Check if any job is running
|
||||
const hasRunningJob = status?.jobs?.some(j => j.status === 'running');
|
||||
const lastRunText = formatRelativeTime(status?.lastRun);
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -78,6 +104,11 @@ function ExtractionCard() {
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.description', defaultMessage: 'Extract structured memories from CLI sessions' })}
|
||||
</p>
|
||||
{lastRunText && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.extraction.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{status && (
|
||||
<div className="text-right">
|
||||
@@ -150,12 +181,24 @@ function ConsolidationCard() {
|
||||
const { data: status, isLoading, refetch } = useConsolidationStatus();
|
||||
const trigger = useTriggerConsolidation();
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleTrigger = () => {
|
||||
trigger.mutate();
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(status?.memoryMdPreview || '');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const isRunning = status?.status === 'running';
|
||||
const lastRunText = formatRelativeTime(status?.lastRun);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -169,6 +212,11 @@ function ConsolidationCard() {
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.description', defaultMessage: 'Merge extracted results into MEMORY.md' })}
|
||||
</p>
|
||||
{lastRunText && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{intl.formatMessage({ id: 'memory.v2.consolidation.lastRun', defaultMessage: 'Last run' })}: {lastRunText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{status && <StatusBadge status={status.status} />}
|
||||
</div>
|
||||
@@ -226,17 +274,43 @@ function ConsolidationCard() {
|
||||
|
||||
{/* MEMORY.md Preview Dialog */}
|
||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogContent className="max-w-4xl 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 className="flex items-center justify-between pr-8">
|
||||
<span className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
MEMORY.md
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="h-8 px-2"
|
||||
title={copied
|
||||
? intl.formatMessage({ id: 'memory.v2.consolidation.copySuccess', defaultMessage: 'Copied' })
|
||||
: intl.formatMessage({ id: 'memory.actions.copy', defaultMessage: 'Copy' })
|
||||
}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</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 className="overflow-auto flex-1 markdown-preview">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
>
|
||||
{status?.memoryMdPreview || intl.formatMessage({
|
||||
id: 'memory.v2.consolidation.noContent',
|
||||
defaultMessage: 'No content available'
|
||||
})}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -246,10 +320,55 @@ function ConsolidationCard() {
|
||||
|
||||
// ========== Jobs List ==========
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function JobsList() {
|
||||
const intl = useIntl();
|
||||
const [kindFilter, setKindFilter] = useState<string>('');
|
||||
const { data, isLoading, refetch } = useV2Jobs(kindFilter ? { kind: kindFilter } : undefined);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [selectedJob, setSelectedJob] = useState<V2Job | null>(null);
|
||||
const [copiedError, setCopiedError] = useState(false);
|
||||
|
||||
// Build filters object
|
||||
const filters = {
|
||||
...(kindFilter && { kind: kindFilter }),
|
||||
...(statusFilter && { status_filter: statusFilter }),
|
||||
};
|
||||
|
||||
const { data, isLoading, refetch } = useV2Jobs(Object.keys(filters).length > 0 ? filters : undefined);
|
||||
|
||||
// Format timestamp to readable string
|
||||
const formatTimestamp = (timestamp: number | undefined): string => {
|
||||
if (!timestamp) return '-';
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
// Copy error to clipboard
|
||||
const handleCopyError = async () => {
|
||||
if (selectedJob?.last_error) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedJob.last_error);
|
||||
setCopiedError(true);
|
||||
setTimeout(() => setCopiedError(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -264,9 +383,36 @@ function JobsList() {
|
||||
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>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.allKinds', defaultMessage: 'All Kinds' })}
|
||||
</option>
|
||||
<option value="phase1_extraction">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.extraction', defaultMessage: 'Extraction' })}
|
||||
</option>
|
||||
<option value="memory_consolidate_global">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.consolidation', defaultMessage: 'Consolidation' })}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-2 py-1 text-sm border rounded bg-background"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.all', defaultMessage: 'All Status' })}
|
||||
</option>
|
||||
<option value="pending">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.pending', defaultMessage: 'Pending' })}
|
||||
</option>
|
||||
<option value="running">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.running', defaultMessage: 'Running' })}
|
||||
</option>
|
||||
<option value="done">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.done', defaultMessage: 'Done' })}
|
||||
</option>
|
||||
<option value="error">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.statusFilter.error', defaultMessage: 'Error' })}
|
||||
</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
||||
@@ -295,7 +441,11 @@ function JobsList() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.jobs.map((job) => (
|
||||
<tr key={`${job.kind}-${job.job_key}`} className="border-b">
|
||||
<tr
|
||||
key={`${job.kind}-${job.job_key}`}
|
||||
className="border-b cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => setSelectedJob(job)}
|
||||
>
|
||||
<td className="p-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{job.kind === 'phase1_extraction' ? 'Extraction' :
|
||||
@@ -330,15 +480,164 @@ function JobsList() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Job Detail Dialog */}
|
||||
<Dialog open={!!selectedJob} onOpenChange={() => setSelectedJob(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.title', defaultMessage: 'Job Details' })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.kind', defaultMessage: 'Kind' })}
|
||||
</label>
|
||||
<p className="text-sm font-medium">
|
||||
{selectedJob?.kind === 'phase1_extraction' ? 'Extraction' :
|
||||
selectedJob?.kind === 'memory_consolidate_global' ? 'Consolidation' : selectedJob?.kind}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.status', defaultMessage: 'Status' })}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
{selectedJob && <StatusBadge status={selectedJob.status} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.jobKey', defaultMessage: 'Job ID' })}
|
||||
</label>
|
||||
<p className="text-sm font-mono break-all">{selectedJob?.job_key}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.startedAt', defaultMessage: 'Started At' })}
|
||||
</label>
|
||||
<p className="text-sm">{formatTimestamp(selectedJob?.started_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.finishedAt', defaultMessage: 'Finished At' })}
|
||||
</label>
|
||||
<p className="text-sm">{formatTimestamp(selectedJob?.finished_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.workerId', defaultMessage: 'Worker ID' })}
|
||||
</label>
|
||||
<p className="text-sm font-mono truncate">{selectedJob?.worker_id || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.retryRemaining', defaultMessage: 'Retry Remaining' })}
|
||||
</label>
|
||||
<p className="text-sm">{selectedJob?.retry_remaining ?? '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Section */}
|
||||
{selectedJob?.last_error && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.error', defaultMessage: 'Error' })}
|
||||
</label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopyError}
|
||||
className="h-6 px-2"
|
||||
title={copiedError
|
||||
? intl.formatMessage({ id: 'memory.actions.copySuccess', defaultMessage: 'Copied' })
|
||||
: intl.formatMessage({ id: 'memory.actions.copy', defaultMessage: 'Copy' })
|
||||
}
|
||||
>
|
||||
{copiedError ? (
|
||||
<Check className="w-3 h-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="text-xs bg-red-50 dark:bg-red-950/30 text-red-800 dark:text-red-300 p-3 rounded overflow-auto max-h-40 whitespace-pre-wrap break-all">
|
||||
{selectedJob.last_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!selectedJob?.last_error && (
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.error', defaultMessage: 'Error' })}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
{intl.formatMessage({ id: 'memory.v2.jobs.detail.noError', defaultMessage: 'No error' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Pipeline Status Banner ==========
|
||||
|
||||
function PipelineStatusBanner() {
|
||||
const intl = useIntl();
|
||||
const { data } = useV2Jobs();
|
||||
|
||||
// Detect running and error jobs
|
||||
const jobs = data?.jobs || [];
|
||||
const runningJobs = jobs.filter(j => j.status === 'running');
|
||||
const errorJobs = jobs.filter(j => j.status === 'error');
|
||||
const hasRunningJobs = runningJobs.length > 0;
|
||||
const errorCount = errorJobs.length;
|
||||
const runningCount = runningJobs.length;
|
||||
|
||||
if (hasRunningJobs) {
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||
{intl.formatMessage(
|
||||
{ id: 'memory.v2.statusBanner.running', defaultMessage: 'Pipeline Running - {count} job(s) in progress' },
|
||||
{ count: runningCount }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
{intl.formatMessage(
|
||||
{ id: 'memory.v2.statusBanner.hasErrors', defaultMessage: 'Pipeline Idle - {count} job(s) failed' },
|
||||
{ count: errorCount }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function V2PipelineTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PipelineStatusBanner />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ExtractionCard />
|
||||
<ConsolidationCard />
|
||||
|
||||
@@ -744,3 +744,125 @@
|
||||
[data-reduced-motion="true"] .bg-image-layer {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Markdown Preview Styles
|
||||
=========================== */
|
||||
.markdown-preview {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview .prose {
|
||||
color: hsl(var(--text));
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.markdown-preview .prose h1,
|
||||
.markdown-preview .prose h2,
|
||||
.markdown-preview .prose h3,
|
||||
.markdown-preview .prose h4 {
|
||||
color: hsl(var(--text));
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-preview .prose h1 { font-size: 1.5em; }
|
||||
.markdown-preview .prose h2 { font-size: 1.25em; }
|
||||
.markdown-preview .prose h3 { font-size: 1.125em; }
|
||||
.markdown-preview .prose h4 { font-size: 1em; }
|
||||
|
||||
.markdown-preview .prose p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose ul,
|
||||
.markdown-preview .prose ol {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose li {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose code {
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 0.125em 0.375em;
|
||||
border-radius: 0.25em;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose pre {
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
overflow-x: auto;
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose blockquote {
|
||||
border-left: 3px solid hsl(var(--accent));
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
color: hsl(var(--text-secondary));
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-preview .prose a {
|
||||
color: hsl(var(--accent));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-preview .prose a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.markdown-preview .prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.markdown-preview .prose th,
|
||||
.markdown-preview .prose td {
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 0.5em 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-preview .prose th {
|
||||
background-color: hsl(var(--muted));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-preview .prose hr {
|
||||
border: none;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* Highlight.js overrides for dark theme */
|
||||
.markdown-preview .hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme^="dark"] .markdown-preview .prose pre {
|
||||
background-color: hsl(220 25% 12%);
|
||||
}
|
||||
|
||||
[data-theme^="dark"] .markdown-preview .prose code {
|
||||
background-color: hsl(220 25% 18%);
|
||||
}
|
||||
|
||||
@@ -1642,6 +1642,7 @@ export async function unarchiveMemory(memoryId: string, projectPath?: string): P
|
||||
|
||||
export interface ExtractionStatus {
|
||||
total_stage1: number;
|
||||
lastRun?: number;
|
||||
jobs: Array<{
|
||||
job_key: string;
|
||||
status: string;
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"extracting": "Extracting...",
|
||||
"extracted": "Extracted",
|
||||
"recentJobs": "Recent Jobs",
|
||||
"lastRun": "Last run",
|
||||
"triggered": "Extraction triggered",
|
||||
"triggerError": "Failed to trigger extraction"
|
||||
},
|
||||
@@ -132,8 +133,11 @@
|
||||
"exists": "Exists",
|
||||
"notExists": "Not Exists",
|
||||
"inputs": "Inputs",
|
||||
"lastRun": "Last run",
|
||||
"triggered": "Consolidation triggered",
|
||||
"triggerError": "Failed to trigger consolidation"
|
||||
"triggerError": "Failed to trigger consolidation",
|
||||
"copySuccess": "Copied",
|
||||
"noContent": "No content available"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "Jobs",
|
||||
@@ -144,7 +148,26 @@
|
||||
"noJobs": "No jobs found",
|
||||
"allKinds": "All Kinds",
|
||||
"extraction": "Extraction",
|
||||
"consolidation": "Consolidation"
|
||||
"consolidation": "Consolidation",
|
||||
"statusFilter": {
|
||||
"all": "All Status",
|
||||
"pending": "Pending",
|
||||
"running": "Running",
|
||||
"done": "Done",
|
||||
"error": "Error"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Job Details",
|
||||
"kind": "Kind",
|
||||
"jobKey": "Job ID",
|
||||
"status": "Status",
|
||||
"startedAt": "Started At",
|
||||
"finishedAt": "Finished At",
|
||||
"workerId": "Worker ID",
|
||||
"retryRemaining": "Retry Remaining",
|
||||
"error": "Error",
|
||||
"noError": "No error"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
@@ -153,6 +176,10 @@
|
||||
"done": "Done",
|
||||
"error": "Error",
|
||||
"pending": "Pending"
|
||||
},
|
||||
"statusBanner": {
|
||||
"running": "Pipeline Running - {count} job(s) in progress",
|
||||
"hasErrors": "Pipeline Idle - {count} job(s) failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"extracting": "提取中...",
|
||||
"extracted": "已提取",
|
||||
"recentJobs": "最近作业",
|
||||
"lastRun": "上次运行",
|
||||
"triggered": "提取已触发",
|
||||
"triggerError": "触发提取失败"
|
||||
},
|
||||
@@ -132,8 +133,11 @@
|
||||
"exists": "存在",
|
||||
"notExists": "不存在",
|
||||
"inputs": "输入",
|
||||
"lastRun": "上次运行",
|
||||
"triggered": "合并已触发",
|
||||
"triggerError": "触发合并失败"
|
||||
"triggerError": "触发合并失败",
|
||||
"copySuccess": "复制成功",
|
||||
"noContent": "暂无内容"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "作业列表",
|
||||
@@ -144,7 +148,26 @@
|
||||
"noJobs": "暂无作业记录",
|
||||
"allKinds": "所有类型",
|
||||
"extraction": "提取",
|
||||
"consolidation": "合并"
|
||||
"consolidation": "合并",
|
||||
"statusFilter": {
|
||||
"all": "所有状态",
|
||||
"pending": "等待",
|
||||
"running": "运行中",
|
||||
"done": "完成",
|
||||
"error": "错误"
|
||||
},
|
||||
"detail": {
|
||||
"title": "作业详情",
|
||||
"kind": "类型",
|
||||
"jobKey": "作业 ID",
|
||||
"status": "状态",
|
||||
"startedAt": "开始时间",
|
||||
"finishedAt": "结束时间",
|
||||
"workerId": "Worker ID",
|
||||
"retryRemaining": "剩余重试次数",
|
||||
"error": "错误信息",
|
||||
"noError": "无错误"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
@@ -153,6 +176,10 @@
|
||||
"done": "完成",
|
||||
"error": "错误",
|
||||
"pending": "等待"
|
||||
},
|
||||
"statusBanner": {
|
||||
"running": "Pipeline 运行中 - {count} 个作业正在执行",
|
||||
"hasErrors": "Pipeline 空闲 - {count} 个作业失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user