Skip to content
Closed
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
56 changes: 55 additions & 1 deletion apps/presentation/dashboard/smoke/team-plan-proposal-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
typedActionKindSchema,
typedActionProposalSchema,
} from "../src/data/chat.js";
import { teamPlanFields, teamPlanGoalId, teamPlanLaneCount } from "../src/features/personal-workspace/team-plan-preview.js";
import {
teamPlanAppliedLine,
teamPlanAppliedOutcome,
teamPlanFields,
teamPlanGoalId,
teamPlanLaneCount,
} from "../src/features/personal-workspace/team-plan-preview.js";

const GOAL_ID = "team-plan-smoke-goal";

Expand Down Expand Up @@ -168,4 +174,52 @@ check(
"the card never renders a lane as already created",
);

// An applied plan reports what the receipt recorded: the lanes that exist and
// the lanes that were left unstaffed, instead of one success sentence for every
// outcome. A receipt without a team-plan outcome keeps the surface's own line.
const outcomeTranslate = (key: string, values?: Record<string, string | number>) => {
const table: Record<string, string> = {
"drawer.proposalApplied": "Applied. LoopX state will refresh.",
"proposal.teamPlan.appliedPartially": "Applied partially: {created} lane(s) created, {gaps} left unstaffed.",
"proposal.teamPlan.appliedAlreadyPresent": "The confirmed lanes already existed, so nothing was duplicated.",
};
const template = table[key] ?? key;
return Object.entries(values ?? {}).reduce(
(text, [name, value]) => text.replaceAll(`{${name}}`, String(value)),
template,
);
};

const partialOutcome = teamPlanAppliedOutcome({
outcome: "team_plan_partially_applied",
gap_count: 1,
lanes: [{ lane_id: "lane_backend" }],
});
check(partialOutcome?.kind === "partially_applied", "a partial application is read as partial");
check(
partialOutcome?.created === 1 && partialOutcome?.gaps === 1,
"the partial outcome counts the lanes that exist and the lanes left unstaffed",
);
check(
teamPlanAppliedLine(partialOutcome, outcomeTranslate as never)
=== "Applied partially: 1 lane(s) created, 1 left unstaffed.",
"a partial application says how much of the plan was created",
);
check(
teamPlanAppliedLine(
teamPlanAppliedOutcome({ outcome: "team_plan_lanes_already_present", lanes: [{}, {}] }),
outcomeTranslate as never,
) === "The confirmed lanes already existed, so nothing was duplicated.",
"a replayed plan says the lanes already existed instead of claiming a new creation",
);
check(
teamPlanAppliedLine(teamPlanAppliedOutcome({ projection_verified: true }), outcomeTranslate as never)
=== "Applied. LoopX state will refresh.",
"a receipt without a team-plan outcome keeps the surface's own applied line",
);
check(
teamPlanAppliedOutcome({ outcome: "team_plan_no_staffable_lane" }) === null,
"a typed failure is not read as an applied outcome",
);

if (process.exitCode !== 1) console.log("team plan proposal smoke ok");
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type { LarkGoalConnection } from "../../data/chat";
import { localizedAttentionAge, localizedGoalState, localizedSessionStatus, useWorkspaceI18n } from "./i18n";
import { formatCostUsd, formatDurationMs, formatTokenCount, formatUsageValue } from "./personal-workspace-model";
import { todoResumeWhenFromMessage } from "./personal-workspace-router";
import { teamPlanAppliedLine } from "./team-plan-preview";

