feat(coordinator): 添加团队节点默认最大工具迭代次数配置
添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数, 并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。 fix(runtime): 修复团队节点运行成功判断逻辑 更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况 视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。 feat(mcp): 添加团队工作流MCP工具类别支持 增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能, 为团队工作流提供本地工具支持。 refactor(engine): 调整AgentLoop最大工具迭代次数设置 将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100, 同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。 perf(mcp): 优化MCP连接管理避免重复连接 添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次, 提高性能并避免不必要的重复连接。 refactor(skills): 移除技能团队模板相关功能 移除与技能团队模板相关的代码,包括解析、存储和处理逻辑, 简化技能记录结构和加载流程。 feat(process): 增强会话过程投影器功能 添加技能激活快照事件处理,改进团队运行完成消息显示, 并增强技能激活事件的时间戳记录功能。 refactor(tasks): 简化任务尝试编排器团队执行逻辑 移除团队执行相关代码,将所有任务统一按单步执行处理, 简化任务编排器的复杂度并提升执行效率。 fix(evidence): 修复节点证据评估中需求验证逻辑 更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证, 只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
||||
import type { ChatMessage } from '@/types';
|
||||
import { MessageList } from '@/components/chat-workbench/MessageList';
|
||||
|
||||
export function ChatWorkbench({
|
||||
@ -10,11 +10,6 @@ export function ChatWorkbench({
|
||||
isThinking,
|
||||
messagesEndRef,
|
||||
messageViewportRef,
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onFeedback,
|
||||
onRequestRevision,
|
||||
}: {
|
||||
@ -22,11 +17,6 @@ export function ChatWorkbench({
|
||||
isThinking: boolean;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
messageViewportRef: React.RefObject<HTMLDivElement>;
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
processArtifacts: ProcessArtifact[];
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'accept' | 'revise' | 'abandon', comment?: string) => void;
|
||||
onRequestRevision: (runId: string) => void;
|
||||
}) {
|
||||
@ -37,11 +27,6 @@ export function ChatWorkbench({
|
||||
isThinking={isThinking}
|
||||
messagesEndRef={messagesEndRef}
|
||||
viewportRef={messageViewportRef}
|
||||
processRuns={processRuns}
|
||||
processEvents={processEvents}
|
||||
processArtifacts={processArtifacts}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={onSelectRun}
|
||||
onFeedback={onFeedback}
|
||||
onRequestRevision={onRequestRevision}
|
||||
/>
|
||||
|
||||
@ -1,24 +1,245 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Activity, PanelRightOpen, X } from 'lucide-react';
|
||||
import { Activity, CheckCircle2, ChevronDown, Circle, FileText, LoaderCircle, PanelRightOpen, Sparkles, TerminalSquare, Users, X } from 'lucide-react';
|
||||
|
||||
import { TaskTimeline } from '@/components/task-detail';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { formatTaskRuntimeDuration, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import type { TaskTimelineCard } from '@/types';
|
||||
import {
|
||||
buildTaskUiModel,
|
||||
taskUiStatusClass,
|
||||
taskUiStatusLabel,
|
||||
type TaskUiModel,
|
||||
type TaskUiStatus,
|
||||
} from '@/lib/task-ui-model';
|
||||
import { containedLongTextClass, containedPreservedLongTextClass } from '@/lib/text-wrapping';
|
||||
import type { BackendTask, SessionProcessProjection, TaskTimelineCard } from '@/types';
|
||||
|
||||
function StatusBadge({ status }: { status: TaskUiStatus }) {
|
||||
const { locale } = useAppI18n();
|
||||
return (
|
||||
<Badge variant="outline" className={`h-7 rounded-full px-2.5 text-[11px] ${taskUiStatusClass(status)}`}>
|
||||
{taskUiStatusLabel(status, locale)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressCard({
|
||||
icon,
|
||||
title,
|
||||
label,
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
label: string;
|
||||
status: TaskUiStatus;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
return (
|
||||
<section className="min-w-0 rounded-lg border border-[#E6E1DE] bg-white shadow-[0_6px_18px_rgba(31,24,20,0.04)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
className="flex min-h-[52px] w-full min-w-0 items-center gap-3 px-4 py-2.5 text-left"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#F1EFEE] text-[#615854]">{icon}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold text-[#1D1715]">{title}</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-muted-foreground">{label}</span>
|
||||
</span>
|
||||
<StatusBadge status={status} />
|
||||
<ChevronDown className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="border-t border-[#ECE8E5] px-4 py-2.5">
|
||||
<div className="min-w-0">{children}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 flex h-9 w-full items-center justify-center rounded-md border border-[#E6E1DE] bg-[#FBFAF9] text-xs font-medium text-[#615854] hover:bg-[#F4F1EF]"
|
||||
>
|
||||
{pickAppText(locale, '详情', 'Details')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SummarySection({ model }: { model: TaskUiModel }) {
|
||||
return (
|
||||
<p className={`line-clamp-2 text-sm leading-5 text-muted-foreground ${containedPreservedLongTextClass}`}>
|
||||
{model.summary.summary}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillSection({ model }: { model: TaskUiModel }) {
|
||||
const { locale } = useAppI18n();
|
||||
if (model.skills.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">{pickAppText(locale, '暂无 Skill 选择', 'No skill selected yet')}</p>;
|
||||
}
|
||||
const primary = model.skills[0];
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className={`line-clamp-1 text-sm font-medium text-[#1D1715] ${containedLongTextClass}`}>{primary.name}</div>
|
||||
{primary.createdAt ? <div className="mt-1 text-xs text-muted-foreground">{formatTaskRuntimeTime(primary.createdAt, locale)}</div> : null}
|
||||
{primary.summary ? (
|
||||
<p className={`mt-1 line-clamp-1 text-xs leading-5 text-muted-foreground ${containedLongTextClass}`}>{primary.summary}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolsSection({ model }: { model: TaskUiModel }) {
|
||||
const { locale } = useAppI18n();
|
||||
const attempts = model.attempts.filter((attempt) => attempt.tools.length > 0);
|
||||
if (attempts.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">{pickAppText(locale, '暂无工具调用', 'No tool calls yet')}</p>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{attempts.map((attempt) => (
|
||||
<div key={attempt.id} className="min-w-0">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<span>{attempt.title}</span>
|
||||
<span>{attempt.tools.length} calls</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{attempt.tools.map((tool) => (
|
||||
<div key={tool.id} className="grid min-w-0 grid-cols-[minmax(0,1fr)_76px_46px] items-center gap-2">
|
||||
<span className={`text-sm text-[#1D1715] ${containedLongTextClass}`}>
|
||||
{tool.toolName}
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 text-xs ${tool.status === 'done' ? 'text-[#22733A]' : tool.status === 'running' ? 'text-[#B26A00]' : tool.status === 'error' ? 'text-[#9D3D2F]' : 'text-muted-foreground'}`}>
|
||||
{tool.status === 'done' ? <CheckCircle2 className="h-3.5 w-3.5" /> : tool.status === 'running' ? <LoaderCircle className="h-3.5 w-3.5" /> : <Circle className="h-3.5 w-3.5" />}
|
||||
{taskUiStatusLabel(tool.status, locale)}
|
||||
</span>
|
||||
<span className="text-right text-xs text-muted-foreground">{formatSidebarToolDuration(tool, locale)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSidebarToolDuration(tool: TaskUiModel['tools'][number], locale: string): string {
|
||||
if (typeof tool.durationMs === 'number') {
|
||||
return formatTaskRuntimeDuration(tool.durationMs, locale);
|
||||
}
|
||||
if (tool.status === 'running' && tool.createdAt) {
|
||||
const startMs = new Date(tool.createdAt).getTime();
|
||||
if (!Number.isNaN(startMs)) {
|
||||
return formatTaskRuntimeDuration(Date.now() - startMs, locale);
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function flattenAgents(model: TaskUiModel) {
|
||||
const output: Array<{ id: string; name: string; status: TaskUiStatus; depth: number }> = [];
|
||||
const visit = (node: TaskUiModel['agentTree'][number], depth: number) => {
|
||||
output.push({ id: node.runId, name: node.title || node.name, status: node.status, depth });
|
||||
node.children.forEach((child) => visit(child, depth + 1));
|
||||
};
|
||||
model.agentTree.forEach((node) => visit(node, 0));
|
||||
return output;
|
||||
}
|
||||
|
||||
function AgentSection({ model }: { model: TaskUiModel }) {
|
||||
const { locale } = useAppI18n();
|
||||
const rows = flattenAgents(model).slice(0, 6);
|
||||
if (!model.team.hasTeam) {
|
||||
return (
|
||||
<div className="rounded-md border border-[#E6E1DE] bg-[#FBFAF9] px-3 py-2 text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '本轮为 Main Agent 单线程执行,未启动 Agent Team。', 'This run uses the Main Agent only; no Agent Team was started.')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">{pickAppText(locale, 'Agent Team 已启动,等待节点数据', 'Agent Team started; waiting for node data')}</p>;
|
||||
}
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-xs text-[#615854]">
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
<span>{rows[0]?.name}</span>
|
||||
</div>
|
||||
<div className="ml-4 mt-2 space-y-2 border-l border-[#D8D2CE] pl-4">
|
||||
{rows.map((node) => (
|
||||
<div key={node.id} className="grid min-w-0 grid-cols-[14px_minmax(0,1fr)_74px] items-center gap-2" style={{ paddingLeft: `${Math.min(node.depth, 3) * 8}px` }}>
|
||||
<span className="h-px w-3 bg-[#D8D2CE]" />
|
||||
<span className={`text-xs text-[#1D1715] ${containedLongTextClass}`}>{node.name}</span>
|
||||
<span className={`flex items-center gap-1 text-xs ${node.status === 'done' ? 'text-[#22733A]' : node.status === 'running' ? 'text-[#B26A00]' : 'text-muted-foreground'}`}>
|
||||
{node.status === 'done' ? <CheckCircle2 className="h-3.5 w-3.5" /> : node.status === 'running' ? <LoaderCircle className="h-3.5 w-3.5" /> : <Circle className="h-3.5 w-3.5" />}
|
||||
{taskUiStatusLabel(node.status, locale)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusForSummary(task: BackendTask): TaskUiStatus {
|
||||
if (task.status === 'awaiting_acceptance' || task.status === 'closed') return 'done';
|
||||
if (task.status === 'running') return 'running';
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
function primarySkillName(model: TaskUiModel) {
|
||||
return model.skills[0]?.name || '';
|
||||
}
|
||||
|
||||
function hasExecutionStructure(model: TaskUiModel): boolean {
|
||||
return model.team.hasTeam || model.agentTree.length > 0;
|
||||
}
|
||||
|
||||
function toolStatus(model: TaskUiModel): TaskUiStatus {
|
||||
if (model.tools.some((tool) => tool.status === 'running')) return 'running';
|
||||
if (model.tools.some((tool) => tool.status === 'error')) return 'error';
|
||||
return model.tools.length ? 'done' : 'waiting';
|
||||
}
|
||||
|
||||
function agentStatus(model: TaskUiModel): TaskUiStatus {
|
||||
if (model.agentTree.some((node) => node.status === 'running' || node.children.some((child) => child.status === 'running'))) return 'running';
|
||||
if (!model.team.hasTeam) return 'waiting';
|
||||
if (model.agentTree.some((node) => node.status === 'error' || node.children.some((child) => child.status === 'error'))) return 'error';
|
||||
return model.agentTree.length ? 'done' : model.team.status;
|
||||
}
|
||||
|
||||
function ProgressPanel({
|
||||
task,
|
||||
process,
|
||||
cards,
|
||||
isLive,
|
||||
onClose,
|
||||
}: {
|
||||
task: BackendTask | null;
|
||||
process: SessionProcessProjection | null;
|
||||
cards: TaskTimelineCard[];
|
||||
isLive: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const model = task
|
||||
? buildTaskUiModel({
|
||||
task,
|
||||
process: process ?? { runs: [], events: [], artifacts: [] },
|
||||
cards,
|
||||
locale,
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-w-0 flex-col overflow-hidden bg-[#FBFAF9]">
|
||||
@ -48,9 +269,58 @@ function ProgressPanel({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<ScrollArea className="min-h-0 min-w-0 flex-1 overflow-hidden px-4 py-4">
|
||||
<ScrollArea className="min-h-0 min-w-0 flex-1 overflow-hidden px-4 py-2.5">
|
||||
<div className="min-w-0 max-w-full pb-6">
|
||||
<TaskTimeline cards={cards} isLive={isLive} showHeader={false} />
|
||||
{model ? (
|
||||
<div className="space-y-2.5">
|
||||
<ProgressCard
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
title={pickAppText(locale, '任务摘要', 'Task summary')}
|
||||
label={model.summary.title}
|
||||
status={task ? statusForSummary(task) : 'waiting'}
|
||||
>
|
||||
<SummarySection model={model} />
|
||||
</ProgressCard>
|
||||
{model.skills.length > 0 ? (
|
||||
<ProgressCard
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
title={pickAppText(locale, 'Skill 选择', 'Skill selection')}
|
||||
label={primarySkillName(model)}
|
||||
status={model.skills[0]?.status || 'waiting'}
|
||||
>
|
||||
<SkillSection model={model} />
|
||||
</ProgressCard>
|
||||
) : null}
|
||||
{model.tools.length > 0 ? (
|
||||
<ProgressCard
|
||||
icon={<TerminalSquare className="h-4 w-4" />}
|
||||
title={pickAppText(locale, '工具调用', 'Tool calls')}
|
||||
label={pickAppText(locale, `${model.tools.length} 个工具调用`, `${model.tools.length} tool calls`)}
|
||||
status={toolStatus(model)}
|
||||
>
|
||||
<ToolsSection model={model} />
|
||||
</ProgressCard>
|
||||
) : null}
|
||||
{hasExecutionStructure(model) ? (
|
||||
<ProgressCard
|
||||
icon={<Users className="h-4 w-4" />}
|
||||
title={pickAppText(locale, '执行结构', 'Execution structure')}
|
||||
label={
|
||||
model.team.hasTeam
|
||||
? pickAppText(locale, `Agent Team · ${model.team.outcome}`, `Agent Team · ${model.team.outcome}`)
|
||||
: pickAppText(locale, 'Agent run', 'Agent run')
|
||||
}
|
||||
status={agentStatus(model)}
|
||||
>
|
||||
<AgentSection model={model} />
|
||||
</ProgressCard>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-[#DED8D4] bg-white px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '当前会话暂无运行任务', 'No running task in this session')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
@ -58,9 +328,13 @@ function ProgressPanel({
|
||||
}
|
||||
|
||||
export function CurrentSessionProgressSidebar({
|
||||
task,
|
||||
process,
|
||||
cards,
|
||||
isLive,
|
||||
}: {
|
||||
task: BackendTask | null;
|
||||
process: SessionProcessProjection | null;
|
||||
cards: TaskTimelineCard[];
|
||||
isLive: boolean;
|
||||
}) {
|
||||
@ -70,7 +344,7 @@ export function CurrentSessionProgressSidebar({
|
||||
return (
|
||||
<>
|
||||
<aside className="hidden h-full w-[380px] min-w-0 shrink-0 overflow-hidden border-l border-[#E6E1DE] xl:flex">
|
||||
<ProgressPanel cards={cards} isLive={isLive} />
|
||||
<ProgressPanel task={task} process={process} cards={cards} isLive={isLive} />
|
||||
</aside>
|
||||
|
||||
<button
|
||||
@ -91,7 +365,7 @@ export function CurrentSessionProgressSidebar({
|
||||
aria-label={pickAppText(locale, '关闭进度面板', 'Close progress panel')}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 w-[min(92vw,390px)] min-w-0 overflow-hidden border-l border-[#E6E1DE] shadow-2xl">
|
||||
<ProgressPanel cards={cards} isLive={isLive} onClose={() => setMobileOpen(false)} />
|
||||
<ProgressPanel task={task} process={process} cards={cards} isLive={isLive} onClose={() => setMobileOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -4,10 +4,9 @@ import React from '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 type { ChatMessage } from '@/types';
|
||||
import { getAccessToken, getFileUrl } from '@/lib/api';
|
||||
import { getTaskCardMessageIndexes, hasVisibleChatContent, normalizedMessageText, shouldDisplayChatMessage } from '@/lib/chat-messages';
|
||||
import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock';
|
||||
import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
@ -268,14 +267,6 @@ function MessageBubble({
|
||||
);
|
||||
}
|
||||
|
||||
type AgentTeamGroup = {
|
||||
rootRun: ProcessRun;
|
||||
memberRuns: ProcessRun[];
|
||||
startedAt: string;
|
||||
};
|
||||
|
||||
const TERMINAL_RUN_STATUSES = new Set<ProcessRun['status']>(['done', 'error', 'cancelled']);
|
||||
|
||||
function shouldHideSystemAgentMessage(message: ChatMessage): boolean {
|
||||
if (message.role !== 'assistant' || typeof message.content !== 'string') {
|
||||
return false;
|
||||
@ -299,72 +290,11 @@ function shouldHideMessage(message: ChatMessage): boolean {
|
||||
return !shouldDisplayChatMessage(message);
|
||||
}
|
||||
|
||||
function parseTimelineTime(value?: string | null): number | null {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value).getTime();
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function buildAgentTeamGroups(processRuns: ProcessRun[]): AgentTeamGroup[] {
|
||||
const runMap = new Map(processRuns.map((run) => [run.run_id, run]));
|
||||
const groups = new Map<string, AgentTeamGroup>();
|
||||
|
||||
for (const run of processRuns) {
|
||||
if (run.actor_type !== 'agent') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let root = run;
|
||||
const seen = new Set<string>([run.run_id]);
|
||||
let parentId = run.parent_run_id ?? null;
|
||||
while (parentId) {
|
||||
const parent = runMap.get(parentId);
|
||||
if (!parent || seen.has(parent.run_id)) {
|
||||
break;
|
||||
}
|
||||
root = parent;
|
||||
seen.add(parent.run_id);
|
||||
parentId = parent.parent_run_id ?? null;
|
||||
}
|
||||
|
||||
const existing = groups.get(root.run_id);
|
||||
if (existing) {
|
||||
existing.memberRuns.push(run);
|
||||
continue;
|
||||
}
|
||||
groups.set(root.run_id, {
|
||||
rootRun: root,
|
||||
memberRuns: [run],
|
||||
startedAt: root.started_at || run.started_at,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map((group) => ({
|
||||
...group,
|
||||
memberRuns: [...group.memberRuns].sort((a: ProcessRun, b: ProcessRun) => {
|
||||
const at = parseTimelineTime(a.started_at) ?? 0;
|
||||
const bt = parseTimelineTime(b.started_at) ?? 0;
|
||||
return at - bt;
|
||||
}),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const at = parseTimelineTime(a.startedAt) ?? 0;
|
||||
const bt = parseTimelineTime(b.startedAt) ?? 0;
|
||||
return at - bt;
|
||||
});
|
||||
}
|
||||
|
||||
export function MessageList({
|
||||
messages,
|
||||
isThinking,
|
||||
messagesEndRef,
|
||||
viewportRef,
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onFeedback,
|
||||
onRequestRevision,
|
||||
}: {
|
||||
@ -372,11 +302,6 @@ export function MessageList({
|
||||
isThinking: boolean;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
viewportRef: React.RefObject<HTMLDivElement>;
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
processArtifacts: ProcessArtifact[];
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'accept' | 'revise' | 'abandon', comment?: string) => void;
|
||||
onRequestRevision: (runId: string) => void;
|
||||
}) {
|
||||
@ -385,37 +310,6 @@ export function MessageList({
|
||||
() => messages.filter((message) => !shouldHideMessage(message)),
|
||||
[messages]
|
||||
);
|
||||
const teamGroups = React.useMemo(
|
||||
() =>
|
||||
buildAgentTeamGroups(processRuns).filter((group) =>
|
||||
group.memberRuns.some((run) => !TERMINAL_RUN_STATUSES.has(run.status))
|
||||
),
|
||||
[processRuns]
|
||||
);
|
||||
const timelineItems = React.useMemo(() => {
|
||||
const messageItems = visibleMessages.map((message, index) => ({
|
||||
kind: 'message' as const,
|
||||
key: `${message.role}:${message.timestamp || index}:${index}`,
|
||||
sortTime: parseTimelineTime(message.timestamp) ?? Number.MAX_SAFE_INTEGER / 2 + index,
|
||||
order: index,
|
||||
message,
|
||||
messageIndex: index,
|
||||
}));
|
||||
const teamItems = teamGroups.map((group, index) => ({
|
||||
kind: 'team' as const,
|
||||
key: `team:${group.rootRun.run_id}`,
|
||||
sortTime: parseTimelineTime(group.startedAt) ?? Number.MAX_SAFE_INTEGER / 2 + visibleMessages.length + index,
|
||||
order: visibleMessages.length + index,
|
||||
group,
|
||||
}));
|
||||
|
||||
return [...messageItems, ...teamItems].sort((a, b) => {
|
||||
if (a.sortTime !== b.sortTime) {
|
||||
return a.sortTime - b.sortTime;
|
||||
}
|
||||
return a.order - b.order;
|
||||
});
|
||||
}, [teamGroups, visibleMessages]);
|
||||
const taskCardMessageIndexes = React.useMemo(
|
||||
() => getTaskCardMessageIndexes(visibleMessages),
|
||||
[visibleMessages]
|
||||
@ -439,7 +333,7 @@ export function MessageList({
|
||||
return (
|
||||
<ScrollArea className="h-full px-3 sm:px-5 md:px-8" viewportRef={viewportRef}>
|
||||
<div className="mx-auto max-w-5xl space-y-8 py-6 md:py-10">
|
||||
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (
|
||||
{visibleMessages.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 text-foreground">Beaver</p>
|
||||
@ -447,28 +341,16 @@ export function MessageList({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{timelineItems.map((item) =>
|
||||
item.kind === 'message' ? (
|
||||
<MessageBubble
|
||||
key={item.key}
|
||||
message={item.message}
|
||||
showTaskCard={taskCardMessageIndexes.has(item.messageIndex)}
|
||||
canSendFeedback={item.messageIndex === latestFeedbackMessageIndex}
|
||||
onFeedback={onFeedback}
|
||||
onRequestRevision={onRequestRevision}
|
||||
/>
|
||||
) : (
|
||||
<AgentTeamBlock
|
||||
key={item.key}
|
||||
rootRun={item.group.rootRun}
|
||||
memberRuns={item.group.memberRuns}
|
||||
events={processEvents}
|
||||
artifacts={processArtifacts}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={onSelectRun}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{visibleMessages.map((message, index) => (
|
||||
<MessageBubble
|
||||
key={`${message.role}:${message.timestamp || index}:${index}`}
|
||||
message={message}
|
||||
showTaskCard={taskCardMessageIndexes.has(index)}
|
||||
canSendFeedback={index === latestFeedbackMessageIndex}
|
||||
onFeedback={onFeedback}
|
||||
onRequestRevision={onRequestRevision}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isThinking && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground px-1">
|
||||
|
||||
Reference in New Issue
Block a user