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
@@ -1,12 +1,19 @@
import { useEffect, useEffectEvent, useId, useRef, useState } from "react";
import { z } from "zod";
import {compactWorkspaceText} from "./personal-workspace-model";
import {compactWorkspaceText, workspaceAgentTodoFromItem} from "./personal-workspace-model";
import { todoItemSchema } from "../../data/status";
import type { WorkspaceAgentTodo, WorkspaceDrawerSelection, WorkspaceGoal } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";

const pageSchema = z.object({
ok: z.literal(true), total: z.number().int().nonnegative(), next_cursor: z.string().nullable(),
items: z.array(z.object({ todo_id: z.string(), text: z.string(), claimed_by: z.string().nullable(), evidence: z.string().nullable(), priority: z.string().nullable(), task_class: z.string().nullable() })).max(40),
items: z.array(z.object(todoItemSchema.shape).pick({
todo_id: true, text: true, done: true, status: true, claimed_by: true,
evidence: true, note: true, priority: true, task_class: true, task_domain: true,
completed_at: true, resume_when: true, resume_ready: true, resume_condition: true,
completion_validation_sha256: true, completion_validation_revision: true,
completion_validation_revision_history: true,
}).extend({ todo_id: z.string().min(1), done: z.literal(true), status: z.literal("done") })).max(40),
});

