feat(web): live step list, option choice card and inline approval card; drop run mode switch

- session page shows the agent.plan TodoList (drawer: one-line progress); options and approval
  cards sit at their step; composer has a "先看方案再执行" toggle instead of the mode switch
- settings expose option timeout, timeout action and approval validity; quick goals follow glossary

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 21:07:01 +08:00
1 parent cf2c4ff0e6
commit 9649854ab3
15 files changed
+731 -205

No files matched your search

+45 -9
View File
@@ -12,7 +12,6 @@ import {
AgentRunDetail,
type AgentRunEvent,
type AgentRunMessage,
AgentRunMode,
type AgentRunStatus,
Approval,
type ApprovalStatus,
@@ -33,9 +32,10 @@ import { usePiUpstreams } from "./pi.ts";
export const AGENT_BASE = "/api/v1/agent/runs";
/** 运行模式:execute = 授权内执行;plan_only = 只调查出方案,不执行写能力 */
export const AgentMode = AgentRunMode;
export type AgentMode = AgentRunMode;
/** 会话选项(运行模式已取消):proposeFirst = 先看方案再执行 */
export type AgentSessionOptions = { proposeFirst: boolean };
/** 会话是否要求先看方案再执行(存在 run.scope 里) */
export const isProposeFirst = (run: Pick<AgentRun, "scope">) => run.scope.proposeFirst === true;
export type AgentRunView = AgentRun;
/** 服务端从业务表汇总的目标完成度(不采信模型文本) */
@@ -196,9 +196,10 @@ export async function fetchRunEvents(id: string, after: number, signal?: AbortSi
export function useCreateAgentRun() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: { goal: string; mode: AgentMode }) => {
if (agentMockEnabled()) return mockCreateRun(input.goal, input.mode);
return apiMutate(AGENT_BASE, { body: { goal: input.goal, mode: input.mode } }).then((d) => AgentRun.parse(d));
mutationFn: async (input: { goal: string; proposeFirst?: boolean }) => {
const proposeFirst = input.proposeFirst === true;
if (agentMockEnabled()) return mockCreateRun(input.goal, { proposeFirst });
return apiMutate(AGENT_BASE, { body: { goal: input.goal, proposeFirst } }).then((d) => AgentRun.parse(d));
},
onSuccess: () => qc.invalidateQueries({ queryKey: agentKeys.all }),
});
@@ -230,6 +231,21 @@ export function useAppendAgentMessage() {
});
}
/** 回答方案选择:选一个方案,或都不要(optionId=null + 原因);挂起的会话随即继续 */
export function useChooseOption() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: { runId: string; decisionId: string; optionId: string | null; reason?: string }) => {
if (agentMockEnabled()) return mockDetail(input.runId).run;
return apiMutate(`${AGENT_BASE}/:id/choice`, {
pathParams: { id: input.runId },
body: { decisionId: input.decisionId, optionId: input.optionId, reason: input.reason },
}).then((d) => AgentRun.parse(d));
},
onSuccess: (_d, v) => qc.invalidateQueries({ queryKey: agentKeys.run(v.runId) }),
});
}
export const RUN_STATUS_LABEL: Record<AgentRunStatus, string> = {
queued: "排队中",
running: "运行中",
@@ -292,13 +308,33 @@ export function useDecideApproval() {
id: string;
status: Extract<ApprovalStatus, "approved" | "rejected">;
payloadHash: string;
/** 拒绝理由 / 建议:写进待处理事项,会话内审批时 Agent 按它调整 */
reason?: string;
}) =>
apiMutate(`/approvals/:id`, {
method: "PATCH",
pathParams: { id: input.id },
body: { status: input.status, payloadHash: input.payloadHash },
body: {
status: input.status,
payloadHash: input.payloadHash,
...(input.reason ? { reason: input.reason } : {}),
},
}).then((d) => Approval.parse(d)),
onSuccess: (a) => qc.invalidateQueries({ queryKey: ["agent", "approvals", a.planId] }),
onSuccess: (a) => {
void qc.invalidateQueries({ queryKey: ["agent", "approvals", a.planId] });
void qc.invalidateQueries({ queryKey: ["agent", "approval", a.id] });
},
});
}
/** 单个审批的当前状态(会话内审批卡用;挂起期间事件流里还没有决定结果) */
export function useApproval(id: string | null) {
const mock = agentMockEnabled();
return useQuery({
queryKey: ["agent", "approval", id ?? ""],
enabled: !!id && !mock,
queryFn: ({ signal }) => apiParsed(Approval, "/approvals/:id", { pathParams: { id: id ?? "" }, signal }),
refetchInterval: (q) => (q.state.data?.status === "pending" ? 5 * TTL_SECOND : false),
});
}