feat(web): 智能体会话去噪——单一免责、短标题、技术信息折叠(DEAI-2)
- 「未经核查」只在最终汇报左栏标题保留一处,删「仅供参考」与数据来源说明; 右栏改叫「实际结果」,去掉「运行已完成」「工具成功 N」徽章,计数只列非零项, 状态值用中文标签,不再显示计划 / 记录短 ID - 工具卡片只标会改数据的动作和中高风险;能力 ID、scope、幂等、耗时、调用 ID、 schema 入口收进技术信息;拒绝说明改成一句「要你批准」 - 用量只显示费用与时长,工具调用数、token、模型、会话 ID 进技术信息(超限时照常显示) - 会话标题 shortGoalTitle:去上下文前缀、UUID、JSON 参数,能力 ID 换中文名,取第一句,40 字截断 - 入口去口号:删「交代目标,边做边汇报」与抽屉里的重复快捷键说明 - 对话组件结构不动 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
260f3ab27f
commit
5a717e414c
11 files changed
+181
-101
No files matched your search
@@ -32,6 +32,8 @@ import {
|
|||||||
useDecideApproval,
|
useDecideApproval,
|
||||||
useIsAdminSession,
|
useIsAdminSession,
|
||||||
} from "~/api/agent.ts";
|
} from "~/api/agent.ts";
|
||||||
|
import { DOWNLOAD_STATUS_LABEL, PLAN_STATUS_LABEL } from "~/api/downloads.ts";
|
||||||
|
import { RECORD_STATUS_LABEL } from "~/api/records.ts";
|
||||||
import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/approval-card.tsx";
|
import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/approval-card.tsx";
|
||||||
import { type CitationItem, CitationList } from "~/components/agentui/citations.tsx";
|
import { type CitationItem, CitationList } from "~/components/agentui/citations.tsx";
|
||||||
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
|
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
|
||||||
@@ -51,8 +53,11 @@ import { StreamingCursor, StreamingResponse } from "~/components/agentui/streami
|
|||||||
import { ToolApproval } from "~/components/agentui/tool-approval.tsx";
|
import { ToolApproval } from "~/components/agentui/tool-approval.tsx";
|
||||||
import { ToolResult, ToolResultOutput, type ToolResultStatus } from "~/components/agentui/tool-result.tsx";
|
import { ToolResult, ToolResultOutput, type ToolResultStatus } from "~/components/agentui/tool-result.tsx";
|
||||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||||
|
import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||||
|
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { errorMessage } from "~/lib/api.ts";
|
import { errorMessage } from "~/lib/api.ts";
|
||||||
|
import { humanizeReason } from "~/lib/reasons.ts";
|
||||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||||
import { InlineApproval, OptionsCard } from "./agent-interaction.tsx";
|
import { InlineApproval, OptionsCard } from "./agent-interaction.tsx";
|
||||||
import { ToolCallIo } from "./tool-call-card-io.tsx";
|
import { ToolCallIo } from "./tool-call-card-io.tsx";
|
||||||
@@ -103,16 +108,13 @@ function CapabilityBadges({ id, extra }: { id: string; extra?: ReactNode }) {
|
|||||||
const m = capabilityMeta(id);
|
const m = capabilityMeta(id);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* 只标需要注意的:会改动数据的动作和中高风险;只读、scope、幂等进技术信息 */}
|
||||||
{m ? (
|
{m ? (
|
||||||
<>
|
<>
|
||||||
<StatusBadge>{KIND_LABEL[m.kind] ?? m.kind}</StatusBadge>
|
{m.kind !== "read" ? <StatusBadge>{KIND_LABEL[m.kind] ?? m.kind}</StatusBadge> : null}
|
||||||
<StatusBadge>scope · {m.scope}</StatusBadge>
|
{m.riskLevel !== "low" ? <RiskBadge level={m.riskLevel} /> : null}
|
||||||
<RiskBadge level={m.riskLevel} />
|
|
||||||
{m.idempotent ? <StatusBadge>幂等</StatusBadge> : null}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : null}
|
||||||
<StatusBadge>未登记能力</StatusBadge>
|
|
||||||
)}
|
|
||||||
{extra}
|
{extra}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -197,20 +199,21 @@ export function ToolCallCard({
|
|||||||
<ToolApproval
|
<ToolApproval
|
||||||
status="rejected"
|
status="rejected"
|
||||||
tool={call.capabilityId}
|
tool={call.capabilityId}
|
||||||
title={`服务端拒绝了「${meta?.title ?? call.capabilityId}」`}
|
title={`未执行:${meta?.title ?? "这个操作"}`}
|
||||||
badges={
|
badges={
|
||||||
<CapabilityBadges
|
<CapabilityBadges
|
||||||
id={call.capabilityId}
|
id={call.capabilityId}
|
||||||
extra={<StatusBadge tone="destructive">{authReason ? "授权校验未通过" : "白名单拦截"}</StatusBadge>}
|
extra={<StatusBadge tone="destructive">{authReason ? "需要你批准" : "本次没有开放"}</StatusBadge>}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
reason={authReason ?? call.reason ?? "未授权"}
|
reason={authReason ?? call.reason ?? "未授权"}
|
||||||
explanation={
|
explanation={
|
||||||
authReason
|
authReason
|
||||||
? '有副作用的能力需要有效的持续授权或单次审批,由服务端在执行前核对,模型声称"用户已同意"无效。控制台发起的会话里,需审批的动作会在对应步骤出审批卡;也可以发布收录策略生成持续授权。'
|
? "会改动数据的操作要你批准:在对应步骤的审批卡上批准,或在设置 → 智能体里给它持续授权。"
|
||||||
: "这个能力不在本次运行的白名单内,或已超出调用预算,调用没有执行。"
|
: "这次会话没有开放这个操作,或已用完调用次数。"
|
||||||
}
|
}
|
||||||
parameters={[
|
parameters={[
|
||||||
|
{ id: "cap", label: "能力", value: call.capabilityId },
|
||||||
{ id: "call", label: "调用 ID", value: call.toolCallId },
|
{ id: "call", label: "调用 ID", value: call.toolCallId },
|
||||||
...(audit?.idempotencyKey ? [{ id: "idem", label: "幂等键", value: audit.idempotencyKey }] : []),
|
...(audit?.idempotencyKey ? [{ id: "idem", label: "幂等键", value: audit.idempotencyKey }] : []),
|
||||||
{ id: "at", label: "时间", value: formatDateTime(call.startedAt) },
|
{ id: "at", label: "时间", value: formatDateTime(call.startedAt) },
|
||||||
@@ -222,9 +225,8 @@ export function ToolCallCard({
|
|||||||
return (
|
return (
|
||||||
<ToolResult
|
<ToolResult
|
||||||
status={status}
|
status={status}
|
||||||
tool={call.capabilityId}
|
tool={meta ? null : call.capabilityId}
|
||||||
title={meta?.title ?? call.capabilityId}
|
title={meta?.title ?? call.capabilityId}
|
||||||
meta={durationText(call.startedAt, call.endedAt) ?? undefined}
|
|
||||||
badges={
|
badges={
|
||||||
<CapabilityBadges
|
<CapabilityBadges
|
||||||
id={call.capabilityId}
|
id={call.capabilityId}
|
||||||
@@ -244,12 +246,27 @@ export function ToolCallCard({
|
|||||||
) : call.status === "running" ? (
|
) : call.status === "running" ? (
|
||||||
<p className="text-meta text-muted-foreground">等待结果…</p>
|
<p className="text-meta text-muted-foreground">等待结果…</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
<TechInfo>
|
||||||
<dl className="grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] gap-x-3 gap-y-1 text-meta">
|
<dl className="grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] gap-x-3 gap-y-1 text-meta">
|
||||||
|
<dt className="text-muted-foreground">能力</dt>
|
||||||
|
<dd className="min-w-0 font-mono break-all text-foreground/80">
|
||||||
|
{call.capabilityId}
|
||||||
|
{meta
|
||||||
|
? ` · ${KIND_LABEL[meta.kind] ?? meta.kind} · scope ${meta.scope}${meta.idempotent ? " · 幂等" : ""}`
|
||||||
|
: ""}
|
||||||
|
</dd>
|
||||||
|
{durationText(call.startedAt, call.endedAt) ? (
|
||||||
|
<>
|
||||||
|
<dt className="text-muted-foreground">耗时</dt>
|
||||||
|
<dd className="min-w-0 text-foreground/80">{durationText(call.startedAt, call.endedAt)}</dd>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<dt className="text-muted-foreground">调用 ID</dt>
|
<dt className="text-muted-foreground">调用 ID</dt>
|
||||||
<dd className="min-w-0 font-mono break-all text-foreground/80">{call.toolCallId}</dd>
|
<dd className="min-w-0 font-mono break-all text-foreground/80">{call.toolCallId}</dd>
|
||||||
<dt className="text-muted-foreground">幂等键</dt>
|
<dt className="text-muted-foreground">幂等键</dt>
|
||||||
<dd className="min-w-0 font-mono break-all text-foreground/80">
|
<dd className="min-w-0 font-mono break-all text-foreground/80">
|
||||||
{audit?.idempotencyKey ?? (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
|
{audit?.idempotencyKey ??
|
||||||
|
(meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
|
||||||
</dd>
|
</dd>
|
||||||
{audit?.grantId || audit?.approvalId ? (
|
{audit?.grantId || audit?.approvalId ? (
|
||||||
<>
|
<>
|
||||||
@@ -261,6 +278,7 @@ export function ToolCallCard({
|
|||||||
) : null}
|
) : null}
|
||||||
</dl>
|
</dl>
|
||||||
<ToolCallIo call={call} />
|
<ToolCallIo call={call} />
|
||||||
|
</TechInfo>
|
||||||
</ToolResult>
|
</ToolResult>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -772,7 +790,26 @@ export function BudgetBar({
|
|||||||
? (run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now()) - new Date(run.startedAt).getTime()
|
? (run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now()) - new Date(run.startedAt).getTime()
|
||||||
: undefined);
|
: undefined);
|
||||||
return (
|
return (
|
||||||
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2 sm:grid-cols-4")}>
|
<div className="grid gap-2">
|
||||||
|
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2")}>
|
||||||
|
<Meter
|
||||||
|
label="费用(估算)"
|
||||||
|
used={u.costUsd}
|
||||||
|
max={run.budget.maxCostUsd}
|
||||||
|
format={(n) => formatUsd(n)}
|
||||||
|
exceeded={exceeded === "cost"}
|
||||||
|
/>
|
||||||
|
<Meter
|
||||||
|
label="时长"
|
||||||
|
used={durationMs === undefined ? undefined : Math.round(durationMs / 1000)}
|
||||||
|
max={run.budget.maxDurationSec}
|
||||||
|
format={(n) => `${n} 秒`}
|
||||||
|
exceeded={exceeded === "duration"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 调用次数与 token 超限时直接显示,否则收进技术信息 */}
|
||||||
|
{exceeded === "tool_calls" || exceeded === "tokens" ? (
|
||||||
|
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2")}>
|
||||||
<Meter
|
<Meter
|
||||||
label="工具调用"
|
label="工具调用"
|
||||||
used={toolCalls}
|
used={toolCalls}
|
||||||
@@ -787,20 +824,26 @@ export function BudgetBar({
|
|||||||
format={(n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))}
|
format={(n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))}
|
||||||
exceeded={exceeded === "tokens"}
|
exceeded={exceeded === "tokens"}
|
||||||
/>
|
/>
|
||||||
<Meter
|
</div>
|
||||||
label="时长"
|
) : (
|
||||||
used={durationMs === undefined ? undefined : Math.round(durationMs / 1000)}
|
<TechInfo
|
||||||
max={run.budget.maxDurationSec}
|
items={[
|
||||||
format={(n) => `${n}s`}
|
{
|
||||||
exceeded={exceeded === "duration"}
|
label: "工具调用",
|
||||||
/>
|
value: `${toolCalls}${run.budget.maxToolCalls ? ` / ${run.budget.maxToolCalls}` : ""}`,
|
||||||
<Meter
|
},
|
||||||
label="费用(估算)"
|
{
|
||||||
used={u.costUsd}
|
label: "token",
|
||||||
max={run.budget.maxCostUsd}
|
value:
|
||||||
format={(n) => `$${n.toFixed(3)}`}
|
u.totalTokens === undefined
|
||||||
exceeded={exceeded === "cost"}
|
? null
|
||||||
|
: `${u.totalTokens.toLocaleString("zh-CN")}${run.budget.maxTokens ? ` / ${run.budget.maxTokens.toLocaleString("zh-CN")}` : ""}`,
|
||||||
|
},
|
||||||
|
{ label: "模型", value: run.model ? `${run.provider ? `${run.provider}/` : ""}${run.model}` : null },
|
||||||
|
{ label: "会话 ID", value: run.id, wide: true },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -946,17 +989,25 @@ export function FinalReport({
|
|||||||
...report.downloads.map((d) => ({
|
...report.downloads.map((d) => ({
|
||||||
id: `d${d.downloadTaskId}`,
|
id: `d${d.downloadTaskId}`,
|
||||||
icon: <Download />,
|
icon: <Download />,
|
||||||
title: d.name ?? `下载任务 ${d.downloadTaskId.slice(0, 8)}`,
|
title: d.name ?? "下载任务",
|
||||||
domain: "下载任务",
|
domain: "下载任务",
|
||||||
meta: <StatusBadge tone={PLAN_STATUS_TONE(d.status)}>{d.status}</StatusBadge>,
|
meta: (
|
||||||
|
<StatusBadge tone={PLAN_STATUS_TONE(d.status)}>
|
||||||
|
{DOWNLOAD_STATUS_LABEL[d.status as keyof typeof DOWNLOAD_STATUS_LABEL] ?? d.status}
|
||||||
|
</StatusBadge>
|
||||||
|
),
|
||||||
to: `/console/downloads/${d.downloadTaskId}`,
|
to: `/console/downloads/${d.downloadTaskId}`,
|
||||||
})),
|
})),
|
||||||
...report.organization.map((o) => ({
|
...report.organization.map((o) => ({
|
||||||
id: `o${o.recordId}`,
|
id: `o${o.recordId}`,
|
||||||
icon: <FolderInput />,
|
icon: <FolderInput />,
|
||||||
title: o.targetPath ?? `整理记录 ${o.recordId.slice(0, 8)}`,
|
title: o.targetPath ?? "整理记录",
|
||||||
domain: "整理记录",
|
domain: "整理记录",
|
||||||
meta: <StatusBadge tone={PLAN_STATUS_TONE(o.status)}>{o.status}</StatusBadge>,
|
meta: (
|
||||||
|
<StatusBadge tone={PLAN_STATUS_TONE(o.status)}>
|
||||||
|
{RECORD_STATUS_LABEL[o.status as keyof typeof RECORD_STATUS_LABEL] ?? o.status}
|
||||||
|
</StatusBadge>
|
||||||
|
),
|
||||||
to: `/console/organize/${o.recordId}`,
|
to: `/console/organize/${o.recordId}`,
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
@@ -975,7 +1026,6 @@ export function FinalReport({
|
|||||||
{said ? <Markdown text={said} /> : "(没有文字汇报)"}
|
{said ? <Markdown text={said} /> : "(没有文字汇报)"}
|
||||||
</StreamingResponse>
|
</StreamingResponse>
|
||||||
</LongText>
|
</LongText>
|
||||||
<p className="mt-2 text-meta text-muted-foreground">仅供参考,不作为完成依据。</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 rounded-xl border border-border/70 bg-foreground/[0.02] p-3">
|
<div className="min-w-0 rounded-xl border border-border/70 bg-foreground/[0.02] p-3">
|
||||||
<p className="mb-1.5 flex items-center gap-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
<p className="mb-1.5 flex items-center gap-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||||
@@ -984,19 +1034,19 @@ export function FinalReport({
|
|||||||
) : (
|
) : (
|
||||||
<LoaderCircle aria-hidden className="size-3.5 animate-spin motion-reduce:animate-none" />
|
<LoaderCircle aria-hidden className="size-3.5 animate-spin motion-reduce:animate-none" />
|
||||||
)}
|
)}
|
||||||
系统核实的结果
|
实际结果
|
||||||
</p>
|
</p>
|
||||||
|
{state || count("failed") || denied ? (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{state ? <StatusBadge tone={state.tone}>{state.label}</StatusBadge> : null}
|
{state ? <StatusBadge tone={state.tone}>{state.label}</StatusBadge> : null}
|
||||||
<StatusBadge tone={RUN_STATUS_TONE[run.status]}>运行{RUN_STATUS_LABEL[run.status]}</StatusBadge>
|
{count("failed") ? <StatusBadge tone="destructive">{count("failed")} 步失败</StatusBadge> : null}
|
||||||
<StatusBadge>工具成功 {count("succeeded")}</StatusBadge>
|
{denied ? <StatusBadge tone="destructive">{denied} 步未执行</StatusBadge> : null}
|
||||||
{count("failed") ? <StatusBadge tone="destructive">失败 {count("failed")}</StatusBadge> : null}
|
|
||||||
{denied ? <StatusBadge tone="destructive">被拒绝 {denied}</StatusBadge> : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
{report ? (
|
{report ? (
|
||||||
<>
|
<>
|
||||||
<p className="mt-2 text-caption break-words text-foreground">{report.summary}</p>
|
<p className="mt-2 text-caption break-words text-foreground">{report.summary}</p>
|
||||||
<dl className="mt-2 grid grid-cols-4 gap-2 text-center">
|
<dl className="mt-2 flex flex-wrap gap-2 text-center">
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["已提交", report.submitted],
|
["已提交", report.submitted],
|
||||||
@@ -1004,8 +1054,13 @@ export function FinalReport({
|
|||||||
["进行中", report.inProgress],
|
["进行中", report.inProgress],
|
||||||
["失败", report.failed],
|
["失败", report.failed],
|
||||||
] as const
|
] as const
|
||||||
).map(([k, v]) => (
|
)
|
||||||
<div key={k} className="flex flex-col-reverse rounded-lg border border-border/60 px-1 py-1.5">
|
.filter(([, v]) => v > 0)
|
||||||
|
.map(([k, v]) => (
|
||||||
|
<div
|
||||||
|
key={k}
|
||||||
|
className="flex min-w-16 flex-col-reverse rounded-lg border border-border/60 px-2 py-1.5"
|
||||||
|
>
|
||||||
<dt className="text-meta text-muted-foreground">{k}</dt>
|
<dt className="text-meta text-muted-foreground">{k}</dt>
|
||||||
<dd className="text-lead font-semibold text-foreground tabular-nums">{v}</dd>
|
<dd className="text-lead font-semibold text-foreground tabular-nums">{v}</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -1016,8 +1071,9 @@ export function FinalReport({
|
|||||||
{report.plans.map((p) => (
|
{report.plans.map((p) => (
|
||||||
<li key={p.planId} className="flex min-w-0 items-center gap-2">
|
<li key={p.planId} className="flex min-w-0 items-center gap-2">
|
||||||
<span className="text-muted-foreground">下载计划</span>
|
<span className="text-muted-foreground">下载计划</span>
|
||||||
<code className="min-w-0 truncate font-mono text-foreground/80">{p.planId.slice(0, 8)}</code>
|
<StatusBadge tone={PLAN_STATUS_TONE(p.status)}>
|
||||||
<StatusBadge tone={PLAN_STATUS_TONE(p.status)}>{p.status}</StatusBadge>
|
{PLAN_STATUS_LABEL[p.status as keyof typeof PLAN_STATUS_LABEL] ?? p.status}
|
||||||
|
</StatusBadge>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1036,18 +1092,17 @@ export function FinalReport({
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="mt-2 text-meta text-muted-foreground">
|
|
||||||
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
|
|
||||||
)。「已提交」指已进入下载器,整理成功才算入库。
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 text-meta text-muted-foreground">服务端汇报还没有生成,以任务和整理记录的实际状态为准。</p>
|
<p className="mt-2 text-meta text-muted-foreground">服务端汇报还没有生成,以任务和整理记录的实际状态为准。</p>
|
||||||
)}
|
)}
|
||||||
{run.error ? (
|
{run.error ? (
|
||||||
|
<>
|
||||||
<p className="mt-2 text-caption break-words text-destructive">
|
<p className="mt-2 text-caption break-words text-destructive">
|
||||||
{String((run.error as { message?: unknown }).message ?? JSON.stringify(run.error))}
|
{humanizeReason(String((run.error as { message?: unknown }).message ?? "")) || "会话出错"}
|
||||||
</p>
|
</p>
|
||||||
|
<TechInfo className="mt-1" items={[{ label: "原始错误", value: JSON.stringify(run.error), wide: true }]} />
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
setAgentDrawerOpen,
|
setAgentDrawerOpen,
|
||||||
setAgentDrawerRun,
|
setAgentDrawerRun,
|
||||||
setAgentDrawerTab,
|
setAgentDrawerTab,
|
||||||
|
shortGoalTitle,
|
||||||
showAgentRunInDrawer,
|
showAgentRunInDrawer,
|
||||||
splitContextGoal,
|
|
||||||
useAgentDrawer,
|
useAgentDrawer,
|
||||||
} from "./agent-store.ts";
|
} from "./agent-store.ts";
|
||||||
import { AgentRunOpenContext } from "./agent-subagent-tree.tsx";
|
import { AgentRunOpenContext } from "./agent-subagent-tree.tsx";
|
||||||
@@ -108,7 +108,7 @@ function HistoryRunView({ runId }: { runId: string }) {
|
|||||||
返回列表
|
返回列表
|
||||||
</Button>
|
</Button>
|
||||||
<p className="min-w-0 flex-1 truncate text-caption text-muted-foreground" title={run.data?.run.goal}>
|
<p className="min-w-0 flex-1 truncate text-caption text-muted-foreground" title={run.data?.run.goal}>
|
||||||
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
|
{run.data ? shortGoalTitle(run.data.run.goal) : ""}
|
||||||
</p>
|
</p>
|
||||||
<Button asChild variant="ghost" size="xs" className="shrink-0 text-muted-foreground">
|
<Button asChild variant="ghost" size="xs" className="shrink-0 text-muted-foreground">
|
||||||
<Link to={`/console/agent/${runId}${mock ? "?mock=1" : ""}`} onClick={() => setAgentDrawerOpen(false)}>
|
<Link to={`/console/agent/${runId}${mock ? "?mock=1" : ""}`} onClick={() => setAgentDrawerOpen(false)}>
|
||||||
@@ -163,11 +163,8 @@ export function AgentDrawer() {
|
|||||||
<Bot className="h-4 w-4" />
|
<Bot className="h-4 w-4" />
|
||||||
智能体
|
智能体
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription className={cn("break-words", history && "sr-only")}>
|
<SheetDescription className={cn("break-words", (history || !context?.label) && "sr-only")}>
|
||||||
{context?.label ? `当前对象:${context.label}` : "交代目标,智能体在授权范围内执行并汇报。"}
|
{context?.label ? `当前对象:${context.label}` : `${agentShortcutLabel()} 打开或收起`}
|
||||||
<span className="ml-1 hidden text-muted-foreground/75 sm:inline">
|
|
||||||
({agentShortcutLabel()} 开关,Esc 关闭)
|
|
||||||
</span>
|
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
{context && !history ? <ContextChips context={context} /> : null}
|
{context && !history ? <ContextChips context={context} /> : null}
|
||||||
<DrawerTabs tab={tab} />
|
<DrawerTabs tab={tab} />
|
||||||
@@ -288,7 +285,7 @@ function AgentMiniDock({ runId }: { runId: string }) {
|
|||||||
<Bot aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
<Bot aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
||||||
{status ? <RunStatusBadge status={status} /> : null}
|
{status ? <RunStatusBadge status={status} /> : null}
|
||||||
<span className="min-w-0 flex-1 truncate text-meta text-muted-foreground">
|
<span className="min-w-0 flex-1 truncate text-meta text-muted-foreground">
|
||||||
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
|
{run.data ? shortGoalTitle(run.data.run.goal) : ""}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="ghost" size="icon-xs" aria-label="展开智能体" title="展开" onClick={restoreAgentDrawer}>
|
<Button variant="ghost" size="icon-xs" aria-label="展开智能体" title="展开" onClick={restoreAgentDrawer}>
|
||||||
<Maximize2 />
|
<Maximize2 />
|
||||||
@@ -326,7 +323,7 @@ export function AgentDrawerTrigger({ className }: { className?: string }) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
aria-label="打开智能体"
|
aria-label="打开智能体"
|
||||||
title={`Agent(${agentShortcutLabel()})`}
|
title={`智能体(${agentShortcutLabel()})`}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
minimized && runId ? restoreAgentDrawer() : openAgentDrawer(contextFromPath(pathname), lastAgentDrawerTab())
|
minimized && runId ? restoreAgentDrawer() : openAgentDrawer(contextFromPath(pathname), lastAgentDrawerTab())
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
toolSummary,
|
toolSummary,
|
||||||
} from "./agent-conversation.tsx";
|
} from "./agent-conversation.tsx";
|
||||||
import { matchesHistory, rangeStart } from "./agent-session-list.tsx";
|
import { matchesHistory, rangeStart } from "./agent-session-list.tsx";
|
||||||
import { contextFromPath, contextPrefix, splitContextGoal } from "./agent-store.ts";
|
import { contextFromPath, contextPrefix, shortGoalTitle, splitContextGoal } from "./agent-store.ts";
|
||||||
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
|
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
|
||||||
|
|
||||||
const call = (p: Partial<ToolCallView>): ToolCallView => ({
|
const call = (p: Partial<ToolCallView>): ToolCallView => ({
|
||||||
@@ -185,3 +185,20 @@ describe("会话历史筛选", () => {
|
|||||||
expect(rangeStart("", now)).toBeNull();
|
expect(rangeStart("", now)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shortGoalTitle(会话短标题)", () => {
|
||||||
|
it("去 UUID、JSON 参数,能力 ID 换中文名,取第一句", () => {
|
||||||
|
const t = shortGoalTitle(
|
||||||
|
'在浏览器会话 16be7338-1111-4222-8333-944445555666 上直接调用一次 browser.solve_challenge,参数:{"sessionId":"x","mode":"auto"}',
|
||||||
|
);
|
||||||
|
expect(t).not.toMatch(/16be7338|\{|sessionId/);
|
||||||
|
expect(t.startsWith("在浏览器会话上直接调用一次")).toBe(true);
|
||||||
|
});
|
||||||
|
it("去上下文前缀,超长截断", () => {
|
||||||
|
expect(shortGoalTitle("[上下文 作品:沙丘] 把沙丘:第二部的 4K 收进来。然后再看看别的")).toBe(
|
||||||
|
"把沙丘:第二部的 4K 收进来",
|
||||||
|
);
|
||||||
|
expect(shortGoalTitle("啊".repeat(60))).toBe(`${"啊".repeat(40)}…`);
|
||||||
|
expect(shortGoalTitle(" ")).toBe("未命名会话");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ import { PromptInput } from "~/components/agentui/prompt-input.tsx";
|
|||||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||||
|
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { Label } from "~/components/ui/label.tsx";
|
import { Label } from "~/components/ui/label.tsx";
|
||||||
import { Switch } from "~/components/ui/switch.tsx";
|
import { Switch } from "~/components/ui/switch.tsx";
|
||||||
@@ -115,8 +116,7 @@ export function GoalComposer({
|
|||||||
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
|
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
|
||||||
{proposeFirst
|
{proposeFirst
|
||||||
? "智能体先调查并给出方案,你选定后才执行。"
|
? "智能体先调查并给出方案,你选定后才执行。"
|
||||||
: "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}{" "}
|
: "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}
|
||||||
Enter 发送,Shift+Enter 换行。
|
|
||||||
</p>
|
</p>
|
||||||
{suggestions?.length ? (
|
{suggestions?.length ? (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
@@ -143,10 +143,6 @@ export function GoalComposer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortTokens(n: number | undefined) {
|
|
||||||
return n === undefined ? null : n >= 1000 ? `${(n / 1000).toFixed(1)}k token` : `${n} token`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 状态 + 来源 + 时间 + 模型 */
|
/** 状态 + 来源 + 时间 + 模型 */
|
||||||
function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunStatus }) {
|
function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunStatus }) {
|
||||||
return (
|
return (
|
||||||
@@ -163,7 +159,6 @@ function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunSta
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-meta break-words text-muted-foreground">
|
<p className="text-meta break-words text-muted-foreground">
|
||||||
{TRIGGER_LABEL[run.trigger]} · {formatDateTime(run.createdAt)}
|
{TRIGGER_LABEL[run.trigger]} · {formatDateTime(run.createdAt)}
|
||||||
{run.model ? ` · ${run.provider ?? ""}/${run.model}` : ""}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -194,8 +189,7 @@ function RunOverviewBar({
|
|||||||
const current = steps.find((s) => s.status === "running") ?? steps.find((s) => s.status === "pending");
|
const current = steps.find((s) => s.status === "running") ?? steps.find((s) => s.status === "pending");
|
||||||
const failed = steps.some((s) => s.status === "failed");
|
const failed = steps.some((s) => s.status === "failed");
|
||||||
const u = { ...r.usage, ...(live.usage ?? {}) };
|
const u = { ...r.usage, ...(live.usage ?? {}) };
|
||||||
const calls = Math.max(r.toolCallCount, live.items.filter((i) => i.kind === "tool").length);
|
const usage = u.costUsd ? `费用约 ${formatUsd(u.costUsd)}` : "";
|
||||||
const usage = [`${calls} 次调用`, shortTokens(u.totalTokens)].filter(Boolean).join(" · ");
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="会话概况" className={cn("shrink-0 rounded-xl border border-border/70", className)}>
|
<section aria-label="会话概况" className={cn("shrink-0 rounded-xl border border-border/70", className)}>
|
||||||
<button
|
<button
|
||||||
@@ -240,7 +234,7 @@ function RunOverviewBar({
|
|||||||
<RunPlan plan={live.plan} active={active} fill />
|
<RunPlan plan={live.plan} active={active} fill />
|
||||||
<SubagentTree snapshot={snapshot} className="p-2.5" />
|
<SubagentTree snapshot={snapshot} className="p-2.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-meta text-muted-foreground">预算与用量</p>
|
<p className="mb-2 text-meta text-muted-foreground">用量</p>
|
||||||
<BudgetBar run={r} live={live} />
|
<BudgetBar run={r} live={live} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,8 +456,8 @@ export function AgentRunPanel({
|
|||||||
<RunMeta run={r} status={shownStatus} />
|
<RunMeta run={r} status={shownStatus} />
|
||||||
<RunPlan plan={live.plan} active={streaming || r.status === "waiting"} fill />
|
<RunPlan plan={live.plan} active={streaming || r.status === "waiting"} fill />
|
||||||
<SubagentTree snapshot={run.data} className="p-2.5" />
|
<SubagentTree snapshot={run.data} className="p-2.5" />
|
||||||
<section aria-label="预算与用量" className="mt-auto rounded-xl border border-border/70 p-3">
|
<section aria-label="用量" className="mt-auto rounded-xl border border-border/70 p-3">
|
||||||
<p className="mb-2 text-meta text-muted-foreground">预算与用量</p>
|
<p className="mb-2 text-meta text-muted-foreground">用量</p>
|
||||||
<BudgetBar run={r} live={live} stacked />
|
<BudgetBar run={r} live={live} stacked />
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "re
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { type AgentRunView, RUN_STATUS_LABEL, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
|
import { type AgentRunView, RUN_STATUS_LABEL, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
|
||||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||||
|
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
import { Input } from "~/components/ui/input.tsx";
|
import { Input } from "~/components/ui/input.tsx";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||||
import { cn } from "~/lib/utils.ts";
|
import { cn } from "~/lib/utils.ts";
|
||||||
import { RunStatusBadge } from "./agent-conversation.tsx";
|
import { RunStatusBadge } from "./agent-conversation.tsx";
|
||||||
import { splitContextGoal } from "./agent-store.ts";
|
import { shortGoalTitle, splitContextGoal } from "./agent-store.ts";
|
||||||
|
|
||||||
// 会话历史列表:会话页左栏与抽屉「历史」Tab 共用。状态、来源在服务端筛选(游标翻页),
|
// 会话历史列表:会话页左栏与抽屉「历史」Tab 共用。状态、来源在服务端筛选(游标翻页),
|
||||||
// 搜索与时间在已加载的条目里筛(按创建时间倒序,早于时间范围的页不再加载)。
|
// 搜索与时间在已加载的条目里筛(按创建时间倒序,早于时间范围的页不再加载)。
|
||||||
@@ -57,16 +58,9 @@ export function matchesHistory(
|
|||||||
return run.goal.toLowerCase().includes(q) || run.id.startsWith(q);
|
return run.goal.toLowerCase().includes(q) || run.id.startsWith(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 列表副行:只写费用(token 与调用次数在会话的技术信息里) */
|
||||||
export function usageText(r: Pick<AgentRunView, "toolCallCount" | "usage">) {
|
export function usageText(r: Pick<AgentRunView, "toolCallCount" | "usage">) {
|
||||||
const parts = [`${r.toolCallCount} 次调用`];
|
return r.usage.costUsd ? `费用约 ${formatUsd(r.usage.costUsd)}` : "";
|
||||||
if (r.usage.totalTokens !== undefined)
|
|
||||||
parts.push(
|
|
||||||
r.usage.totalTokens >= 1000
|
|
||||||
? `${(r.usage.totalTokens / 1000).toFixed(1)}k token`
|
|
||||||
: `${r.usage.totalTokens} token`,
|
|
||||||
);
|
|
||||||
if (r.usage.costUsd !== undefined) parts.push(`$${r.usage.costUsd.toFixed(3)}`);
|
|
||||||
return parts.join(" · ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL = "__all__";
|
const ALL = "__all__";
|
||||||
@@ -115,7 +109,7 @@ function RunRow({ r, current, children }: { r: AgentRunView; current: boolean; c
|
|||||||
<>
|
<>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<p className="min-w-0 flex-1 truncate text-caption text-foreground" title={goal.text}>
|
<p className="min-w-0 flex-1 truncate text-caption text-foreground" title={goal.text}>
|
||||||
{goal.text}
|
{shortGoalTitle(r.goal)}
|
||||||
</p>
|
</p>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -130,10 +124,11 @@ function RunRow({ r, current, children }: { r: AgentRunView; current: boolean; c
|
|||||||
{relTime(r.createdAt)}
|
{relTime(r.createdAt)}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
|
{goal.context || usageText(r) ? (
|
||||||
<p className="mt-0.5 truncate text-meta text-muted-foreground tabular-nums">
|
<p className="mt-0.5 truncate text-meta text-muted-foreground tabular-nums">
|
||||||
{goal.context ? `${goal.context.split(" · ")[0]} · ` : ""}
|
{[goal.context?.split(" · ")[0], usageText(r)].filter(Boolean).join(" · ")}
|
||||||
{usageText(r)}
|
|
||||||
</p>
|
</p>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CAPABILITY_CATALOG, type KnownCapabilityId } from "@umt/contracts";
|
||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
// Agent 侧栏抽屉的全局开关与上下文:任意页面调用 openAgentDrawer({ entityId, ... }) 即可带着当前对象打开。
|
// Agent 侧栏抽屉的全局开关与上下文:任意页面调用 openAgentDrawer({ entityId, ... }) 即可带着当前对象打开。
|
||||||
@@ -192,3 +193,29 @@ export function splitContextGoal(goal: string): { context: string | null; text:
|
|||||||
const m = /^\[上下文 ([^\]]*)\]\s*/.exec(goal);
|
const m = /^\[上下文 ([^\]]*)\]\s*/.exec(goal);
|
||||||
return m ? { context: m[1] ?? null, text: goal.slice(m[0].length) } : { context: null, text: goal };
|
return m ? { context: m[1] ?? null, text: goal.slice(m[0].length) } : { context: null, text: goal };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const capabilityTitle = (id: string): string | null =>
|
||||||
|
(CAPABILITY_CATALOG as Record<string, { title?: string }>)[id as KnownCapabilityId]?.title ?? null;
|
||||||
|
|
||||||
|
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
||||||
|
const CAP_RE = /\b[a-z][a-z_]*(?:\.[a-z_]+)+\b/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话标题:从目标生成短标题(DEAI-2)。去掉上下文前缀、UUID、JSON 参数,能力 ID 换成中文名,
|
||||||
|
* 取第一句,超过 40 字截断。完整目标仍在会话里第一条消息。
|
||||||
|
*/
|
||||||
|
export function shortGoalTitle(goal: string, max = 40): string {
|
||||||
|
let t = splitContextGoal(goal).text;
|
||||||
|
t = t.replace(/\s*[{[]\s*["{[][\s\S]*$/, ""); // JSON 参数到结尾
|
||||||
|
t = t.replace(UUID_RE, "");
|
||||||
|
t = t.replace(/\b[0-9a-f]{8}(?:-[0-9a-f]{0,4})*…?/gi, ""); // 截断的短 ID
|
||||||
|
t = t.replace(CAP_RE, (id) => capabilityTitle(id) ?? id);
|
||||||
|
t = t.split(/[\n。!?!?]/)[0] ?? "";
|
||||||
|
t = t
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.replace(/(?<=[\p{Script=Han}(])\s+(?=[\p{Script=Han},。:;、)])/gu, "")
|
||||||
|
.replace(/[,,::;;、\s]*(?:参数|args?)?[::]?\s*$/i, "")
|
||||||
|
.trim();
|
||||||
|
if (!t) return "未命名会话";
|
||||||
|
return t.length > max ? `${t.slice(0, max)}…` : t;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { Link } from "react-router";
|
|||||||
import { agentMockEnabled, type RunSnapshot } from "~/api/agent.ts";
|
import { agentMockEnabled, type RunSnapshot } from "~/api/agent.ts";
|
||||||
import { cn } from "~/lib/utils.ts";
|
import { cn } from "~/lib/utils.ts";
|
||||||
import { RunStatusBadge } from "./agent-conversation.tsx";
|
import { RunStatusBadge } from "./agent-conversation.tsx";
|
||||||
import { splitContextGoal } from "./agent-store.ts";
|
import { shortGoalTitle } from "./agent-store.ts";
|
||||||
|
|
||||||
// 子代理(agent.spawn):会话页显示父会话链接 + 继承信息,以及子会话树(可展开、可跳转)与会话树合计用量。
|
// 子代理(agent.spawn):会话页显示父会话链接 + 继承信息,以及子会话树(可展开、可跳转)与会话树合计用量。
|
||||||
// 未派生过子会话、也不是子会话时不渲染(不显示空的多 Agent 看板)。
|
// 未派生过子会话、也不是子会话时不渲染(不显示空的多 Agent 看板)。
|
||||||
@@ -70,7 +70,7 @@ function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<str
|
|||||||
<span className="mt-0.5 size-4 shrink-0" aria-hidden />
|
<span className="mt-0.5 size-4 shrink-0" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<RunLink id={node.id}>{splitContextGoal(node.goal).text}</RunLink>
|
<RunLink id={node.id}>{shortGoalTitle(node.goal)}</RunLink>
|
||||||
<div className="mt-0.5 flex flex-wrap items-center gap-1.5">
|
<div className="mt-0.5 flex flex-wrap items-center gap-1.5">
|
||||||
<RunStatusBadge status={node.status} />
|
<RunStatusBadge status={node.status} />
|
||||||
<span className="text-meta text-muted-foreground tabular-nums">
|
<span className="text-meta text-muted-foreground tabular-nums">
|
||||||
@@ -116,7 +116,7 @@ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; c
|
|||||||
<CornerLeftUp aria-hidden className="size-3.5" />
|
<CornerLeftUp aria-hidden className="size-3.5" />
|
||||||
父会话
|
父会话
|
||||||
</p>
|
</p>
|
||||||
<RunLink id={parent.id}>{splitContextGoal(parent.goal).text}</RunLink>
|
<RunLink id={parent.id}>{shortGoalTitle(parent.goal)}</RunLink>
|
||||||
<p className="text-meta text-muted-foreground">
|
<p className="text-meta text-muted-foreground">
|
||||||
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
||||||
{budgetText(run.budget as Record<string, number | undefined>)}
|
{budgetText(run.budget as Record<string, number | undefined>)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx";
|
|||||||
import {
|
import {
|
||||||
agentShortcutLabel,
|
agentShortcutLabel,
|
||||||
isAgentShortcut,
|
isAgentShortcut,
|
||||||
splitContextGoal,
|
shortGoalTitle,
|
||||||
toggleAgentDrawer,
|
toggleAgentDrawer,
|
||||||
useAgentDrawer,
|
useAgentDrawer,
|
||||||
} from "~/components/agent/agent-store.ts";
|
} from "~/components/agent/agent-store.ts";
|
||||||
@@ -24,7 +24,7 @@ export function useActiveAgentRuns() {
|
|||||||
const latest = active[0];
|
const latest = active[0];
|
||||||
return {
|
return {
|
||||||
count: active.length,
|
count: active.length,
|
||||||
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${splitContextGoal(latest.goal).text}` : null,
|
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${shortGoalTitle(latest.goal)}` : null,
|
||||||
};
|
};
|
||||||
}, [runs.data]);
|
}, [runs.data]);
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ export function AgentLauncher() {
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-keyshortcuts="Control+J Meta+J"
|
aria-keyshortcuts="Control+J Meta+J"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
title={`${open ? "收起" : "打开"} Agent(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
|
title={`${open ? "收起" : "打开"}智能体(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
|
||||||
style={{ bottom: `calc(${BASE_BOTTOM + lift}px + env(safe-area-inset-bottom))` }}
|
style={{ bottom: `calc(${BASE_BOTTOM + lift}px + env(safe-area-inset-bottom))` }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"pg-float prism-ring group pointer-events-auto fixed right-4 z-40 flex h-14 min-w-14 cursor-pointer items-center justify-center gap-3 rounded-full text-foreground outline-none select-none",
|
"pg-float prism-ring group pointer-events-auto fixed right-4 z-40 flex h-14 min-w-14 cursor-pointer items-center justify-center gap-3 rounded-full text-foreground outline-none select-none",
|
||||||
@@ -179,7 +179,7 @@ export function AgentLauncher() {
|
|||||||
</kbd>
|
</kbd>
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-56 truncate text-meta leading-4 text-muted-foreground">
|
<span className="max-w-56 truncate text-meta leading-4 text-muted-foreground">
|
||||||
{open ? "Esc 关闭" : (summary ?? "交代目标,边做边汇报")}
|
{open ? "Esc 关闭" : (summary ?? "")}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>,
|
</button>,
|
||||||
@@ -196,7 +196,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
|
|||||||
onNavigate?.();
|
onNavigate?.();
|
||||||
toggleAgentDrawer(pathname);
|
toggleAgentDrawer(pathname);
|
||||||
};
|
};
|
||||||
const title = `Agent(${shortcut})${summary ? `\n${summary}` : ""}`;
|
const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`;
|
||||||
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
return (
|
return (
|
||||||
@@ -231,9 +231,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
|
|||||||
智能体
|
智能体
|
||||||
{count ? <PulseDot /> : null}
|
{count ? <PulseDot /> : null}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-meta text-muted-foreground">
|
{count ? <span className="truncate text-meta text-muted-foreground">{count} 个会话进行中</span> : null}
|
||||||
{count ? `${count} 个会话进行中` : "交代目标,边做边汇报"}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
{count ? (
|
{count ? (
|
||||||
<CountBadge count={count} />
|
<CountBadge count={count} />
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
to: "/console/agent",
|
to: "/console/agent",
|
||||||
label: "智能体会话",
|
label: "智能体会话",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
description: "交代目标,智能体在授权内执行并汇报",
|
description: "给智能体一个目标,看它做了什么",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, useParams } from "react-router";
|
|||||||
import { agentMockEnabled, useAgentRun } from "~/api/agent.ts";
|
import { agentMockEnabled, useAgentRun } from "~/api/agent.ts";
|
||||||
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
||||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||||
import { splitContextGoal } from "~/components/agent/agent-store.ts";
|
import { shortGoalTitle } from "~/components/agent/agent-store.ts";
|
||||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default function AgentRunPage() {
|
|||||||
description={
|
description={
|
||||||
run.data ? (
|
run.data ? (
|
||||||
<span className="line-clamp-2 break-words" title={run.data.run.goal}>
|
<span className="line-clamp-2 break-words" title={run.data.run.goal}>
|
||||||
{splitContextGoal(run.data.run.goal).text}
|
{shortGoalTitle(run.data.run.goal)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
"加载中…"
|
"加载中…"
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ export default function AgentPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader title="智能体会话" />
|
||||||
title="智能体会话"
|
|
||||||
description="交代目标,智能体列出步骤后在授权范围内执行并汇报;需要审批的动作停在对应步骤等你批准。"
|
|
||||||
/>
|
|
||||||
{mock ? (
|
{mock ? (
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-caption text-warning">
|
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-caption text-warning">
|
||||||
<FlaskConical className="h-4 w-4 shrink-0" />
|
<FlaskConical className="h-4 w-4 shrink-0" />
|
||||||
|
|||||||
Reference in new issue
Block a user