feat(web): W4-A wire console to real endpoints

- Agent page uses contracts (AgentRun/AgentRunDetail/AgentGoalReport) instead of a local
  mirror; ?mock=1 stays as an explicit demo switch; run snapshot refreshes on agent.run_status;
  stream auto-follow only while the user stays near the bottom (abort stays reachable)
- settings: Pi provider select incl. fake, new Agent runtime card (budgets, fakeScript)
- download trace uses /tasks?subjectId= and /organization-records?downloadTaskId= instead of
  time-window engine run matching and path matching; new business-task tab with cancel/retry
- workbench download dialog posts /acquisition-plans and shows per-slot rejection reasons
- preference tag revision history; subscription search by title/kind; identity/alias/subscription
  lists show entity titles; slot cards show current asset summary; policy preview sample titles
- DecisionSummary / IdentityRevisionList / ReleaseIdentityResult / TaskAccepted / PolicyPreviewSummary /
  OrganizePlanArtifactContent taken from contracts
- docs: SSE for console (cookie) vs GET /events?after=&limit= JSON replay for API keys

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 21:32:11 +08:00
1 parent 34e8751b45
commit 1023642ef9
30 files changed
+703 -271

No files matched your search

@@ -372,6 +372,9 @@ export function FinalReport({
}
/** 会话流:用户目标 → 流式文本/工具卡片/拒绝/压缩 → 结束 */
/** 末尾标记离视口底部不超过这么多像素算"停在底部" */
const FOLLOW_SLACK_PX = 160;
export function AgentConversation({
run,
live,
@@ -386,9 +389,20 @@ export function AgentConversation({
const endRef = useRef<HTMLDivElement>(null);
const active = isRunActive(run.status) && !live.finished;
const count = live.items.length;
// 有新内容时跟随到底,但只在用户停在底部附近时跟随:往上翻(去点"中止"、看早先的工具卡)后不再把视口拽回去,
// 翻回底部附近自动恢复。捕获阶段监听 scroll,侧栏抽屉里的滚动容器也算。
const stick = useRef(true);
useEffect(() => {
const onScroll = () => {
const el = endRef.current;
if (el) stick.current = el.getBoundingClientRect().top - window.innerHeight < FOLLOW_SLACK_PX;
};
document.addEventListener("scroll", onScroll, { capture: true, passive: true });
return () => document.removeEventListener("scroll", onScroll, { capture: true });
}, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: 有新内容时滚到底
useEffect(() => {
if (active) endRef.current?.scrollIntoView({ block: "nearest" });
if (active && stick.current) endRef.current?.scrollIntoView({ block: "nearest" });
}, [count, active]);
// 追加的用户消息按时间插入
@@ -1,8 +1,11 @@
import { useQueryClient } from "@tanstack/react-query";
import { EventType } from "@umt/contracts";
import { Send, Square } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
type AgentMode,
agentKeys,
agentMockEnabled,
isRunActive,
useAbortAgentRun,
@@ -15,6 +18,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useSse } from "~/lib/sse.ts";
import { cn, formatDateTime } from "~/lib/utils.ts";
import { AgentConversation, BudgetBar, FinalReport, RunStatusBadge } from "./agent-conversation.tsx";
import { useAgentStream } from "./use-agent-stream.ts";
@@ -120,6 +124,16 @@ export function AgentRunPanel({ runId, compact = false }: { runId: string; compa
const abort = useAbortAgentRun();
const append = useAppendAgentMessage();
const [msg, setMsg] = useState("");
const qc = useQueryClient();
// run 状态变化(queued → running → 终态)经 SSE 推送:到达即刷新快照,不等 3 秒轮询
useSse(
[EventType.agentRunStatus],
(e) => {
if ((e.payload as { runId?: string } | null)?.runId === runId)
void qc.invalidateQueries({ queryKey: agentKeys.run(runId) });
},
!agentMockEnabled(),
);
if (run.isPending) return <LoadingState rows={4} />;
if (run.error || !run.data) return <ErrorState plain error={run.error} onRetry={() => void run.refetch()} />;
@@ -129,7 +143,13 @@ export function AgentRunPanel({ runId, compact = false }: { runId: string; compa
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<RunStatusBadge status={live.finished && isRunActive(r.status) ? "running" : r.status} />
<RunStatusBadge
status={
(live.finished && isRunActive(r.status)) || (r.status === "queued" && live.events.length > 0)
? "running"
: r.status
}
/>
<span className="text-meta text-muted-foreground">
{TRIGGER_LABEL[r.trigger]} · {formatDateTime(r.createdAt)}
{r.model ? ` · ${r.provider ?? ""}/${r.model}` : ""}
+14 -2
View File
@@ -288,9 +288,12 @@ export function mockDetail(id: string, op?: { abort?: boolean; append?: string }
const at = Date.now() - e.startedAt;
e.appended.push({
id: uuid(9000 + e.appended.length),
runId: id,
createdAt: iso(Date.now()),
updatedAt: iso(Date.now()),
text: op.append,
status: "pending",
createdBy: "user:mock",
consumedAt: null,
});
const last = e.steps.findIndex((s) => s.body.type === "run_completed" || s.body.type === "run_failed");
@@ -325,9 +328,18 @@ function report(e: Entry, run: AgentRunView): GoalReport | null {
imported: 0,
inProgress: submitted ? 1 : 0,
failed: 0,
plans: submitted ? [{ planId: uuid(7001), status: "submitted" }] : [],
plans: submitted
? [{ planId: uuid(7001), status: "submitted", slotId: uuid(7003), sourceReleaseId: uuid(7004) }]
: [],
downloads: submitted
? [{ downloadTaskId: uuid(7002), status: "downloading", name: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX" }]
? [
{
downloadTaskId: uuid(7002),
planId: uuid(7001),
status: "downloading",
name: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX",
},
]
: [],
organization: [],
modelText: text || null,