/** Fixed-height previews keep layout/DOM cost bounded; the drawer retains full text. */
Expand Down Expand Up @@ -69,7 +76,7 @@ export function CompletedTaskLane({ goal, agentId, seed, enabled, refreshRevisio
if (!response.ok) throw new Error("history unavailable");
const page = pageSchema.parse(await response.json());
if (controller.signal.aborted) return;
const next = page.items.map((todo): WorkspaceAgentTodo => ({ todoId: todo.todo_id, text: todo.text, claimedBy: todo.claimed_by, evidence: todo.evidence, priority: todo.priority, taskClass: todo.task_class, done: true, status: "done" }));
const next = page.items.map((todo) => workspaceAgentTodoFromItem(todo, todo.todo_id));
setRows((current) => cursor === undefined ? next : [...current, ...next.filter((todo) => !current.some((existing) => existing.todoId === todo.todoId))]);
setTotal(page.total);
setCursor(page.next_cursor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { localizedGoalState, localizedSessionStatus, useWorkspaceI18n } from "./
import { formatCostUsd, formatDurationMs, formatTokenCount, formatUsageValue } from "./personal-workspace-model";
import { TeamPlanResult } from "./team-plan-result";
import { parseTodoResumeCondition } from "./todo-resume-condition";
import { MarkdownText } from "./markdown";
import { formatMonitorDate } from "./monitor-readback";

function subagentModelRequest(include: boolean, model: string, effort: string) {
Expand Down Expand Up @@ -643,12 +644,13 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<div><dt>{t("common.owner")}</dt><dd>{selection.item.ownerLabel ?? selection.item.claimedBy ?? t("drawer.notAssigned")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? t("drawer.notSet")}</dd></div>
<div><dt>{t("drawer.dependencies")}</dt><dd>{selection.item.dependencies?.join(" · ") || t("common.none")}</dd></div>
{selection.item.completedAt ? <div><dt>{t("drawer.completedAt")}</dt><dd><time dateTime={selection.item.completedAt}>{selection.item.completedAt}</time></dd></div> : null}
{selection.item.dependencies ? <div><dt>{t("drawer.dependencies")}</dt><dd>{selection.item.dependencies.join(" · ") || t("common.none")}</dd></div> : null}
{selection.item.status === "deferred" || selection.item.resumeWhen ? <div><dt>{t("drawer.resumeWhen")}</dt><dd>{selection.item.resumeWhen || t("drawer.notSet")}</dd></div> : null}
{selection.item.resumeWhen ? <div><dt>{t("drawer.resumeState")}</dt><dd>{selection.item.resumeReady ? t("drawer.resumeReady") : t("drawer.resumePending")}</dd></div> : null}
{selection.item.resumeWhen ? <div><dt>{t("drawer.resumeState")}</dt><dd>{selection.item.resumeReady == null ? t("drawer.notSet") : selection.item.resumeReady ? t("drawer.resumeReady") : t("drawer.resumePending")}</dd></div> : null}
{selection.item.resumeReceiptId ? <div><dt>{t("drawer.resumeReceipt")}</dt><dd>{selection.item.resumeReceiptId}</dd></div> : null}
{selection.item.validationDigest ? <>
<div><dt>{t("drawer.validationRevision")}</dt><dd>{selection.item.validationRevision ?? 0}</dd></div>
<div><dt>{t("drawer.validationRevision")}</dt><dd>{selection.item.validationRevision ?? t("drawer.notSet")}</dd></div>
<div><dt>{t("drawer.validationDigest")}</dt><dd><code>{selection.item.validationDigest}</code></dd></div>
{selection.item.validationRevisionActor ? <div><dt>{t("drawer.validationRevisionActor")}</dt><dd>{selection.item.validationRevisionActor}</dd></div> : null}
</> : null}
Expand Down Expand Up @@ -708,6 +710,10 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
</details>
<button className="personal-primary-action" onClick={() => void previewTodoTransition(selection.item, "complete", t("drawer.taskComplete"))} type="button"><Check size={17} />{t("drawer.taskComplete")}</button>
</div> : null}
{selection.item.evidence ? <section className="personal-detail-card" aria-label={t("drawer.evidence")}>
<h4>{t("drawer.evidence")}</h4>
<MarkdownText text={selection.item.evidence} />
</section> : null}
{selection.item.done ? <div className="personal-task-completed-note"><Check size={16} /><span><strong>{t("drawer.taskCompletedTitle")}</strong><small>{t("drawer.taskCompletedNote")}</small></span></div> : null}
</>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ const en = {
"drawer.nextTransition": "Next transition",
"drawer.validationRevision": "Validation revision",
"drawer.validationDigest": "Validation declaration digest",
"drawer.completedAt": "Completed at",
"drawer.validationRevisionActor": "Revised by",
"drawer.noExecutionHistory": "No execution history yet.",
"drawer.noRun": "Execution Session has not started",
Expand Down Expand Up @@ -1522,6 +1523,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.nextTransition": "下一转换",
"drawer.validationRevision": "验证声明修订",
"drawer.validationDigest": "验证声明摘要",
"drawer.completedAt": "完成时间",
"drawer.validationRevisionActor": "修订者",
"drawer.noExecutionHistory": "暂无执行记录。",
"drawer.noRun": "尚未启动执行 Session",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ActionReviewPlan } from "../../../../../../loopx/control_plane/pre
import type { GoalAcceptanceObservation } from "../../data/goal-acceptance-observation";
import type { AttentionDetails } from "./attention-details";
import type { WorkspaceLoadError, WorkspaceReadScope } from "../../data/workspace-progressive-status";
import type { TodoItem } from "../../data/status";
import { goalWorkKind, type GoalHostThreadActivity, type WorkspaceGoalExecution } from "./goal-activity";
export type WorkspaceGoalState =
| "需修复"
Expand Down Expand Up @@ -52,6 +53,36 @@ export type WorkspaceAgentTodo = {
validationRevisionActor?: string | null;
};

/** Both active status and retained history carry the same inspector facts. */
export function workspaceAgentTodoFromItem(todo: Pick<TodoItem,
"todo_id" | "text" | "done" | "status" | "claimed_by" | "evidence" | "note"
| "priority" | "task_class" | "task_domain" | "completed_at" | "resume_when"
| "resume_ready" | "resume_condition" | "completion_validation_sha256"
| "completion_validation_revision" | "completion_validation_revision_history"
>, fallbackId: string): WorkspaceAgentTodo {
const receipt = todo.resume_condition?.resume_receipt;
const receiptId = receipt && typeof receipt === "object" && !Array.isArray(receipt)
? (receipt as Record<string, unknown>).receipt_id : null;
return {
todoId: todo.todo_id?.trim() || fallbackId,
text: todo.text,
done: todo.status === "deferred" ? false : todo.done,
status: todo.status ?? null,
claimedBy: todo.claimed_by ?? null,
evidence: todo.evidence || todo.note || null,
priority: todo.priority ?? null,
taskClass: todo.task_class ?? null,
taskDomain: todo.task_domain ?? null,
completedAt: todo.completed_at ?? null,
resumeWhen: todo.resume_when ?? null,
resumeReady: todo.resume_ready ?? null,
resumeReceiptId: typeof receiptId === "string" && receiptId.trim() ? receiptId.trim() : null,
validationDigest: todo.completion_validation_sha256 ?? null,
validationRevision: todo.completion_validation_revision ?? null,
validationRevisionActor: todo.completion_validation_revision_history.at(-1)?.actor_agent_id ?? null,
};
}

export type WorkspaceTodo = WorkspaceAgentTodo & {
goalId: string;
goalTitle: string;
Expand Down
50 changes: 14 additions & 36 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withTurnActivity, type TurnStep } from "../data/turn-steps";
import { conversationReturnSessions, conversationPendingReturnSessions, reconcileConversationHistory, reconcileConversationReturns } from "../data/conversation-returns";
import { readConversationReturns } from "../data/conversation-return-observation";
import { currentChannelSession, useConversationHistory } from "../data/use-conversation-history";
import {compactWorkspaceText as compactShareText} from "../features/personal-workspace/personal-workspace-model";
import {compactWorkspaceText as compactShareText, workspaceAgentTodoFromItem} from "../features/personal-workspace/personal-workspace-model";
import type { GoalAcceptanceObservation } from "../data/goal-acceptance-observation";
import { attentionDetails, attentionDetailsFromSnapshot, sourceAttention } from "../features/personal-workspace/attention-details";
import type { AttentionDetails } from "../features/personal-workspace/attention-details";
Expand Down Expand Up @@ -238,8 +238,6 @@ type TodoExplorerItem = {
todo: TodoItem;
};

type PersonalAgentTodoItem = WorkspaceAgentTodo;

function inferLifecyclePhase(status?: string | null, run?: RunRecord) {
if (run?.controller_readiness?.decision_advisor_ready || run?.controller_readiness?.write_controller_ready) {
return "controller_ready";
Expand Down Expand Up @@ -457,7 +455,7 @@ type PersonalGoalItem = {
activationState: "active" | "stopped";
agentId: string;
agentSentence: string;
agentTodos: PersonalAgentTodoItem[];
agentTodos: WorkspaceAgentTodo[];
doneTodoCount: number;
goalId: string;
latestActivity?: string;
Expand Down Expand Up @@ -702,34 +700,14 @@ function personalTodoText(todo: TodoItem) {
return compactShareText(todo.title ?? todo.text, 112);
}

function personalTodoResumeReceiptId(todo: TodoItem) {
const receipt = todo.resume_condition?.resume_receipt;
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return null;
const receiptId = (receipt as Record<string, unknown>).receipt_id;
return typeof receiptId === "string" && receiptId.trim() ? receiptId.trim() : null;
}

function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): PersonalAgentTodoItem {
const latestValidationRevision = todo.completion_validation_revision_history.at(-1);
function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): WorkspaceAgentTodo {
return {
...monitorTodoReadback(todo),
completedAt: todo.completed_at ?? null,
resumeWhen: todo.resume_when ?? null,
resumeReady: todo.resume_ready ?? null,
resumeReceiptId: personalTodoResumeReceiptId(todo),
claimedBy: todo.claimed_by ?? null,
// Legacy summaries mark deferred entries checked; they are not completed work.
done: todo.status === "deferred" ? false : todo.done,
...workspaceAgentTodoFromItem(todo, `${row.goal.id}:agent:${todo.index}`),
// Preserve the active queue's existing preview budget. History and its
// inspector use full text/evidence from the retained read.
evidence: todo.evidence ? compactShareText(todo.evidence, 96) : null,
priority: todo.priority ?? null,
status: todo.status ?? null,
taskClass: todo.task_class ?? null,
taskDomain: todo.task_domain ?? null,
text: personalTodoText(todo),
todoId: todo.todo_id?.trim() || `${row.goal.id}:agent:${todo.index}`,
validationDigest: todo.completion_validation_sha256 ?? null,
validationRevision: todo.completion_validation_revision ?? null,
validationRevisionActor: latestValidationRevision?.actor_agent_id ?? null,
};
}

Expand All @@ -746,16 +724,16 @@ function personalAgentTodoItems(row: GoalDirectoryRow): TodoItem[] {
return [...merged.values()];
}

function personalAgentTodos(row: GoalDirectoryRow): PersonalAgentTodoItem[] {
function personalAgentTodos(row: GoalDirectoryRow): WorkspaceAgentTodo[] {
return personalAgentTodoItems(row).map((todo) => personalAgentTodoFromItem(todo, row));
}

function personalSubagentDomainCandidates(
payload: StatusPayload,
row: GoalDirectoryRow,
fallbackTodos: PersonalAgentTodoItem[],
fallbackTodos: WorkspaceAgentTodo[],
) {
const candidateTodos = new Map<string, PersonalAgentTodoItem>();
const candidateTodos = new Map<string, WorkspaceAgentTodo>();
for (const todo of payload.todo_index?.items ?? []) {
if (todo.goal_id !== row.goal.id || todo.role !== "agent") continue;
const projected = personalAgentTodoFromItem(todo, row);
Expand All @@ -778,7 +756,7 @@ function personalSubagentDomainCandidates(
function personalAgentTodoFromProjection(
todo: NonNullable<AgentManagementProjection["agents"][number]["current_todo"]>,
row: GoalDirectoryRow,
): PersonalAgentTodoItem {
): WorkspaceAgentTodo {
return {
claimedBy: todo.claimed_by ?? null,
done: todo.status === "done" || todo.status === "completed",
Expand All @@ -791,10 +769,10 @@ function personalAgentTodoFromProjection(
}

function mergePersonalAgentTodos(
projectedTodos: PersonalAgentTodoItem[],
projectedTodos: WorkspaceAgentTodo[],
agentRows: AgentManagementRow[],
row: GoalDirectoryRow,
): PersonalAgentTodoItem[] {
): WorkspaceAgentTodo[] {
const merged = new Map(projectedTodos.map((todo) => [todo.todoId, todo]));
for (const agent of agentRows) {
const current = agent.currentTodo;
Expand All @@ -813,7 +791,7 @@ function mergePersonalAgentTodos(
function personalAgentTodoFacts(row: GoalDirectoryRow): {
doneTodoCount: number;
nextTodoText: string | null;
recentCompleted: PersonalAgentTodoItem[];
recentCompleted: WorkspaceAgentTodo[];
} {
const assetTodos = row.queueItem?.project_asset?.agent_todos;
const queueTodos = row.queueItem?.agent_todos;
Expand All @@ -840,7 +818,7 @@ function personalAgentTodoFacts(row: GoalDirectoryRow): {
return { doneTodoCount, nextTodoText, recentCompleted };
}

function personalVisiblePlanTodos(todos: PersonalAgentTodoItem[], limit = 4) {
function personalVisiblePlanTodos(todos: WorkspaceAgentTodo[], limit = 4) {
if (todos.length <= limit) {
return todos;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,29 @@ The representative request falls from over 2 MiB to about 0.96 MB without raisin
budgets. Real File/SQLite CLI tests cover exact counts, distant inferred edges,
metadata preservation and unchanged provider state. This is a bounded capacity
repair, not unlimited graph capacity, stable latency evidence, D2 qualification
or permission to change the default provider. Full-Goal summary/list/detail
adoption and sustained observation remain separate work.
or permission to change the default provider.

### Retained completion consumption

The completed-history consumer
now explicitly reads retained active and archived records through the existing
typed summary/filter owner. Previously, archiving 84 of 85 completions made
the workspace history return only one row. Its bounded HTTP pages now include
all 85, preserve full task text/evidence and Agent filtering, and expose only
display fields. Explicit history reads opt out of the existing 500-character
summary limit; the default active Todo list and scheduling input still exclude
archives and retain their display limit. Real File/SQLite and legacy Markdown
reads preserve provider state. The packaged workspace keeps its existing
pagination and opens retained evidence in the read-only Todo inspector using
the shared Markdown renderer, without another navigation step or edit action.
History also preserves completion time, resume facts and validation-declaration
revision/actor facts. Active and history consumers reuse the same TS inspector
mapping and the existing Todo schema; the active queue's preview budgets remain
unchanged. Unprovided dependencies are not presented as an empty dependency
list, and absent resume readiness or validation revisions are not invented.
This closes that history-enumeration/detail gap, not whole-Goal summary/list/detail
adoption, sustained observation, D2 or default-provider admission. No persisted
format, writer or PostgreSQL consumer is changed.

### Packaged-source fingerprint cost

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,23 @@ Todo/租约集合中位数为 File 输入 32.4→27.5 ms、SQLite 输入 32.9
响应不变。代表性复核请求从超过 2 MiB 降到约 0.96 MB,没有提高预算。
真实 File/SQLite CLI 验证精确计数、跨远端记录的推断后继、metadata 保留与
provider 状态不变。这只修复有界容量,不证明任意规模、稳定延迟、D2 验收,
也不授予默认 provider 切换;整 Goal summary/list/detail 消费和持续观察仍待推进。
也不授予默认 provider 切换。

### 保留完成记录的消费

完成历史消费者现在显式读取活跃与归档的保留记录,
复用既有类型化 summary/filter owner。此前 85 条完成记录归档 84 条后,工作区
历史只返回 1 条;有界 HTTP 分页现在包含全部 85 条,保留完整任务文本、证据和
Agent 筛选,仅暴露展示字段。显式历史读取不再套用既有的 500 字符摘要上限;
默认活跃 Todo 列表与调度输入仍排除归档,并保留原展示上限。真实 File/SQLite
和旧 Markdown 读取保持 provider 状态不变。打包工作区沿用已有分页,在只读
Todo 详情中用共享 Markdown 组件呈现保留证据,不增加导航步骤或编辑动作。
历史同时保留完成时间、恢复事实及验收声明的版本/修订者。活跃列表和历史
复用同一 TS 详情映射与既有 Todo schema,活跃队列的预览预算保持不变。
未提供的依赖列表不再展示为“无依赖”,缺失的恢复就绪性或验收版本也不虚构。
这关闭历史枚举与详情缺口,不代表整 Goal summary/list/detail
消费、持续观察、D2 或默认 provider 准入完成;不改变持久格式、writer 或
PostgreSQL 消费者。

### 打包源码指纹成本

Expand Down
Loading
Loading