feat(engine): 添加MCP连接管理和工具集成功能

- 集成MCP连接管理器,支持MCP服务器连接
- 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、
  PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、
  TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等
- 实现工具注册和装配功能
- 添加技能选择上下文参数
- 支持思考模式控制参数thinking_enabled

feat(coordinator): 重构任务执行计划器参数命名

- 将learning_candidate_enabled重命名为allow_candidate_generation
- 更新TeamGraphScheduler中的参数传递
- 修改LocalAgentRunner中的相关参数处理
- 更新README文档中的相应描述

refactor(context): 标准化工具调用参数格式

- 添加_json导入用于参数序列化
- 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷
- 修复工具调用中参数非字符串类型的序列化问题

refactor(session): 优化消息历史记录过滤逻辑

- 修改get_messages_as_conversation为基于运行状态过滤消息
- 排除未完成、失败或错误结束的运行记录
- 改进对话历史的可见性控制机制

fix(store): 修复FTS索引重建逻辑

- 添加异常处理防止FTS索引创建失败
- 实现_rebuild_fts_index方法重新构建全文搜索索引
- 优化索引触发器和表的维护流程
This commit is contained in:
2026-05-14 09:43:48 +08:00
parent 8a12c30141
commit 30ab74ffb2
149 changed files with 12293 additions and 2812 deletions

View File