function subagentModelRequest(include: boolean, model: string, effort: string) {
if (!include) return {};
Expand Down Expand Up @@ -971,7 +972,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
{selection.item.status === "ready" ? <p className="personal-proposal-explainer">{t("drawer.proposalExplainer")}</p> : null}
<dl>{selection.item.fields.map((field) => <div key={field.key}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}</dl>
</section>
{selection.item.status === "applied" ? <p className={`personal-proposal-state ${selection.item.actionKind === "operation.execute" && selection.item.reviewPlan?.reason === "readback_unverified" ? "is-gated" : "is-applied"}`}><Check size={16} />{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : t("drawer.proposalApplied")}</p> : null}
{selection.item.status === "applied" ? <p className={`personal-proposal-state ${selection.item.actionKind === "operation.execute" && selection.item.reviewPlan?.reason === "readback_unverified" ? "is-gated" : "is-applied"}`}><Check size={16} />{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : selection.item.actionKind === "team.plan" ? teamPlanAppliedLine(selection.item.teamPlanOutcome ?? null, t) : t("drawer.proposalApplied")}</p> : null}
{selection.item.status === "applied" && selection.item.actionKind !== "operation.execute" && selection.item.goalId ? <button className="personal-primary-action" onClick={() => { const goalId = selection.item.goalId!; onClose(); void callbacks.onOpenGoal?.(goalId); }} type="button"><ExternalLink size={16} />{selection.item.actionKind === "goal.create" ? t("drawer.proposalEnterGoal") : t("drawer.proposalViewGoal")}</button> : null}
{selection.item.status === "stale" ? <p className="personal-proposal-state is-stale">{t("drawer.proposalStale")}</p> : null}
{selection.item.status === "error" ? <div className="personal-proposal-state is-error"><span>{selection.item.reviewPlan?.reason === "readback_unverified" ? t("actionReview.readback_unverified") : t("drawer.proposalApplyFailed")}</span>{selection.item.errorMessage ? <small>{selection.item.errorMessage}</small> : null}<small>{t("drawer.proposalApplyFailedHint")}</small></div> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,8 @@ const en = {
"proposal.field.laneGaps": "Unstaffed lanes",
"proposal.field.quotaEnvelope": "Quota envelope",
"proposal.teamPlan.acceptanceShort": "acceptance",
"proposal.teamPlan.appliedAlreadyPresent": "The confirmed lanes already existed, so nothing was duplicated.",
"proposal.teamPlan.appliedPartially": "Applied partially: {created} lane(s) created, {gaps} left unstaffed.",
"proposal.teamPlan.gapLane": "unstaffed",
"proposal.teamPlan.laneUnstaffed": "staffing gap, no first Todo",
"proposal.workspace.current": "Current local workspace (no Repository bound)",
Expand Down Expand Up @@ -1736,6 +1738,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"proposal.field.laneGaps": "未配齐的 lane",
"proposal.field.quotaEnvelope": "配额包络",
"proposal.teamPlan.acceptanceShort": "验收",
"proposal.teamPlan.appliedAlreadyPresent": "确认的 lane 已存在,未重复创建。",
"proposal.teamPlan.appliedPartially": "部分组建:已创建 {created} 条 lane,{gaps} 条未组建。",
"proposal.teamPlan.gapLane": "未配齐",
"proposal.teamPlan.laneUnstaffed": "编制缺口,无首个 Todo",
"proposal.workspace.current": "当前本地工作区(未绑定 Repository)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 } from "../../data/workspace-progressive-status";
import type { TeamPlanAppliedOutcome } from "./team-plan-preview";
export type WorkspaceGoalState =
| "需修复"
| "等你"
Expand Down Expand Up @@ -231,6 +232,14 @@ export type WorkspaceActionPreview = {
previewId: string;
primaryLabel?: string;
errorMessage?: string;
/**
* What an applied team plan actually did, read from its receipt.
*
* A confirmation can create every lane, some of them, or find them already
* present; the card shows which happened instead of one success sentence for
* all three.
*/
teamPlanOutcome?: TeamPlanAppliedOutcome;
status: "draft" | "ready" | "applying" | "applied" | "gated" | "stale" | "error" | "rejected" | "deferred";
title: string;
sourceRequest?: WorkspaceActionPreviewRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from "../../../../../../loopx/control_plane/presentation/action_review_plan.js";
import { refreshAttention } from "./attention-details";
import { teamPlanFields, teamPlanGoalId, teamPlanLaneCount } from "./team-plan-preview";
import { teamPlanAppliedOutcome } from "./team-plan-preview";
import { useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent } from "react";
import { AlertCircle, Bot, CalendarClock, FileText, ListPlus, MessageCircleQuestion, Paperclip, Plus, RefreshCw, Send, X } from "lucide-react";

Expand Down Expand Up @@ -634,6 +635,11 @@ function workspaceProposal(proposal: TypedActionProposal, t: WorkspaceTranslate)
&& reviewPlan.interaction !== "completed"
? "error"
: proposalStatus(proposal.status),
// A confirmed plan reports what its receipt recorded, so a partial
// application cannot read as a full success on this card.
teamPlanOutcome: proposal.action_kind === "team.plan"
? teamPlanAppliedOutcome(proposal.receipt) ?? undefined
: undefined,
title: localizedSummary,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,60 @@ export function teamPlanGoalId(parameters: Record<string, unknown>): string {
const plan = asRecord(parameters.plan);
return asText(parameters.goal_id) || asText(plan.goal_id);
}

/**
* What confirming a plan actually did, read from the receipt the apply wrote.
*
* A confirmation the owner was shown as one commitment can create every lane,
* some of them, or find them already present. The receipt is the only surface
* that knows which happened, so the card reads it instead of reporting every
* applied plan as the same success.
*/
export type TeamPlanAppliedOutcome = {
kind: "applied" | "partially_applied" | "already_present";
created: number;
gaps: number;
};

export function teamPlanAppliedOutcome(receipt: unknown): TeamPlanAppliedOutcome | null {
const record = asRecord(receipt);
const outcome = asText(record.outcome);
if (!outcome.startsWith("team_plan_")) return null;
const lanes = Array.isArray(record.lanes) ? record.lanes.length : 0;
const gaps = typeof record.gap_count === "number" ? record.gap_count : 0;
if (outcome === "team_plan_partially_applied") {
return { kind: "partially_applied", created: lanes, gaps };
}
if (outcome === "team_plan_lanes_already_present") {
return { kind: "already_present", created: lanes, gaps };
}
if (outcome === "team_plan_applied") {
return { kind: "applied", created: lanes, gaps };
}
// A plan whose receipt the apply recorded as a typed failure is not an applied
// outcome, so the card keeps the failure it already renders.
return null;
}

/**
* The line an applied plan shows, or the surface's own applied sentence.
*
* A partial application names how many lanes exist and how many were left
* unstaffed, because "applied" alone told the owner that a commitment was kept
* when part of it was not.
*/
export function teamPlanAppliedLine(
outcome: TeamPlanAppliedOutcome | null,
t: WorkspaceTranslate,
): string {
if (outcome?.kind === "partially_applied") {
return t("proposal.teamPlan.appliedPartially", {
created: String(outcome.created),
gaps: String(outcome.gaps),
});
}
if (outcome?.kind === "already_present") {
return t("proposal.teamPlan.appliedAlreadyPresent");
}
return t("drawer.proposalApplied");
}
31 changes: 30 additions & 1 deletion examples/personal-workspace-browser/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1565,11 +1565,40 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
state.durableResources.add(resourceKey);
state.durableWriteCount += 1;
}
// A confirmed team plan reports what it actually materialized: the lanes
// it created and the lanes it could not staff. The card reads this receipt,
// so the fixture has to answer with the same shape the product writes.
const teamPlanReceipt = (() => {
if (actionKind !== "team.plan") return null;
// An injected proposal lives in the action store rather than in the
// session previews, so the plan is read from whichever holds it.
const plan = actionProposals.get(apply[1])?.normalized_parameters?.plan
?? preview?.normalized_parameters?.plan;
const lanes = Array.isArray(plan?.lanes)
? plan.lanes
: [];
const ready = lanes.filter((lane) => lane?.staffing !== "gap");
const gaps = lanes.length - ready.length;
return {
projection_verified: true,
receipt_id: "fixture-team-plan-receipt",
outcome: gaps === 0 ? "team_plan_applied" : "team_plan_partially_applied",
...(gaps ? { gap_count: gaps } : {}),
lanes: ready.map((lane, index) => ({
lane_id: lane.lane_id,
agent_id: lane.agent_id,
priority: lane.first_todo?.priority ?? "P1",
disposition: "created",
todo_id: `todo_fixture_lane_${index + 1}`,
acceptance: lane.acceptance,
})),
};
})();
const proposal = {
schema_version: "loopx_chat_action_proposal_v1", proposal_id: apply[1], action_kind: actionKind,
summary: "已应用", normalized_parameters: preview?.normalized_parameters ?? {}, context: preview?.context ?? {}, expected_state_fingerprint: "fixture-r1",
permission_classification: "durable_write", validation_evidence: [], available_transitions: ["apply", "cancel"],
status: "applied", receipt: { projection_verified: true, receipt_id: "fixture-receipt" }, stale: null, created_at: "2026-08-13T01:00:00Z", updated_at: "2026-08-13T01:00:01Z",
status: "applied", receipt: teamPlanReceipt ?? { projection_verified: true, receipt_id: "fixture-receipt" }, stale: null, created_at: "2026-08-13T01:00:00Z", updated_at: "2026-08-13T01:00:01Z",
};
actionProposals.set(apply[1], proposal);
await route.fulfill({ contentType: "application/json", json: { ok: true, proposal, turn: acceptedTurn }, status: acceptedTurn ? 202 : 200 });
Expand Down
4 changes: 3 additions & 1 deletion examples/personal-workspace-browser/team-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ export const teamPlanScenario = {
"confirming sends exactly one apply for the confirmed proposal",
);
check(api.durableWriteCount === 1, "the confirmed apply performed exactly one durable write");
await drawer.getByText("已应用,LoopX 状态将刷新。", { exact: true })
// The plan the owner confirmed staffed one lane and left one unstaffed, so
// the card has to report a partial application instead of a full success.
await drawer.getByText("部分组建:已创建 1 条 lane,1 条未组建。", { exact: true })
.waitFor({ state: "visible", timeout: 15_000 });
await page.screenshot({
path: resolve(outputDir, "team-plan-applied.png"),
Expand Down
6 changes: 3 additions & 3 deletions loopx/web/chat/asset-retention.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2",
"assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2",
"assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2",
"assets/index-ByNCYQIY.js",
"assets/index-B76FcRTn.js",
"assets/index-C58Y-FU6.css"
],
[
Expand All @@ -28,8 +28,8 @@
"assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2",
"assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2",
"assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2",
"assets/index-C58Y-FU6.css",
"assets/index-CFOC0-T9.js"
"assets/index-ByNCYQIY.js",
"assets/index-C58Y-FU6.css"
]
]
}

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion loopx/web/chat/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
content="LoopX 个人 Agent 工作区:在同一个频道里查看、纠偏并推进 Goal。"
/>
<title>LoopX 个人 Agent 工作区</title>
<script type="module" crossorigin src="/chat/assets/index-ByNCYQIY.js"></script>
<script type="module" crossorigin src="/chat/assets/index-B76FcRTn.js"></script>
<link rel="stylesheet" crossorigin href="/chat/assets/index-C58Y-FU6.css">
</head>
<body>
Expand Down
Loading