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
22 changes: 22 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,28 @@ export function readLoopXTeamWork(sessionId: string, operationId: string) {
method: "POST", body: JSON.stringify({operation: "read", operation_id: operationId}),
});
}
export type ManagedGoalResultRow = {
todo_id: string; title: string; producer_agent_id: string; sha256: string;
content_type: string; size_bytes: number; completed_at?: string | null;
};
export type ManagedGoalResultPage = {
ok: true; items: ManagedGoalResultRow[]; total: number; next_cursor: string | null;
unavailable_count: number; unavailable_todo_ids: string[];
};
export type ManagedGoalResultRead = {
ok: true; goal_id: string; todo_id: string; text: string;
result: {sha256: string; content_type: string; producer_agent_id: string};
};
export function fetchManagedGoalResults(goalId: string, cursor?: string) {
const params = new URLSearchParams({goal_id: goalId});
if (cursor) params.set("cursor", cursor);
return requestJson<ManagedGoalResultPage>(`/api/chat/goal-results?${params}`);
}
export function readManagedGoalResult(goalId: string, todoId: string) {
return requestJson<ManagedGoalResultRead>(
`/api/chat/goal-results/${encodeURIComponent(todoId)}?goal_id=${encodeURIComponent(goalId)}`,
);
}
// Keep inventory and selected-operation labels consistent; unknown states stay unknown.
export function delegationStateLabel(row: {status: string; worker_active?: boolean; recovery_required: boolean | null}, zh: boolean) {
if (row.status === "unavailable") return zh ? "无法核验" : "Unavailable";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
.goal-team-results p { font-size: 12px; line-height: 1.7; color: var(--pw-muted); }
.goal-team-results [role="alert"] { color: var(--pw-red, #b42318); }
.goal-team-results button, .goal-team-results select { min-height: 44px; padding: 8px 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-surface, #fff); color: inherit; font: inherit; cursor: pointer; }
.goal-managed-results > header button { flex-shrink: 0; white-space: nowrap; }
.goal-team-results button[aria-pressed="true"] { border-color: var(--pw-text); }
.goal-team-results button:disabled { opacity: .5; cursor: default; }
.goal-team-results summary { cursor: pointer; padding: 12px 0; font-size: 12px; min-height: 44px; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {useEffect, useRef, useState} from "react";
import {FileText, RefreshCw} from "lucide-react";
import {
fetchManagedGoalResults, readManagedGoalResult,
type ManagedGoalResultPage, type ManagedGoalResultRow, type ManagedGoalResultRead,
} from "../../data/chat";
import {TeamArtifactReport, managedReportArtifact} from "./team-artifact-content";

/** Goal-scoped local reports; an inventory row never stands in for exact acceptance readback. */
export function GoalManagedResults({goalId, zh}: {goalId: string; zh: boolean}) {
const [page, setPage] = useState<ManagedGoalResultPage | null>(null);
const [selected, setSelected] = useState<{row: ManagedGoalResultRow; read: ManagedGoalResultRead} | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const generation = useRef(0);
const chosen = useRef<{todoId: string; sha256: string} | null>(null);
const reader = useRef<HTMLDivElement>(null);

useEffect(() => {
chosen.current = null;
void load();
return () => {generation.current++;};
}, [goalId]);

async function read(row: ManagedGoalResultRow, current: number, focus = false) {
const result = await readManagedGoalResult(goalId, row.todo_id);
if (current !== generation.current) return;
if (result.todo_id !== row.todo_id || result.goal_id !== goalId ||
result.result.sha256 !== row.sha256 || result.result.producer_agent_id !== row.producer_agent_id) {
throw new Error(zh ? "报告版本或验收已变化" : "Report version or acceptance changed");
}
chosen.current = {todoId: row.todo_id, sha256: row.sha256};
setSelected({row, read: result});
if (focus) window.requestAnimationFrame(() => reader.current?.focus());
}

async function load(cursor?: string) {
const current = ++generation.current;
if (cursor) chosen.current = null;
setBusy(true); setError(""); setSelected(null);
try {
const next = await fetchManagedGoalResults(goalId, cursor);
if (current !== generation.current) return;
if (!Array.isArray(next.items) || !Number.isInteger(next.total) ||
!Number.isInteger(next.unavailable_count) ||
!Array.isArray(next.unavailable_todo_ids) ||
next.unavailable_count !== next.unavailable_todo_ids.length ||
(next.next_cursor !== null && typeof next.next_cursor !== "string")) {
throw new Error(zh ? "报告列表响应不完整" : "Report inventory response is incomplete");
}
setPage(next);
const previous = chosen.current;
const row = previous
? next.items.find(item => item.todo_id === previous.todoId && item.sha256 === previous.sha256)
: next.items[0];
if (previous && !row) {
setError(zh ? "上次报告已不在当前验收结果中。" : "The previous report is no longer in current accepted results.");
} else if (row) {
await read(row, current);
}
} catch (failure) {
if (current === generation.current) {
setPage(null);
setError(`${zh ? "无法核验报告;旧内容已清除。" : "Cannot verify report; previous content was cleared."} ${String(failure)}`);
}
} finally {
if (current === generation.current) setBusy(false);
}
}

async function select(row: ManagedGoalResultRow) {
const current = ++generation.current;
chosen.current = {todoId: row.todo_id, sha256: row.sha256};
setBusy(true); setError(""); setSelected(null);
try {await read(row, current, true);}
catch (failure) {
if (current === generation.current) setError(`${zh ? "报告或验收已变化;旧内容已清除。" : "Report or acceptance changed; previous content was cleared."} ${String(failure)}`);
} finally {if (current === generation.current) setBusy(false);}
}

const artifact = selected ? managedReportArtifact(
selected.row.content_type, selected.row.sha256, selected.read.text) : null;
return <section className="goal-team-results goal-managed-results" aria-label={zh ? "已验收的团队报告" : "Accepted team reports"} aria-busy={busy}>
<header><div><h3>{zh ? "团队报告" : "Team reports"}</h3>
<p>{zh ? "只有仍能通过当前验收的报告会出现在这里。" : "Only reports that still pass current acceptance appear here."}</p></div>
<button type="button" disabled={busy} onClick={() => void load()}><RefreshCw size={14} aria-hidden="true"/>{zh ? "刷新" : "Refresh"}</button></header>
{busy ? <p role="status">{zh ? "正在核验报告…" : "Verifying reports…"}</p> : null}
{error ? <p role="alert">{error}</p> : null}
{page && page.unavailable_count > 0 ? <p role="status">{zh
? `本页有 ${page.unavailable_count} 份报告已无法通过当前核验。`
: `${page.unavailable_count} report(s) on this page cannot pass current verification.`}</p> : null}
{page && !busy && !page.items.length ? <p>{page.next_cursor
? (zh ? "本页没有可核验的报告,可继续下一页。" : "No verifiable reports on this page; continue to the next page.")
: (zh ? "暂无可核验的团队报告。" : "No verifiable team reports yet.")}</p> : null}
{page?.next_cursor && !page.items.length ? <button type="button" disabled={busy} onClick={() => void load(page.next_cursor!)}>
{zh ? "下一页" : "Next page"}
</button> : null}
{page && page.items.length > 0 ? <div className="goal-team-results-layout">
<nav className="goal-team-result-list" aria-label={zh ? "选择团队报告" : "Choose a team report"}>
{page.items.map(row => <button type="button" key={row.todo_id} disabled={busy}
aria-pressed={selected?.row.todo_id === row.todo_id} onClick={() => void select(row)}>
<FileText size={16} aria-hidden="true"/><span><strong>{row.title}</strong><small>{row.producer_agent_id}</small></span>
</button>)}
{page.next_cursor ? <button type="button" disabled={busy} onClick={() => void load(page.next_cursor!)}>
{zh ? "下一页" : "Next page"}
</button> : null}
</nav>
{artifact && selected ? <div ref={reader} tabIndex={-1} className="goal-team-result-reader">
<TeamArtifactReport key={`${selected.row.todo_id}:${artifact.sha256}`} artifact={artifact}
zh={zh} heading={selected.row.title}/>
<p>{zh ? "验收任务" : "Accepted Todo"}: <code>{selected.row.todo_id}</code></p>
</div> : null}
</div> : null}
</section>;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {useEffect, useState} from "react";
import {ChatApiError, fetchChatSessions, fetchLoopXMode, fetchLoopXTeamWork, readLoopXTeamWork} from "../../data/chat";
import {TeamArtifactReport, isMarkdownArtifact, type TeamArtifact} from "./team-artifact-content";
import {ChatApiError, fetchChatSessions, fetchLoopXMode, fetchLoopXTeamWork, fetchManagedGoalResults, readLoopXTeamWork, readManagedGoalResult, type ManagedGoalResultRow} from "../../data/chat";
import {TeamArtifactReport, isMarkdownArtifact, managedReportArtifact, type TeamArtifact} from "./team-artifact-content";

type Readback = {kind: "waiting" | "unavailable" | "multiple"} | {
kind: "adopted"; artifact: TeamArtifact; agentId: string;
};
type ManagedReadback = {kind: "waiting" | "unavailable" | "multiple"} | {
kind: "accepted"; artifact: TeamArtifact; agentId: string; title: string;
};

/**
* The Todo identities a Goal conversation itself reports work for, or the
Expand All @@ -21,6 +24,11 @@ const WORK_INDEX_WINDOW_MS = 30_000;
const RELATED_SESSION_LIMIT = 8;
const workIndexes = new Map<string, GoalWorkIndex>();
const workIndexReads = new Map<string, Promise<GoalWorkIndex>>();
/** Goal-wide inventory plus the exact Todo ids the server could not verify, so a
* plan scopes its readback to its own ids instead of the whole Goal's health. */
type ManagedGoalIndex = {readAt: number; rows: ManagedGoalResultRow[]; unavailableTodoIds: Set<string>};
const managedIndexes = new Map<string, ManagedGoalIndex>();
const managedIndexReads = new Map<string, Promise<ManagedGoalIndex>>();

/** A 4xx is the server declining this conversation's team readback: without a
* coordinator identity it cannot own delegation work, so it is unrelated rather
Expand Down Expand Up @@ -154,11 +162,87 @@ async function readAdoptedResult(goalId: string, todoIds: Set<string>, force: bo
return adopted.values().next().value ?? {kind: "waiting"};
}

async function collectManagedGoalIndex(goalId: string): Promise<ManagedGoalIndex> {
let cursor: string | undefined;
let total: number | undefined;
const rows: ManagedGoalResultRow[] = [];
const unavailableTodoIds = new Set<string>();
// Page until the snapshot ends. The previous fixed eight-page budget hid a
// matching report that happened to sort later in the Goal's history.
for (;;) {
const page = await fetchManagedGoalResults(goalId, cursor);
if (!Array.isArray(page.items) || !Number.isInteger(page.total) || page.total < 0 ||
(total !== undefined && page.total !== total) ||
!Number.isInteger(page.unavailable_count) || page.unavailable_count < 0 ||
!Array.isArray(page.unavailable_todo_ids) ||
page.unavailable_count !== page.unavailable_todo_ids.length ||
(page.next_cursor !== null && (!page.next_cursor || typeof page.next_cursor !== "string"))) {
throw new Error("managed inventory incomplete");
}
total = page.total;
rows.push(...page.items);
for (const todoId of page.unavailable_todo_ids) {
if (typeof todoId !== "string" || !todoId) throw new Error("managed inventory incomplete");
unavailableTodoIds.add(todoId);
}
if (!page.next_cursor) return {readAt: Date.now(), rows, unavailableTodoIds};
if (page.next_cursor === cursor) throw new Error("managed cursor repeated");
if (page.items.length === 0 && page.unavailable_count === 0) {
throw new Error("managed inventory made no progress");
}
cursor = page.next_cursor;
}
}

/** Share the Goal inventory across plan cards; manual refresh always bypasses the short cache. */
async function readManagedGoalIndex(goalId: string, force: boolean): Promise<ManagedGoalIndex> {
const running = managedIndexReads.get(goalId);
if (running) return running;
const cached = managedIndexes.get(goalId);
if (!force && cached && Date.now() - cached.readAt < WORK_INDEX_WINDOW_MS) return cached;
const read = collectManagedGoalIndex(goalId)
.then(index => {managedIndexes.set(goalId, index); return index;})
.finally(() => {managedIndexReads.delete(goalId);});
managedIndexReads.set(goalId, read);
return read;
}

/** A confirmed plan supplies the only Todo ids that may return to its source conversation. */
async function readManagedPlanResult(goalId: string, todoIds: Set<string>, force: boolean): Promise<ManagedReadback> {
try {
const index = await readManagedGoalIndex(goalId, force);
// Only a plan-owned unreadable row can hide this plan's report; unrelated
// historical rows stay the server's and the Files view's concern.
for (const todoId of todoIds) {
if (index.unavailableTodoIds.has(todoId)) return {kind: "unavailable"};
}
const matched = new Map<string, ManagedGoalResultRow>();
for (const row of index.rows) {
if (!todoIds.has(row.todo_id)) continue;
if (!row.sha256 || !row.producer_agent_id || !row.title) return {kind: "unavailable"};
const previous = matched.get(row.todo_id);
if (previous && previous.sha256 !== row.sha256) return {kind: "unavailable"};
matched.set(row.todo_id, row);
}
if (matched.size > 1) return {kind: "multiple"};
const row = matched.values().next().value;
if (!row) return {kind: "waiting"};
const read = await readManagedGoalResult(goalId, row.todo_id);
if (read.goal_id !== goalId || read.todo_id !== row.todo_id ||
read.result.sha256 !== row.sha256 || read.result.producer_agent_id !== row.producer_agent_id ||
read.result.content_type !== row.content_type || typeof read.text !== "string") return {kind: "unavailable"};
return {kind: "accepted", artifact: managedReportArtifact(row.content_type, row.sha256, read.text),
agentId: row.producer_agent_id, title: row.title};
} catch { /* A failed inventory or exact read cannot keep an earlier report visible. */ }
return {kind: "unavailable"};
}

/** Return only an accepted, currently adopted report to the manager conversation. */
export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
goalId: string; todoIds: string[]; zh: boolean; onOpenGoalEvidence: (goalId: string) => void;
}) {
const [result, setResult] = useState<{key: string; readback: Readback} | null>(null);
const [managed, setManaged] = useState<{key: string; readback: ManagedReadback} | null>(null);
const [request, setRequest] = useState({count: 0, force: false});
const todoKey = [...todoIds].sort().join(",");
// A new read must withdraw the previous accepted report immediately. The
Expand All @@ -170,6 +254,9 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
void readAdoptedResult(goalId, new Set(todoKey.split(",")), request.force)
.then(value => {if (!cancelled) setResult({key, readback: value});})
.catch(() => {if (!cancelled) setResult({key, readback: {kind: "unavailable"}});});
void readManagedPlanResult(goalId, new Set(todoKey.split(",")), request.force)
.then(value => {if (!cancelled) setManaged({key, readback: value});})
.catch(() => {if (!cancelled) setManaged({key, readback: {kind: "unavailable"}});});
return () => {cancelled = true;};
}, [goalId, todoKey, key, request.force]);
useEffect(() => {
Expand All @@ -179,16 +266,25 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
return () => window.clearInterval(timer);
}, []);
const readback = result?.key === key ? result.readback : null;
const managedReadback = managed?.key === key ? managed.readback : null;
if (!goalId || !todoKey) return null;
return <section className={`personal-manager-team-result is-${readback?.kind ?? "loading"}`} aria-label={zh ? "团队结果回到管家" : "Team result returned to manager"} aria-busy={!readback}>
return <section className={`personal-manager-team-result is-${readback?.kind ?? "loading"}`} aria-label={zh ? "团队结果回到管家" : "Team result returned to manager"} aria-busy={!readback || !managedReadback}>
{!readback ? <p role="status">{zh ? "正在核验团队结果…" : "Verifying team result…"}</p> : readback.kind === "adopted" ? <>
<header><strong>{zh ? "团队验收结果" : "Team result"}</strong><small>{goalId} · {readback.agentId}</small></header>
<TeamArtifactReport artifact={readback.artifact} zh={zh} heading={zh ? "依据已采用 · 结果已验收" : "Source adopted · Result accepted"}/>
</> : <p role="status">{readback.kind === "unavailable"
</> : readback.kind === "waiting" && managedReadback?.kind === "accepted" ? null : <p role="status">{readback.kind === "unavailable"
? (zh ? "团队结果或采用证据无法核验,请到 Goal 查看版本关系。" : "Team result or adoption evidence cannot be verified; inspect versions in the Goal.")
: readback.kind === "multiple"
? (zh ? "有多个已验收的下游结果,请到 Goal 选择要采用的结论。" : "Multiple downstream results are accepted; choose the conclusion in the Goal.")
: (zh ? "团队任务已分配,尚无可核验的已采用结果。" : "Team work is assigned; no verifiable adopted result yet.")}</p>}
{managedReadback?.kind === "accepted" ? <div className="personal-manager-managed-report">
<header><strong>{zh ? "托管团队报告 · 已验收,采用尚未核验" : "Managed report · Accepted, adoption not verified"}</strong>
<small>{managedReadback.agentId}</small></header>
<TeamArtifactReport artifact={managedReadback.artifact} zh={zh} heading={managedReadback.title}/>
</div> : managedReadback?.kind === "multiple" ? <p role="status">{zh
? "这次分配已有多份托管报告;请到 Goal 选择结论。" : "This assignment has multiple managed reports; choose a conclusion in the Goal."}</p>
: managedReadback?.kind === "unavailable" ? <p role="status">{zh
? "托管报告无法核验;旧内容已撤回。" : "Managed report cannot be verified; previous content was withdrawn."}</p> : null}
<div className="personal-manager-team-result-actions">
<button type="button" onClick={() => onOpenGoalEvidence(goalId)}>{zh ? "查看证据与任务" : "Inspect evidence and tasks"}</button>
<button type="button" disabled={!readback} onClick={() => setRequest(previous => ({count: previous.count + 1, force: true}))}>{zh ? "刷新结果" : "Refresh result"}</button>
Expand Down
Loading
Loading