@ -25,28 +25,28 @@ const TERMINAL_STATUSES = new Set<ProcessRun['status']>(['done', 'error', 'cance
const AGENT_ACCENTS = [
{
frame: 'border-sky-500/25 bg-sky-500/[0.05]',
title: 'text-sky-300',
dot: 'bg-sky-400',
result: 'border-sky-500/25 bg-sky-500/[0.08]',
frame: 'border-[#BCC4CE] bg-[#E4E7EB]/45',
title: 'text-[#697281]',
dot: 'bg-[#8C96A3]',
result: 'border-[#BCC4CE] bg-[#E4E7EB]/55',
},
{
frame: 'border-emerald-500/25 bg-emerald-500/[0.05]',
title: 'text-emerald-300',
dot: 'bg-emerald-400',
result: 'border-emerald-500/25 bg-emerald-500/[0.08]',
frame: 'border-[#B7C2B5] bg-[#E3E8E2]/45',
title: 'text-[#657162]',
dot: 'bg-[#869683]',
result: 'border-[#B7C2B5] bg-[#E3E8E2]/55',
},
{
frame: 'border-amber-500/25 bg-amber-500/[0.05]',
title: 'text-amber-300',
dot: 'bg-amber-400',
result: 'border-amber-500/25 bg-amber-500/[0.08]',
frame: 'border-[#B8AEA8] bg-[#E7E2DE]/55',
title: 'text-[#5F5550]',
dot: 'bg-[#8B7E77]',
result: 'border-[#B8AEA8] bg-[#E7E2DE]/65',
},
{
frame: 'border-fuchsia-500/25 bg-fuchsia-500/[0.05]',
title: 'text-fuchsia-300',
dot: 'bg-fuchsia-400',
result: 'border-fuchsia-500/25 bg-fuchsia-500/[0.08]',
frame: 'border-[#D8D2CE] bg-[#ECE8E5]/70',
title: 'text-[#4F4642]',
dot: 'bg-[#6A5E58]',
result: 'border-[#D8D2CE] bg-[#ECE8E5]/80',
},
] as const;
@ -55,12 +55,12 @@ function accentFor(index: number) {
}
function statusTone(status: ProcessRun['status']) {
if (status === 'done') return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300';
if (status === 'error') return 'border-rose-500/20 bg-rose-500/10 text-rose-300';
if (status === 'cancelled') return 'border-zinc-500/20 bg-zinc-500/10 text-zinc-300';
if (status === 'waiting') return 'border-amber-500/20 bg-amber-500/10 text-amber-300';
if (status === 'queued') return 'border-sky-500/20 bg-sky-500/10 text-sky-300';
return 'border-sky-500/20 bg-sky-500/10 text-sky-300';
if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]';
if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]';
if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]';
if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]';
if (status === 'queued') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#4F4642]';
return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]';
}
function feedTone(role: AgentFeedItem['role']) {
@ -166,8 +166,8 @@ function SkillChips({ metadata }: { metadata?: Record<string, unknown> }) {
const rawEphemeral = metadata?.ephemeral_skill_names;
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : '';
if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) {
return null;
}
return (
@ -182,9 +182,9 @@ function SkillChips({ metadata }: { metadata?: Record<string, unknown> }) {
ephemeral:{name}
</Badge>
))}
{draftId && (
{guidanceId && (
<Badge variant="outline" className="text-[10px]">
draft:{draftId.slice(0, 8)}
guidance:{guidanceId.slice(0, 8)}
</Badge>
)}
</div>
@ -390,7 +390,7 @@ function ResultCard({
<div className="text-[10px] font-medium uppercase tracking-[0.18em] text-muted-foreground">{pickAppText(locale, '结果', 'Result')}</div>
<div className={cn('mt-1 truncate text-sm font-semibold', accent.title)}>{run.actor_name}</div>
</div>
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
<CheckCircle2 className="h-4 w-4 text-[#657162]" />
</div>
<div className="mt-2 line-clamp-3 text-sm text-foreground/80">{summary}</div>
<div className="mt-3 flex items-center gap-2 text-[11px] text-muted-foreground">

View File

@ -28,7 +28,7 @@ function renderArtifactBody(artifact: ProcessArtifact, locale: 'zh-CN' | 'en-US'
}
if (artifact.artifact_type === 'link' && artifact.url) {
return (
<a href={artifact.url} target="_blank" rel="noreferrer" className="text-sm text-sky-300 underline break-all">
<a href={artifact.url} target="_blank" rel="noreferrer" className="text-sm text-[#5F5550] underline break-all">
{artifact.url}
</a>
);

View File

@ -3,12 +3,7 @@
import React from 'react';
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { MessageList } from '@/components/chat-workbench/MessageList';
import { ArtifactSidebar } from '@/components/chat-workbench/ArtifactSidebar';
import { ProcessLane } from '@/components/chat-workbench/ProcessLane';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
export function ChatWorkbench({
messages,
@ -33,58 +28,10 @@ export function ChatWorkbench({
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
}) {
const { locale } = useAppI18n();
const [isDesktop, setIsDesktop] = React.useState(() =>
typeof window === 'undefined' ? true : window.matchMedia('(min-width: 1024px)').matches
);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const mediaQuery = window.matchMedia('(min-width: 1024px)');
const updateLayout = () => setIsDesktop(mediaQuery.matches);
updateLayout();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateLayout);
return () => mediaQuery.removeEventListener('change', updateLayout);
}
mediaQuery.addListener(updateLayout);
return () => mediaQuery.removeListener(updateLayout);
}, []);
const selectedRun = selectedRunId
? processRuns.find((item) => item.run_id === selectedRunId) || null
: null;
const selectedRunEvents = selectedRun
? processEvents.filter((item) => item.run_id === selectedRun.run_id)
: [];
const selectedRunArtifacts = selectedRun
? processArtifacts.filter((item) => item.run_id === selectedRun.run_id)
: [];
const hasResultsPanel = Boolean(
selectedRun &&
(
selectedRun.summary ||
selectedRunEvents.length > 0 ||
selectedRunArtifacts.length > 0
)
);
const hasProcessPanel = processRuns.length > 0;
const desktopColumns = hasProcessPanel && hasResultsPanel
? 'grid-cols-[minmax(0,1fr)_340px_360px]'
: hasProcessPanel
? 'grid-cols-[minmax(0,1fr)_340px]'
: hasResultsPanel
? 'grid-cols-[minmax(0,1fr)_360px]'
: 'grid-cols-[minmax(0,1fr)]';
const messageList = (
return (
<div className="h-full">
<MessageList
messages={messages}
isThinking={isThinking}
@ -93,81 +40,11 @@ export function ChatWorkbench({
processRuns={processRuns}
processEvents={processEvents}
processArtifacts={processArtifacts}
selectedRunId={selectedRun?.run_id || null}
selectedRunId={selectedRunId}
onSelectRun={onSelectRun}
onCancelRun={onCancelRun}
onFeedback={onFeedback}
/>
);
if (isDesktop) {
return (
<div className={`grid h-full ${desktopColumns}`}>
<div className="min-h-0">
{messageList}
</div>
{hasProcessPanel && (
<div className="min-h-0">
<ProcessLane
runs={processRuns}
events={processEvents}
selectedRunId={selectedRun?.run_id || null}
onSelectRun={onSelectRun}
onCancelRun={onCancelRun}
/>
</div>
)}
{hasResultsPanel && (
<div className="min-h-0">
<ArtifactSidebar
selectedRun={selectedRun}
events={processEvents}
artifacts={processArtifacts}
/>
</div>
)}
</div>
);
}
return (
<div className="h-full">
{!hasResultsPanel && !hasProcessPanel ? (
messageList
) : (
<Tabs defaultValue="chat" className="h-full flex flex-col">
<div className="px-4 pt-3 border-b border-border">
<TabsList className={`grid w-full ${hasResultsPanel ? 'grid-cols-3' : 'grid-cols-2'}`}>
<TabsTrigger value="chat">{pickAppText(locale, '聊天', 'Chat')}</TabsTrigger>
<TabsTrigger value="process">{pickAppText(locale, '过程', 'Process')}</TabsTrigger>
{hasResultsPanel && (
<TabsTrigger value="results">{pickAppText(locale, '结果', 'Results')}</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="chat" className="flex-1 min-h-0 mt-0">
{messageList}
</TabsContent>
<TabsContent value="process" className="flex-1 min-h-0 mt-0">
<ProcessLane
runs={processRuns}
events={processEvents}
selectedRunId={selectedRun?.run_id || null}
onSelectRun={onSelectRun}
onCancelRun={onCancelRun}
/>
</TabsContent>
{hasResultsPanel && (
<TabsContent value="results" className="flex-1 min-h-0 mt-0">
<ArtifactSidebar
selectedRun={selectedRun}
events={processEvents}
artifacts={processArtifacts}
/>
</TabsContent>
)}
</Tabs>
)}
</div>
);
}

View File

@ -5,34 +5,34 @@ import remarkGfm from 'remark-gfm';
export function MarkdownContent({ content }: { content: string }) {
return (
<div className="prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<div className="prose prose-sm max-w-none text-[#1D1715] prose-headings:text-[#0B0B0B] prose-p:text-[#1D1715] prose-p:leading-7 prose-strong:text-[#0B0B0B] prose-a:text-[#342E2B] prose-a:underline prose-a:decoration-[#B8AEA8] prose-a:underline-offset-4 prose-li:text-[#1D1715] prose-blockquote:border-l-[#D8D2CE] prose-blockquote:text-[#4F4642] prose-code:rounded-md prose-code:bg-[#ECE8E5] prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[#342E2B] prose-pre:border prose-pre:border-[#D8D2CE] prose-pre:bg-[#ECE8E5] prose-pre:text-[#342E2B] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
table: ({ children, ...props }) => (
<div className="my-3 overflow-x-auto rounded-lg border border-border">
<div className="my-3 overflow-x-auto rounded-lg border border-[#D8D2CE]">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
thead: ({ children, ...props }) => (
<thead className="bg-muted/60" {...props}>
<thead className="bg-[#ECE8E5]" {...props}>
{children}
</thead>
),
th: ({ children, ...props }) => (
<th className="px-3 py-2 text-left font-semibold text-foreground border-b border-border" {...props}>
<th className="border-b border-[#D8D2CE] px-3 py-2 text-left font-semibold text-[#0B0B0B]" {...props}>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className="px-3 py-2 border-b border-border/50" {...props}>
<td className="border-b border-[#E7E2DE] px-3 py-2 text-[#1D1715]" {...props}>
{children}
</td>
),
tr: ({ children, ...props }) => (
<tr className="hover:bg-muted/30 transition-colors" {...props}>
<tr className="transition-colors hover:bg-[#F7F5F4]" {...props}>
{children}
</tr>
),

View File

@ -1,7 +1,8 @@
'use client';
import React from 'react';
import { Bot, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react';
import Link from 'next/link';
import { Bot, CheckCircle2, ChevronRight, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react';
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
import { getAccessToken, getFileUrl } from '@/lib/api';
@ -44,24 +45,31 @@ function MessageBubble({
}: {
message: ChatMessage;
canSendFeedback: boolean;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
}) {
const { locale } = useAppI18n();
const isUser = message.role === 'user';
const textContent = typeof message.content === 'string' ? message.content : String(message.content || '');
const [feedbackMode, setFeedbackMode] = React.useState<'satisfied' | 'revise' | null>(null);
const [feedbackComment, setFeedbackComment] = React.useState('');
const validationFailed = message.validation_status === 'failed';
const validationDetails =
validationFailed
? pickAppText(locale, '详细原因会在任务验证区展示;展开任务可查看验证报告。', 'Detailed reasons are shown in the task validation area. Open the task to inspect the validation report.')
: '';
return (
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
{!isUser && (
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
<div className="mt-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-[#F1EFEE]">
<Bot className="w-4 h-4 text-primary" />
</div>
)}
<div
className={`rounded-xl px-4 py-3 max-w-[88%] shadow-sm ${
className={`max-w-[88%] px-4 py-3 ${
isUser
? 'bg-primary text-primary-foreground'
: 'bg-card border border-border/80'
? 'rounded-[28px] bg-primary text-primary-foreground'
: 'rounded-none bg-transparent text-[#1D1715]'
}`}
>
{message.attachments && message.attachments.length > 0 && (
@ -110,42 +118,107 @@ function MessageBubble({
) : (
<MarkdownContent content={textContent} />
)}
{!isUser && message.task_id && (
<div className="mt-3 rounded-md border border-border bg-muted/35 p-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium uppercase text-muted-foreground">Task</div>
<div className="mt-1 truncate text-sm font-medium">
{pickAppText(locale, '已创建任务', 'Task created')}: {message.task_id}
</div>
</div>
<Link
href={`/tasks/${encodeURIComponent(message.task_id)}`}
className="inline-flex h-8 items-center gap-1 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
{pickAppText(locale, '查看任务', 'Open task')}
<ChevronRight className="h-3.5 w-3.5" />
</Link>
</div>
</div>
)}
{!isUser && validationFailed && (
<details className="mt-3 rounded-md border border-destructive/30 bg-destructive/5 p-3">
<summary className="cursor-pointer text-base font-semibold text-destructive">
{pickAppText(locale, '验证失败', 'Validation failed')}
</summary>
<p className="mt-2 text-xs leading-5 text-muted-foreground">{validationDetails}</p>
</details>
)}
{!isUser && canSendFeedback && message.run_id && (
<div className="mt-3 flex flex-wrap items-center gap-2 border-t border-border/70 pt-2">
<div className="mt-3 space-y-2 border-t border-border/70 pt-3">
{message.feedback_state ? (
<span className="text-xs text-muted-foreground">
{message.feedback_state === 'satisfied'
? pickAppText(locale, '已标记满意', 'Marked satisfied')
: message.feedback_state === 'revise'
? pickAppText(locale, '已请求修改', 'Revision requested')
: pickAppText(locale, '已放弃任务', 'Task abandoned')}
</span>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<CheckCircle2 className="h-3.5 w-3.5" />
<span>
{message.feedback_state === 'satisfied'
? pickAppText(locale, '已标记满意', 'Marked satisfied')
: message.feedback_state === 'revise'
? pickAppText(locale, '已请求修改', 'Revision requested')
: pickAppText(locale, '已放弃任务', 'Task abandoned')}
</span>
</div>
) : (
<>
<button
type="button"
onClick={() => onFeedback(message.run_id!, 'satisfied')}
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ThumbsUp className="h-3.5 w-3.5" />
{pickAppText(locale, '满意', 'Satisfied')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, 'revise')}
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<RefreshCcw className="h-3.5 w-3.5" />
{pickAppText(locale, '需要修改', 'Revise')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, 'abandon')}
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<XCircle className="h-3.5 w-3.5" />
{pickAppText(locale, '放弃', 'Abandon')}
</button>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => setFeedbackMode('satisfied')}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ThumbsUp className="h-3.5 w-3.5" />
{pickAppText(locale, '满意', 'Satisfied')}
</button>
<button
type="button"
onClick={() => setFeedbackMode('revise')}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<RefreshCcw className="h-3.5 w-3.5" />
{pickAppText(locale, '需要修改', 'Revise')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, 'abandon')}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<XCircle className="h-3.5 w-3.5" />
{pickAppText(locale, '放弃', 'Abandon')}
</button>
</div>
{feedbackMode && (
<div className="space-y-2 rounded-md border border-border bg-background p-2">
<textarea
value={feedbackComment}
onChange={(event) => setFeedbackComment(event.target.value)}
placeholder={
feedbackMode === 'revise'
? pickAppText(locale, '写下需要修改的地方...', 'Describe what needs to change...')
: pickAppText(locale, '可选:补充说明...', 'Optional note...')
}
className="min-h-20 w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring"
/>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => {
setFeedbackMode(null);
setFeedbackComment('');
}}
className="h-8 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent"
>
{pickAppText(locale, '取消', 'Cancel')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, feedbackMode, feedbackComment.trim() || undefined)}
className="h-8 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
{pickAppText(locale, '提交', 'Submit')}
</button>
</div>
</div>
)}
</>
)}
{message.validation_status && message.validation_status !== 'unknown' && (
@ -162,7 +235,7 @@ function MessageBubble({
)}
</div>
{isUser && (
<div className="w-7 h-7 rounded-full bg-secondary flex items-center justify-center flex-shrink-0 mt-0.5">
<div className="mt-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-secondary">
<User className="w-4 h-4" />
</div>
)}
@ -269,7 +342,7 @@ export function MessageList({
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
}) {
const { locale } = useAppI18n();
const visibleMessages = React.useMemo(
@ -311,12 +384,12 @@ export function MessageList({
.find((message) => message.role === 'assistant' && message.run_id && message.task_id)?.run_id;
return (
<ScrollArea className="h-full px-4" viewportRef={viewportRef}>
<div className="max-w-6xl mx-auto py-4 space-y-4">
<ScrollArea className="h-full px-8" viewportRef={viewportRef}>
<div className="mx-auto max-w-5xl space-y-8 py-10">
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<Bot className="w-12 h-12 mb-4 opacity-50" />
<p className="text-lg font-medium">Boardware Agent Sandbox</p>
<p className="text-lg font-medium text-foreground">Beaver</p>
<p className="text-sm">{pickAppText(locale, '发送消息开始对话', 'Send a message to start the conversation')}</p>
</div>
)}

View File

@ -13,11 +13,11 @@ import { useAppI18n } from '@/lib/i18n/provider';
import { cn } from '@/lib/utils';
function statusTone(status: string) {
if (status === 'done') return 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20';
if (status === 'error') return 'bg-rose-500/10 text-rose-300 border-rose-500/20';
if (status === 'cancelled') return 'bg-zinc-500/10 text-zinc-300 border-zinc-500/20';
if (status === 'waiting') return 'bg-amber-500/10 text-amber-300 border-amber-500/20';
return 'bg-sky-500/10 text-sky-300 border-sky-500/20';
if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]';
if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]';
if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]';
if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]';
return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]';
}
function actorIcon(run: ProcessRun) {
@ -147,7 +147,7 @@ export function ProcessLane({
</div>
))}
{run.status === 'error' && (
<div className="flex items-center gap-2 text-xs text-rose-300">
<div className="flex items-center gap-2 text-xs text-[#5F5550]">
<AlertCircle className="w-3.5 h-3.5" />
{pickAppText(locale, '此任务执行失败。', 'This task failed.')}
</div>
@ -168,8 +168,8 @@ function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
const rawEphemeral = metadata?.ephemeral_skill_names;
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : '';
if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) {
return null;
}
return (
@ -184,9 +184,9 @@ function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
ephemeral:{name}
</Badge>
))}
{draftId && (
{guidanceId && (
<Badge variant="outline" className="text-[10px]">
draft:{draftId.slice(0, 8)}
guidance:{guidanceId.slice(0, 8)}
</Badge>
)}
</div>