Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ for (const task_class of ["user_action", undefined, "unknown_future_class"]) {
const action = { ...row, details: attentionDetails({ ...source, task_class: "user_action" }) };
const boundAction = attentionDetails(todoItemSchema.parse({ ...source, task_class: "user_action", bound_agent: "codex-delivery" }));
assert(boundAction.boundAgent === "codex-delivery", "canonical User action actor survives schema and projection");
const reviewSource = {...source, role: "user", task_class: "user_action", action_kind: "review_periodic_report",
capability_binding_ref: `periodic-report-review:r${"a".repeat(64)}`};
assert(attentionDetails(todoItemSchema.parse(reviewSource)).reportReview, "native report edition survives the production schema");
for (const changed of [{role: "agent"}, {task_class: "user_gate"}, {capability_binding_ref: "periodic-report-review:r../other"}])
assert(!attentionDetails(todoItemSchema.parse({...reviewSource, ...changed})).reportReview, "prose or another role cannot select report review");
assert(!canHandleUserAction({ ...row, details: attentionDetails(source) }), "a User gate is decided, not marked done");
const { todo_id: _omitted, ...idless } = source;
assert(!canHandleUserAction({ ...action, details: attentionDetails({ ...idless, task_class: "user_action" }) }), "missing todo_id cannot be written");
Expand Down
22 changes: 22 additions & 0 deletions apps/presentation/dashboard/src/data/report-reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {requestJson} from "./chat";
import {z} from "zod";

const row = z.object({todo_id: z.string(), bundle_sha256: z.string().regex(/^[a-f0-9]{64}$/),
title: z.string(), status: z.string(), created_by: z.string().nullable(), bound_agent: z.string().nullable(),
created_at: z.string().nullable(), updated_at: z.string().nullable(), completed_at: z.string().nullable(),
superseded_by: z.string().nullable()});
export type ReportReviewRow = z.infer<typeof row>;
const page = z.object({ok: z.literal(true), available: z.boolean().default(true),
reason_code: z.string().optional(), items: z.array(row), total: z.number().int().nonnegative(), next_cursor: z.string().nullable()});
const read = z.object({ok: z.literal(true), goal_id: z.string(), review: row,
source_session_id: z.string().regex(/^[a-f0-9]{32}$/).nullable(),
todo: z.record(z.string(), z.unknown()), document: z.object({title: z.string(), generated_at: z.string()}).passthrough(),
generation_id: z.string(), artifacts: z.array(z.object({renderer_id: z.string(), content: z.string(), content_digest: z.string()}))});
export type ReportReviewRead = z.infer<typeof read>;
export async function fetchReportReviews(goalId: string, cursor?: string) {
const query = new URLSearchParams({goal_id: goalId}); if (cursor) query.set("cursor", cursor);
return page.parse(await requestJson<unknown>(`/api/chat/report-reviews?${query}`));
}
export async function readReportReview(goalId: string, todoId: string) {
return read.parse(await requestJson<unknown>(`/api/chat/report-reviews/${encodeURIComponent(todoId)}?${new URLSearchParams({goal_id: goalId})}`));
}
14 changes: 14 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const todoItemSchema = z.object({
watch_only: z.union([z.boolean(), z.string()]).optional().nullable(),
task_domain: z.string().optional().nullable(),
action_kind: z.string().optional().nullable(),
capability_binding_ref: z.string().optional().nullable(),
// Canonical author provenance, distinct from the Agent authorized to reply.
created_by: z.string().optional().nullable(),
claimed_by: z.string().optional().nullable(),
Expand All @@ -114,6 +115,17 @@ export const todoItemSchema = z.object({
{ path: ["todo_id"], message: "Todo without a source index requires todo_id" },
);

export const todoCapabilityActionsSchema = z.object({
schema_version: z.literal("todo_capability_actions_v0"),
total_count: z.number().int().nonnegative(),
groups: z.array(z.object({
action_kind: z.string().min(1),
count: z.number().int().nonnegative(),
latest_at: z.string().optional().nullable(),
items: z.array(todoItemSchema).max(3),
})).default([]),
});

export const todoGroupSchema = z.object({
source_section: z.string().optional().nullable(),
total_count: z.number().optional().default(0),
Expand All @@ -123,6 +135,7 @@ export const todoGroupSchema = z.object({
items: z.array(todoItemSchema).optional().default([]),
deferred_items: z.array(todoItemSchema).optional(),
recent_completed_advancement_items: z.array(todoItemSchema).optional(),
capability_actions: todoCapabilityActionsSchema.optional(),
});

export const todoIndexItemSchema = todoItemSchema.safeExtend({
Expand Down Expand Up @@ -267,6 +280,7 @@ export const projectAssetTodoSummarySchema = z.object({
next_index: z.number().optional().nullable(),
items: z.array(todoItemSchema).optional().default([]),
recent_completed_advancement_items: z.array(todoItemSchema).optional().default([]),
capability_actions: todoCapabilityActionsSchema.optional(),
});

export const dependencyBlockerSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export function AttentionActions({ item, readOnly, callbacks, fallbackAgentId }:
callbacks: PersonalWorkspaceCallbacks;
fallbackAgentId: string;
}) {
const { t } = useWorkspaceI18n();
const { t, locale } = useWorkspaceI18n();
const reportReview = item.details?.reportReview === true;
const [preview, setPreview] = useState<PreviewState>({ status: "idle" });
const scope = JSON.stringify([item.sourceId ?? "", item.goalId, item.todoId]);
const pendingRequest = useRef<{scope: string; token: object | null}>({scope, token: null});
Expand Down Expand Up @@ -83,7 +84,7 @@ export function AttentionActions({ item, readOnly, callbacks, fallbackAgentId }:
? <p className="personal-proposal-state is-error" role="alert"><X size={16} /><span>{t("drawer.attentionPreviewFailed", { error: preview.message })}</span></p>
: pending ? <p className="personal-proposal-state" role="status">{t("drawer.attentionPreparing")}</p> : null;
const reply = callbacks.onReplyToAttention
? <button className="personal-secondary-action" onClick={() => callbacks.onReplyToAttention?.(item, "reply")} type="button"><MessageSquareReply size={16} />{t("drawer.attentionReply")}</button>
? <button className="personal-secondary-action" onClick={() => callbacks.onReplyToAttention?.(item, "reply")} type="button"><MessageSquareReply size={16} />{reportReview ? (locale === "zh-CN" ? "提出反馈" : "Give feedback") : t("drawer.attentionReply")}</button>
: null;
const explain = (label: string) => callbacks.onExplainDecision
? <button onClick={() => void callbacks.onExplainDecision?.(item)} type="button"><MessageCircleQuestion size={16} />{label}</button>
Expand All @@ -110,9 +111,9 @@ export function AttentionActions({ item, readOnly, callbacks, fallbackAgentId }:
}
if (canHandleUserAction(item)) {
return <>
<p className="personal-proposal-explainer">{t(item.details?.unblocksTodoId ? "drawer.userActionUnblocks" : "drawer.userActionReminder")}</p>
{!reportReview ? <p className="personal-proposal-explainer">{t(item.details?.unblocksTodoId ? "drawer.userActionUnblocks" : "drawer.userActionReminder")}</p> : null}
<div className="personal-decision-bar" role="group" aria-label={t("drawer.userActionGroup")}>
<button className="personal-primary-action" disabled={pending} onClick={() => previewUserAction(item, "complete")} type="button"><Check size={17} />{t("drawer.userActionComplete")}</button>
<button className="personal-primary-action" disabled={pending} onClick={() => previewUserAction(item, "complete")} type="button"><Check size={17} />{reportReview ? (locale === "zh-CN" ? "验收这份材料" : "Accept this material") : t("drawer.userActionComplete")}</button>
{reply}
</div>
{feedback}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { WorkspaceAttention } from "./personal-workspace-model";
import { todoRequestContent } from "../../../../../../loopx/control_plane/presentation/todo_request_content.js";
import {reportReviewDigest} from "../../../../../../loopx/control_plane/presentation/report_review_binding.js";

/** A display of existing Todo facts, never a gate or dependency evaluator. */
export type AttentionDetails = {
Expand All @@ -18,6 +19,8 @@ export type AttentionDetails = {
unblocksTodoId: string | null;
decisionScope: { kind: string; granularity: string; scopeKey: string } | null;
supersededBy: string | null;
reportReview?: boolean;
reportSessionId?: string;
};

const text = (value: unknown): string | null => typeof value === "string" && value.trim() ? value.trim() : null;
Expand Down Expand Up @@ -47,6 +50,7 @@ export function attentionDetails(todo: Record<string, unknown>): AttentionDetail
unblocksTodoId: text(todo.unblocks_todo_id),
decisionScope: kind && granularity && scopeKey ? { kind, granularity, scopeKey } : null,
supersededBy,
...(reportReviewDigest(todo) ? {reportReview: true} : {}),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
composition_mode: { label: "Composition policy", description: "Replan requires an exact experiment successor or typed result; it grants no execution authority.", options: {disabled: "Off", explicit_only: "Explicit candidates"} },
composition_scope_id: { label: "Research coverage scope", description: "An opaque scope id required by the explicit-only composition policy." },
profile_preset: { label: "Report profile", description: "Capability-owned report profile, such as weekly-progress." },
review_required: { label: "Request review when ready", description: "For reports returned to this Goal's original conversation. Each edition stays on Home until you handle its review." },
wait_for_ci: { label: "Wait for CI", description: "Disable to use local validation without querying or waiting for CI. Merge authority is unchanged." },
review_order: { label: "Review direction", description: "Forward visits other authors first and oldest first. Reverse inverts the whole actionable queue." },
agent_orders: { label: "Agent review directions", description: "Use the Goal default or choose a direction for each registered Agent." },
Expand Down Expand Up @@ -214,6 +215,7 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
composition_mode: { label: "组合策略", description: "重新规划需要精确的实验后继或类型化结果;它不授予执行权限。", options: {disabled: "关闭", explicit_only: "仅显式候选"} },
composition_scope_id: { label: "研究覆盖范围", description: "显式组合策略要求填写不含私有内容的范围标识。" },
profile_preset: { label: "报告 Profile", description: "由该能力管理的报告 profile,例如 weekly-progress。" },
review_required: { label: "材料就绪后请求验收", description: "适用于回到本 Goal 原工作对话的报告。每个版本独立提醒,处理审阅后从首页移除。" },
wait_for_ci: { label: "等待 CI", description: "关闭后使用本地验证,不查询或等待 CI;不改变合并权限。" },
review_order: { label: "审阅方向", description: "正向先审其他作者,同层先审较早就绪的 PR;反向将整个可执行队列倒序。" },
agent_orders: { label: "Agent 审阅方向", description: "每个注册 Agent 可继承 Goal,或单独选择正向、反向。" },
Expand Down
Loading
Loading