feat(web): W4-F rebuild agent conversation and drawer with agentui components
- Port agentui (MIT, ashish200729/agentui@b2b752b) Message/MessageBubble/MessageScroller, StreamingResponse, ToolResult/ToolApproval, TodoList, ApprovalCard, CodeBlock/FileDiff, Citations, PromptInput into components/agentui; drop motion/shiki/Next, switch colors, type scale, radii, durations and easing to semantic tokens, honour reduced motion - Conversation: streaming cursor, collapsible thinking, tool cards with kind/scope/risk/ idempotency badges and audit idempotency key, prominent server-rejection state for both whitelist rejections and unauthorized errors, compaction markers, budget meters with over-limit state, approval cards (admin session only), two-column final report (model said vs system verified with task/record links), follow-scroll with jump-to-bottom - Composer: Enter send / Shift+Enter newline, stop = abort (confirmed), append, execute/plan_only switch, "execute this plan" from finished plan_only runs - Session list sidebar with status filter and usage; reconnect/error banners with retry - Drawer: route-derived context (entity/site/task), minimize to a live mini dock - Fix stream refetch race when the run switches from loading to finished Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
29cd10b27e
commit
8352e5e881
26 files changed
+3390
-585
No files matched your search
+111
-1
@@ -1,4 +1,11 @@
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
AgentGoalReport,
|
||||
AgentRun,
|
||||
@@ -7,11 +14,15 @@ import {
|
||||
type AgentRunMessage,
|
||||
AgentRunMode,
|
||||
type AgentRunStatus,
|
||||
Approval,
|
||||
type ApprovalStatus,
|
||||
AuditEvent,
|
||||
agentRunEventFromRow,
|
||||
cursorPage,
|
||||
} from "@umt/contracts";
|
||||
import { MOCK_RUNS, mockCreateRun, mockDetail, mockEventsAfter } from "~/components/agent/mock.ts";
|
||||
import { TTL_SECOND } from "~/lib/api.ts";
|
||||
import { useSession } from "./auth.ts";
|
||||
import { apiMutate, apiParsed, cleanParams } from "./parse.ts";
|
||||
import { useSettings } from "./settings.ts";
|
||||
|
||||
@@ -242,3 +253,102 @@ export const REPORT_STATE: Record<
|
||||
partial: { label: "部分完成", tone: "warning" },
|
||||
failed: { label: "未达成", tone: "destructive" },
|
||||
};
|
||||
|
||||
// ── 会话页附属数据:审批、幂等键、管理员会话判定 ──
|
||||
|
||||
const ApprovalPage = cursorPage(Approval);
|
||||
|
||||
/** 本次运行产生的计划上的审批(按 report.plans 的 planId 逐个查;空列表不发请求) */
|
||||
export function useRunApprovals(planIds: string[]) {
|
||||
const mock = agentMockEnabled();
|
||||
const results = useQueries({
|
||||
queries: planIds.map((planId) => ({
|
||||
queryKey: ["agent", "approvals", planId],
|
||||
enabled: !mock,
|
||||
queryFn: ({ signal }: { signal: AbortSignal }) =>
|
||||
apiParsed(ApprovalPage, "/approvals", { params: { planId, limit: 20 }, signal }),
|
||||
refetchInterval: 15 * TTL_SECOND,
|
||||
})),
|
||||
});
|
||||
return {
|
||||
items: results.flatMap((r) => r.data?.items ?? []),
|
||||
isPending: results.some((r) => r.isPending && r.fetchStatus !== "idle"),
|
||||
error: results.find((r) => r.error)?.error ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 批准/拒绝审批:回传 payloadHash,服务端比对防挪用;只接受控制台管理员会话 */
|
||||
export function useDecideApproval() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: {
|
||||
id: string;
|
||||
status: Extract<ApprovalStatus, "approved" | "rejected">;
|
||||
payloadHash: string;
|
||||
}) =>
|
||||
apiMutate(`/approvals/:id`, {
|
||||
method: "PATCH",
|
||||
pathParams: { id: input.id },
|
||||
body: { status: input.status, payloadHash: input.payloadHash },
|
||||
}).then((d) => Approval.parse(d)),
|
||||
onSuccess: (a) => qc.invalidateQueries({ queryKey: ["agent", "approvals", a.planId] }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 当前是否为控制台管理员会话(cookie 会话;API Key 不算)。审批按钮只在这里可用 */
|
||||
export function useIsAdminSession(): boolean {
|
||||
const s = useSession();
|
||||
return s.data?.actor?.type === "admin" && s.data.expiresAt !== null;
|
||||
}
|
||||
|
||||
export type InvocationAudit = {
|
||||
capabilityId: string;
|
||||
at: string;
|
||||
outcome: string;
|
||||
idempotencyKey: string | null;
|
||||
durationMs: number | null;
|
||||
grantId: string | null;
|
||||
approvalId: string | null;
|
||||
};
|
||||
|
||||
const AuditPage = cursorPage(AuditEvent);
|
||||
|
||||
/**
|
||||
* 本次运行的能力调用审计(capability.invoke / capability.denied,按时间升序)。
|
||||
* 审计按全站新→旧分页,这里最多翻 3 页、按 payload.agentRunId 过滤;较早的运行可能拿不全,界面按"未取到"处理。
|
||||
*/
|
||||
export function useRunInvocationAudit(runId: string, enabled: boolean, version = 0) {
|
||||
const mock = agentMockEnabled();
|
||||
return useQuery({
|
||||
queryKey: ["agent", "invocation-audit", runId, version],
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: enabled && !mock && !!runId,
|
||||
staleTime: 10 * TTL_SECOND,
|
||||
queryFn: async ({ signal }): Promise<InvocationAudit[]> => {
|
||||
const out: InvocationAudit[] = [];
|
||||
let cursor: string | undefined;
|
||||
for (let page = 0; page < 3; page++) {
|
||||
const d = await apiParsed(AuditPage, "/audit-events", {
|
||||
params: cleanParams({ action: "capability.*", targetType: "capability", limit: 200, cursor }),
|
||||
signal,
|
||||
});
|
||||
for (const e of d.items) {
|
||||
const p = e.payload as Record<string, unknown>;
|
||||
if (p.agentRunId !== runId) continue;
|
||||
out.push({
|
||||
capabilityId: e.targetId ?? "",
|
||||
at: e.at,
|
||||
outcome: String(p.outcome ?? e.action),
|
||||
idempotencyKey: typeof p.idempotencyKey === "string" ? p.idempotencyKey : null,
|
||||
durationMs: typeof p.durationMs === "number" ? p.durationMs : null,
|
||||
grantId: typeof p.grantId === "string" ? p.grantId : null,
|
||||
approvalId: typeof p.approvalId === "string" ? p.approvalId : null,
|
||||
});
|
||||
}
|
||||
if (!d.nextCursor) break;
|
||||
cursor = d.nextCursor;
|
||||
}
|
||||
return out.sort((a, b) => a.at.localeCompare(b.at));
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user