/** @canvas * title: @local/dsh-canvas 门禁 * description: 两条真实门禁:全量测试与模板语料;Run 走真实 ctx.shell 并回传 exitCode * icon: board */ import { H1, H2, Text, Code, Stack, Row, Grid, Divider, Card, CardBody, CardHeader, Callout, Stat, Table, Pill, Button, CollapsibleSection, useCanvasState, useCanvasOverlay, useCanvasAction, } from "dsh/canvas"; type GateStatus = "pass" | "fail" | "acked" | "missing"; // 注:overlay 只能覆盖 DATA 里出现过的字面量。因为 DATA 用了 as const, // 上面 GateStatus 里的每个值都应该至少在一个 gate 上出现一次, // 否则 overlay.set(id, { status: "..." }) 在编辑器里会报类型不匹配。 export const DATA = { baseline: "node --test test/*.test.mjs(8 文件 / 92 断言)", gates: [ { id: "tests", runId: "gate:tests", command: "node --test test/*.test.mjs", baseline: "8 files", last: "8 files", status: "pass", at: "2026-09-28", }, { id: "templates", runId: "gate:templates", command: "node --test test/templates.test.mjs", baseline: "4 compiled", last: "4 compiled", status: "pass", at: "2026-09-28", }, ], } as const; const TONE: Record = { pass: "success", fail: "danger", acked: "neutral", missing: "warning", }; const LABEL: Record = { pass: "pass", fail: "fail", acked: "acked", missing: "not run", }; export default function GateDashboard() { const dispatch = useCanvasAction(); const [filter, setFilter] = useCanvasState("filter", "all"); // 人的确认走 overlay(不是 useCanvasState):agent 必须能看见"这条红被接受了"。 const overlay = useCanvasOverlay("gates", DATA.gates); const gates = overlay.items; const failing = gates.filter((g) => g.status === "fail"); const visible = filter === "failing" ? failing : gates; const lastRun = gates.reduce((acc, g) => (g.at > acc ? g.at : acc), ""); const run = async (g: (typeof DATA.gates)[number]) => { const r = await dispatch({ type: "runCommand", id: g.runId }); if (!r.ok) { // 白名单未登记、ctx.shell 缺失或启动失败:在画布上说出来,不要静默失败。 await dispatch({ type: "notify", tone: "warning", message: g.id + " 未执行: " + r.message }); return; } // 动作成功 != 门禁通过:退出码才是结果,红门禁必须红着显示。 await dispatch({ type: "notify", tone: r.exitCode === 0 ? "success" : "warning", message: g.id + " exit=" + String(r.exitCode) + (r.timedOut === true ? "(超时)" : ""), }); }; // 顺序执行:门禁通常是重命令(编译 / 对拍),并发触发会互相抢资源。 const runAll = async () => { // 顺序执行:门禁通常是重命令(编译 / 对拍),并发触发会互相抢资源。 let denied = 0; let failed = 0; for (const g of gates) { const r = await dispatch({ type: "runCommand", id: g.runId }); if (!r.ok) denied += 1; else if (r.exitCode !== 0) failed += 1; } await dispatch({ type: "notify", tone: denied > 0 || failed > 0 ? "warning" : "info", message: denied > 0 || failed > 0 ? String(denied) + " 条未执行(白名单 / 权限)," + String(failed) + " 条非零退出" : "全部门禁退出码为 0", }); }; return (

@local/dsh-canvas 门禁

基线:{DATA.baseline}
g.status === "pass").length} label="pass" tone="success" /> 0 ? "danger" : "neutral"} /> g.status === "acked").length} label="acked" tone="neutral" /> {failing.length > 0 ? ( {failing.map((g) => ( {g.id} 基线 {g.baseline} → 最近 {g.last} ))} ) : ( 保持基线:不要把数字抄进 DATA,让它来自真实运行结果。 )} setFilter("all")}> All {gates.length} setFilter("failing")}> Failing {failing.length} {lastRun.length > 0 ? ( 最近一次:{lastRun} ) : null} [ {g.id}, g.baseline, g.last, g.baseline === g.last ? "0" : "changed", {LABEL[g.status]} , , ])} rowTone={visible.map((g) => TONE[g.status])} /> 人工确认}> 接受一条红门禁 确认只表示"我知道它红着,且现在接受"。改动落进 sidecar,agent 下一轮通过 canvas_read 就能看到。 {gates.map((g) => ( ))} 需配置}> runCommand 默认全部拒绝。先在插件 Config 的 commandWhitelist 里登记,且要逐字匹配: {"commandWhitelist: [{ id: 'gate:phase19', command: 'cargo test -p occt-topo phase19' }]"} 被拒时动作返回 code: "denied" —— 画布要显式提示,而不是假装成功。

口径

每行的基线与最近值都应来自真实运行输出;本画布只负责显示与触发。 ); }