feat(web): 智能体对话重做(AGUI-3)

- 会话流:用户消息 → 一行进度 → 结论正文 → 没做的事 → 一行实际结果。进度行把一段里的工具调用、思考和过程中的话
  聚成一行(1 次也折叠),按能力 verb/noun 人话计数(「查了站点、2 个下载任务和媒体库 · 1 项没做 · 3 秒」),
  展开每步一行;工具名、调用 ID、幂等键、输入输出进技术信息。去掉回合头、「模型说的」重复卡和系统核实大卡。
- agent.plan / agent.propose_options 不作为工具调用出现(旧事件回放同样跳过);被拒调用不出红卡,
  回合末尾一行「没做:检测站点,需要你批准 [批准并重试]」(非计划类能力建一次性持续授权后重试)。
- 方案卡:每个方案一个按钮,点一下就选定;推荐项第一位、主按钮;按钮写服务端给的动作要点;风险折叠;
  「都不要…」点开才出原因框;选完收成一行。审批卡只留摘要和批准 / 拒绝,计划 ID、hash、版本、到期进技术信息。
- 会话页去掉右侧栏和页头目标,步骤 / 子会话 / 模型 / 预算 / ID 进顶部概况条;输入区只有输入框和发送 / 停止,
  / 指令 /先给方案、/重跑(「先看方案再执行」开关原本只是新建时的本地状态,不需要迁移)。
- 抽屉去掉「当前 / 历史」Tab:头部「历史」弹层(最近 20 条)+「新对话」;上下文显示成名称标签,可去掉。
- 会话列表:搜索 + 全部 / 等你处理 / 失败;等你处理置顶,子会话收在父会话下,正常结束不加徽章。
- 入口页与空态给 3 个快捷目标。mock 加审查报告 3 个示例的演示数据;smoke 改测历史弹层。
- 组件测试:聚合、隐藏、拒绝一行、方案一键选定、历史置顶与子会话、指令。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 22:41:34 +08:00
1 parent 2ab6d452dd
commit 6fcf82afb0
17 files changed
+2381 -1846

No files matched your search

@@ -1,9 +1,11 @@
import {
type AgentOptionItem,
type AgentOptionView,
type AgentPlanStep,
type AgentRunEvent,
type AgentUsageTotals,
CAPABILITY_ACTIVITY,
EventType,
type KnownCapabilityId,
} from "@umt/contracts";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { agentMockEnabled, fetchRunEvents } from "~/api/agent.ts";
@@ -45,7 +47,7 @@ export type StreamItem =
export type OptionsView = {
decisionId: string;
question: string;
options: AgentOptionItem[];
options: AgentOptionView[];
recommendedId: string | null;
stepId: string | null;
expiresAt: string;
@@ -88,7 +90,14 @@ export type AgentStreamState = {
const SSE_TYPES: string[] = [EventType.agentTextDelta, EventType.agentToolCall, EventType.agentRunStatus];
/** 事件 → 展示项:相邻文本增量合并;工具调用按 toolCallId 折叠成一张卡 */
/**
* 不作为工具调用出现的能力(AGUI-3):步骤清单(agent.plan)在概况条里,方案(agent.propose_options)有自己的方案卡。
* 旧会话的事件里照样有它们的 tool_call_*,回放时一并跳过。
*/
export const isHiddenCapability = (id: string | null | undefined) =>
!!id && CAPABILITY_ACTIVITY[id as KnownCapabilityId]?.hidden === true;
/** 事件 → 展示项:相邻文本增量合并;工具调用按 toolCallId 折叠成一项;步骤清单 / 方案的调用不出现 */
export function foldEvents(events: AgentRunEvent[]): StreamItem[] {
const items: StreamItem[] = [];
const tools = new Map<string, ToolCallView>();
@@ -96,6 +105,11 @@ export function foldEvents(events: AgentRunEvent[]): StreamItem[] {
const approvals = new Map<string, ApprovalView>();
for (const e of events) {
const last = items[items.length - 1];
if (
(e.type === "tool_call_start" || e.type === "tool_call_end" || e.type === "tool_call_rejected") &&
isHiddenCapability(e.capabilityId)
)
continue;
switch (e.type) {
case "text_delta":
if (last?.kind === "text") last.text += e.delta;