feat(agent): 抽屉与会话页统一成一个对话界面,子会话在父会话里实时可见
- 抽屉和会话页渲染同一套 GoalComposer / AgentRunPanel;抽屉标题即最近会话切换, 头部只有新对话、在页面打开、关闭;去掉右下角悬浮按钮和最小化小窗 - 会话列表合一:对话与巡检混排带类型小标签,搜索 + 一个状态下拉,只列顶层会话 (GET /agent/runs?roots=true,新增可选参数) - 子会话卡片:由镜像的 subagent_* 事件折成,显示任务、状态、当前步骤、用时、调用次数、费用, 可展开看过程、可打开;顶部「N 个子会话运行中」跳到卡片;subagent_completed 加可选 durationMs - 回答去内部字段:汇报规则禁止照抄键值与字段表;展示层 stripInternalFields 把字段行 / 字段表移进技术信息 - 开场建议按页面给 2 条完整问法、点了直接发;指令只留 /先给方案,/重跑 改成「重新运行」按钮 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
dd0f6e2c09
commit
3692793dff
35 files changed
+1454
-1137
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { CAPABILITY_ACTIVITY, CAPABILITY_CATALOG, type KnownCapabilityId } from "@umt/contracts";
|
||||
import type { ApprovalView, OptionsView, StreamItem, ToolCallView } from "./use-agent-stream.ts";
|
||||
import type { ApprovalView, OptionsView, StreamItem, SubagentView, ToolCallView } from "./use-agent-stream.ts";
|
||||
|
||||
// 会话流的排版模型(AGUI-3,纯函数,单测覆盖):
|
||||
// 用户消息 → 一行进度(这一段里的工具调用、思考和过程中的话) → 结论正文 → 没做的事 → 一行实际结果。
|
||||
@@ -178,7 +178,7 @@ export type ActivityItem =
|
||||
export type BodyText = { key: string; text: string; at: string };
|
||||
|
||||
export type TurnPart =
|
||||
| { kind: "segment"; key: string; activity: ActivityItem[]; body: BodyText[] }
|
||||
| { kind: "segment"; key: string; activity: ActivityItem[]; body: BodyText[]; subagents: SubagentView[] }
|
||||
| { kind: "options"; key: string; at: string; view: OptionsView }
|
||||
| { kind: "approval"; key: string; at: string; view: ApprovalView }
|
||||
| { kind: "compaction"; key: string; at: string; item: Extract<StreamItem, { kind: "compaction" }> }
|
||||
@@ -217,7 +217,7 @@ export function buildConversation(
|
||||
const closeSegment = () => {
|
||||
if (!turn) return;
|
||||
if (seg || texts.length) {
|
||||
const s = seg ?? { kind: "segment" as const, key: `s${texts[0]?.key}`, activity: [], body: [] };
|
||||
const s = seg ?? { kind: "segment" as const, key: `s${texts[0]?.key}`, activity: [], body: [], subagents: [] };
|
||||
s.body = texts;
|
||||
turn.parts.push(s);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export function buildConversation(
|
||||
return turn;
|
||||
};
|
||||
const segment = (key: string) => {
|
||||
if (!seg) seg = { kind: "segment", key: `s${key}`, activity: [], body: [] };
|
||||
if (!seg) seg = { kind: "segment", key: `s${key}`, activity: [], body: [], subagents: [] };
|
||||
return seg;
|
||||
};
|
||||
|
||||
@@ -273,6 +273,15 @@ export function buildConversation(
|
||||
// 这次拒绝已经转成审批卡:不再算"没做"
|
||||
t.denied = t.denied.filter((c) => c.capabilityId !== it.view.capabilityId);
|
||||
break;
|
||||
case "subagent": {
|
||||
// 子会话卡片跟在派出它的那段进度行下面(在结论正文之前);不切开进度行,
|
||||
// 一次派两个就是一行「派出了 2 个子会话」+ 两张卡片
|
||||
const s = segment(it.key);
|
||||
for (const x of texts) s.activity.push({ kind: "note", key: x.key, text: x.text, at: x.at });
|
||||
texts = [];
|
||||
s.subagents.push(it.view);
|
||||
break;
|
||||
}
|
||||
case "compaction":
|
||||
closeSegment();
|
||||
t.parts.push({ kind: "compaction", key: it.key, at: it.at, item: it });
|
||||
@@ -376,3 +385,61 @@ export function lastReportText(items: readonly StreamItem[]): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 子会话卡片(纯函数) ──
|
||||
|
||||
export type SubagentState = "running" | "waiting" | "done" | "failed" | "stopped";
|
||||
|
||||
/**
|
||||
* 子会话卡片的状态:镜像里收到 subagent_completed 就按它的结果;还没收到时用快照里的子会话状态
|
||||
* (等待、已停止更新得更及时);都没有按运行中。
|
||||
*/
|
||||
export function subagentState(view: Pick<SubagentView, "status">, nodeStatus?: string | null): SubagentState {
|
||||
switch (view.status ?? nodeStatus) {
|
||||
case "succeeded":
|
||||
return "done";
|
||||
case "failed":
|
||||
case "budget_exceeded":
|
||||
return "failed";
|
||||
case "cancelled":
|
||||
return "stopped";
|
||||
case "waiting":
|
||||
return "waiting";
|
||||
default:
|
||||
return "running";
|
||||
}
|
||||
}
|
||||
|
||||
/** 子会话已运行多久(毫秒):结束的取 durationMs 或起止时间,运行中的到 now */
|
||||
export function subagentElapsed(
|
||||
view: Pick<SubagentView, "startedAt" | "endedAt" | "durationMs">,
|
||||
now: number,
|
||||
finishedAt?: string | null,
|
||||
): number {
|
||||
if (view.durationMs !== null) return view.durationMs;
|
||||
const start = Date.parse(view.startedAt);
|
||||
const end = view.endedAt ? Date.parse(view.endedAt) : finishedAt ? Date.parse(finishedAt) : now;
|
||||
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片上的一句结果:服务端摘要是「核查结论\n 模型汇报(未经核查):…」,有模型汇报时取它的第一句,
|
||||
* 否则取摘要第一行。
|
||||
*/
|
||||
export function subagentResultLine(summary: string | null | undefined): string | null {
|
||||
const t = summary?.trim();
|
||||
if (!t) return null;
|
||||
const model = /模型汇报(未经核查):([\s\S]*)$/.exec(t)?.[1]?.trim();
|
||||
const src = model || t;
|
||||
return (
|
||||
src
|
||||
.split("\n")
|
||||
.find((l) => l.trim())
|
||||
?.trim() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** 会话里的全部子会话卡片(只看直接派出的;按出现顺序) */
|
||||
export function subagentViews(items: readonly StreamItem[]): SubagentView[] {
|
||||
return items.flatMap((i) => (i.kind === "subagent" ? [i.view] : []));
|
||||
}
|
||||
Reference in new issue
Block a user