feat(agent): in-drawer history tab and denser session layout (AGUI-2)

- Drawer tabs 当前/历史: searchable run list (status/source/time filters,
  cursor auto-load), opens runs inside the drawer, keeps scroll and filters,
  remembers the last tab; 在页面打开 is secondary
- Ended runs: append to resume, or 用同目标新建 with 先看方案再执行 toggle
- Session page: 88ch conversation column, turns grouped with one timestamp,
  tool cards collapse to one line, 3+ activities fold; steps/subagents/budget
  moved to a right rail (collapsible overview bar on narrow screens and in
  the drawer), long model reports fold behind 展开全文
- Smoke covers the drawer history flow

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 15:17:37 +08:00
1 parent ac478673bc
commit 20b63db254
14 files changed
+1234 -349

No files matched your search

@@ -1,6 +1,6 @@
import type { AgentFamilyUsage, AgentRunNode } from "@umt/contracts";
import { ChevronRight, CornerLeftUp, GitBranch } from "lucide-react";
import { useState } from "react";
import { createContext, type ReactNode, useContext, useState } from "react";
import { Link } from "react-router";
import { agentMockEnabled, type RunSnapshot } from "~/api/agent.ts";
import { cn } from "~/lib/utils.ts";
@@ -20,6 +20,27 @@ export function usageLine(u: { toolCalls: number; totalTokens?: number; costUsd?
const href = (id: string) => `/console/agent/${id}${agentMockEnabled() ? "?mock=1" : ""}`;
/** 抽屉里提供:点父/子会话在抽屉内打开,不跳页 */
export const AgentRunOpenContext = createContext<((runId: string) => void) | null>(null);
const LINK_CLASS =
"line-clamp-2 cursor-pointer rounded-tight text-left text-caption break-words text-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring";
function RunLink({ id, children }: { id: string; children: ReactNode }) {
const open = useContext(AgentRunOpenContext);
if (open)
return (
<button type="button" onClick={() => open(id)} className={LINK_CLASS}>
{children}
</button>
);
return (
<Link to={href(id)} className={LINK_CLASS}>
{children}
</Link>
);
}
function budgetText(b: Record<string, number | undefined>) {
const parts: string[] = [];
if (b.maxToolCalls !== undefined) parts.push(`${b.maxToolCalls} 次调用`);
@@ -49,12 +70,7 @@ function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<str
<span className="mt-0.5 size-4 shrink-0" aria-hidden />
)}
<div className="min-w-0 flex-1">
<Link
to={href(node.id)}
className="line-clamp-2 rounded-tight text-caption break-words text-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
>
{splitContextGoal(node.goal).text}
</Link>
<RunLink id={node.id}>{splitContextGoal(node.goal).text}</RunLink>
<div className="mt-0.5 flex flex-wrap items-center gap-1.5">
<RunStatusBadge status={node.status} />
<span className="text-meta text-muted-foreground tabular-nums">
@@ -82,7 +98,7 @@ function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<str
}
/** 子会话树 + 父会话链接;两者都没有时返回 null */
export function SubagentTree({ snapshot }: { snapshot: RunSnapshot }) {
export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; className?: string }) {
const { run, parent, children = [], familyUsage } = snapshot;
if (!parent && children.length === 0) return null;
const byParent = new Map<string, AgentRunNode[]>();
@@ -93,19 +109,14 @@ export function SubagentTree({ snapshot }: { snapshot: RunSnapshot }) {
const direct = byParent.get(run.id) ?? [];
const caps = ((run.scope as { capabilities?: string[] }).capabilities ?? []).length;
return (
<section aria-label="子会话" className="space-y-2 rounded-xl border border-border/70 p-3">
<section aria-label="子会话" className={cn("space-y-2 rounded-xl border border-border/70 p-3", className)}>
{parent ? (
<div className="space-y-1">
<p className="flex items-center gap-1.5 text-meta text-muted-foreground">
<CornerLeftUp aria-hidden className="size-3.5" />
父会话
</p>
<Link
to={href(parent.id)}
className="line-clamp-2 rounded-tight text-caption break-words text-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
>
{splitContextGoal(parent.goal).text}
</Link>
<RunLink id={parent.id}>{splitContextGoal(parent.goal).text}</RunLink>
<p className="text-meta text-muted-foreground">
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
{budgetText(run.budget as Record<string, number | undefined>)}
@@ -115,7 +126,7 @@ export function SubagentTree({ snapshot }: { snapshot: RunSnapshot }) {
) : null}
{direct.length ? (
<div className="space-y-1">
<p className="flex items-center gap-1.5 text-meta text-muted-foreground">
<p className="flex flex-wrap items-center gap-1.5 text-meta text-muted-foreground">
<GitBranch aria-hidden className="size-3.5" />
子会话 {children.length} 个{familyUsage ? <FamilyUsage usage={familyUsage} /> : null}
</p>