Show line-by-line live G-code execution trace

This commit is contained in:
2026-08-29 07:51:44 +08:00
parent 43670525d6
commit 55627010c3
7 changed files with 63 additions and 33 deletions

View File

@@ -87,10 +87,15 @@ function DroView({ axes, state, coordType, displayType, metric, setCoordType, se
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-column-headings"><span /><span>Displayed</span><span>Actual</span><span>Relative</span></div><div className="dro-table">{labels.map((label, index) => { const actual = Number(axes[index] ?? 0); const target = Number(commanded[index] ?? actual); const relativeValue = Number(relative[index] ?? actual); const displayed = displayType === "commanded" ? target : actual; return <div className="dro-row" key={label}><strong>{label}</strong><output>{format(coordType === "relative" ? relativeValue : displayed, index)}</output><output>{format(actual, index)}</output><output>{format(relativeValue, index)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b> / deg</span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
}
function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, onOpenFile, canLoad, canRun, canStep, currentLine, executionLine, executionLineText, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; onOpenFile: () => void; canLoad: boolean; canRun: boolean; canStep: boolean; currentLine: number | null; executionLine: number | null; executionLineText: string; diagnostic: string | null }) {
function ProgramPane({ text, setText, activeGcodes, executionTrace, onLoad, onRun, onStep, onOpenFile, canLoad, canRun, canStep, currentLine, executionLine, executionLineText, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; executionTrace: Array<{ line: number; text: string; program_status?: string | null }>; onLoad: () => void; onRun: () => void; onStep: () => void; onOpenFile: () => void; canLoad: boolean; canRun: boolean; canStep: boolean; currentLine: number | null; executionLine: number | null; executionLineText: string; diagnostic: string | null }) {
const lines = text.split("\n");
const mainLine = currentLine && currentLine > 0 && currentLine <= lines.length ? currentLine : null;
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-editor-wrap"><div className="program-gutter" aria-label="G-code line numbers">{lines.map((_, index) => <span key={index} className={index + 1 === mainLine ? "current-line" : ""} aria-current={index + 1 === mainLine ? "true" : undefined}>{index + 1}</span>)}</div><textarea id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div><div className="program-execution-line" role="status" aria-label="Currently executing G-code"><span>{executionLine ? `Executing source line ${executionLine}` : "Execution idle"}</span><code>{executionLineText || "-"}</code></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onOpenFile}>Open file</TextButton><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!canRun}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!canStep}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
const traceRef = useRef<HTMLOListElement>(null);
useEffect(() => {
const node = traceRef.current;
if (node) node.scrollTop = node.scrollHeight;
}, [executionTrace]);
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-editor-wrap"><div className="program-gutter" aria-label="G-code line numbers">{lines.map((_, index) => <span key={index} className={index + 1 === mainLine ? "current-line" : ""} aria-current={index + 1 === mainLine ? "true" : undefined}>{index + 1}</span>)}</div><textarea id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div><div className="program-execution-line" role="status" aria-label="Currently executing G-code"><span>{executionLine ? `Executing source line ${executionLine}` : "Execution idle"}</span><code>{executionLineText || "-"}</code></div><div className="program-execution-trace" role="log" aria-label="G-code execution trace" aria-live="polite"><strong>Execution trace</strong><ol ref={traceRef}>{executionTrace.slice(-8).map((entry, index) => <li key={`${entry.line}-${index}`}><span>L{entry.line}</span><code>{entry.text || "-"}</code></li>)}</ol></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onOpenFile}>Open file</TextButton><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!canRun}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!canStep}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
}
function DiagnosticsPanel({ state, gates }: { state: typeof INITIAL_STATE; gates: Record<string, { enabled: boolean; reason: string }> }) {
@@ -235,7 +240,7 @@ export function App() {
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
<XyzacProgramPane status={String(state.program?.program_status ?? "idle")} currentLine={executionLine} sourceHash={String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onOpenFile={openProgramFile} onLoad={() => { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: PROGRAM_PATH }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={currentLine} executionLine={executionLine} executionLineText={executionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
<XyzacProgramPane status={String(state.program?.program_status ?? "idle")} currentLine={executionLine} sourceHash={String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={state.executionTrace} onOpenFile={openProgramFile} onLoad={() => { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: PROGRAM_PATH }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={currentLine} executionLine={executionLine} executionLineText={executionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
</main>;

View File

@@ -24,6 +24,7 @@ export type FrontendState = {
axisState: AxisStateView | null;
gates: Record<string, { action: string; enabled: boolean; reason: string }>;
toolpath: Array<{ id: number; source_line?: number | null; start: Record<string, number>; end: Record<string, number>; active?: boolean }>;
executionTrace: Array<{ line: number; text: string; program_status?: string | null }>;
};
export const INITIAL_STATE: FrontendState = {
@@ -48,6 +49,7 @@ export const INITIAL_STATE: FrontendState = {
axisState: null,
gates: {},
toolpath: [],
executionTrace: [],
};
function payload<T>(message: MessageEnvelope): T {
@@ -56,6 +58,12 @@ function payload<T>(message: MessageEnvelope): T {
function projection(result: Record<string, unknown>, message: MessageEnvelope, previous: FrontendState) {
const panel = result.panel_snapshot as PanelSnapshotV1 | undefined;
const trace = Array.isArray(result.execution_trace)
? (result.execution_trace as Array<Record<string, unknown>>)
.map((entry) => ({ line: Number(entry.line), text: String(entry.text ?? ""), program_status: entry.program_status ? String(entry.program_status) : null }))
.filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0)
: [];
const executionTrace = trace.length ? [...previous.executionTrace, ...trace].slice(-32) : previous.executionTrace;
const delta = result.motion_sample as { tick?: number; pose?: Record<string, number>; joints?: number[]; active_segment_id?: number; active_progress?: number } | undefined;
if (!panel && delta && previous.snapshot) {
const pose = delta.pose ?? {};
@@ -89,6 +97,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
program: (result.program as Record<string, unknown>) ?? previous.program,
axisState: (result.axis_state as AxisStateView) ?? previous.axisState,
gates: (result.gates as FrontendState["gates"]) ?? previous.gates,
executionTrace,
};
}
if (panel?.schema === "axis-panel-snapshot.v1") {
@@ -145,6 +154,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
hal: panel.hal ?? previous.hal,
axisState: panel.axis_state,
gates: { ...panel.gates, jog: panel.gates.jogStart, load: panel.gates.load, run: panel.gates.run, step: panel.gates.step, pause: panel.gates.pause, resume: panel.gates.resume, abort: panel.gates.abort, mdi: panel.gates.mdi, home: panel.gates.home },
executionTrace,
};
}
const wasmMotion = (result.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state;
@@ -186,7 +196,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
for (const segment of nextToolpath) segment.active = false;
}
const snapshot = { simulation_tick: message.simulation_tick ?? previous.tick, state_hash: String(result.state_hash ?? previous.snapshot?.state_hash ?? ""), task: result.task as SimSnapshotV1["task"], motion: { axes, joints, velocity: Array.isArray(motionState.velocity) ? motionState.velocity as number[] : [], homed: typeof motionState.homed === "boolean" ? motionState.homed : undefined, homed_axes: Array.isArray(motionState.homed_axes) ? motionState.homed_axes as boolean[] : undefined }, authoritative } satisfies SimSnapshotV1;
return { snapshot, toolpath: nextToolpath, task: (result.task as Record<string, unknown>) ?? previous.task, program: programResult as Record<string, unknown> ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState, gates: (result.gates as FrontendState["gates"]) ?? previous.gates };
return { snapshot, toolpath: nextToolpath, task: (result.task as Record<string, unknown>) ?? previous.task, program: programResult as Record<string, unknown> ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState, gates: (result.gates as FrontendState["gates"]) ?? previous.gates, executionTrace };
}
export function reduceFrontendState(state: FrontendState, message: MessageEnvelope): FrontendState {

View File

@@ -170,7 +170,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
.axis-slider { display: grid; grid-template-columns: repeat(3, minmax(170px, 1fr)); gap: 12px; }
.axis-slider label { grid-template-columns: auto 48px; }
.axis-program-pane { display: grid; grid-template-columns: minmax(0, 1fr) 230px; flex: 0 0 205px; height: 205px; min-height: 205px; margin: 0 5px 5px; border: 2px solid #777; background: #f7f7f7; }
.axis-program-pane { display: grid; grid-template-columns: minmax(0, 1fr) 230px; grid-template-rows: minmax(100px, 1fr) auto auto; flex: 0 0 285px; height: 285px; min-height: 285px; margin: 0 5px 5px; border: 2px solid #777; background: #f7f7f7; }
.axis-diagnostics { display: grid; gap: 8px; padding: 10px; min-height: 190px; overflow: auto; background: #f7f7f7; font-size: 12px; }
.diagnostic-summary, .diagnostic-error, .diagnostic-row { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.diagnostic-summary { padding-bottom: 6px; border-bottom: 1px solid #b3b3b3; }
@@ -186,7 +186,13 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
.program-execution-line { display: grid; grid-column: 1; grid-row: 2; grid-template-columns: max-content minmax(0, 1fr); align-items: baseline; gap: 10px; min-width: 0; padding: 5px 8px; border-top: 1px solid #9aa8b4; border-bottom: 1px solid #9aa8b4; background: #e9f1f8; color: #173b5f; font: 12px/1.35 ui-monospace, monospace; }
.program-execution-line span { font-weight: 700; white-space: nowrap; }
.program-execution-line code { min-width: 0; overflow: hidden; color: #111; text-overflow: ellipsis; white-space: nowrap; }
.program-side { display: flex; grid-column: 2; grid-row: 1 / span 2; flex-direction: column; gap: 12px; padding: 8px; border-left: 1px solid #aaa; background: #d9d9d9; }
.program-execution-trace { grid-column: 1; grid-row: 3; min-width: 0; padding: 4px 8px; overflow: auto; border-bottom: 1px solid #9aa8b4; background: #f4f7fa; color: #253746; font: 11px/1.3 ui-monospace, monospace; }
.program-execution-trace strong { display: block; margin-bottom: 2px; font-size: 11px; }
.program-execution-trace ol { display: grid; gap: 1px; max-height: 64px; margin: 0; padding: 0; overflow: auto; list-style: none; }
.program-execution-trace li { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 8px; min-width: 0; }
.program-execution-trace li span { color: #174d84; font-weight: 700; }
.program-execution-trace li code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.program-side { display: flex; grid-column: 2; grid-row: 1 / span 3; flex-direction: column; gap: 12px; padding: 8px; border-left: 1px solid #aaa; background: #d9d9d9; }
.program-actions { display: flex; flex-wrap: wrap; gap: 5px; }
.active-gcodes-label { display: grid; gap: 5px; color: #333; }
.active-gcodes-label textarea { width: 100%; min-height: 84px; resize: vertical; border: 1px solid #999; background: white; color: #222; font: 11px/1.35 ui-monospace, monospace; }
@@ -208,9 +214,10 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
.axis-top-pane { grid-template-columns: 1fr; height: auto; min-height: 0; flex: 0 0 auto; }
.axis-notebook { min-height: 330px; }
.axis-preview { min-height: 320px; }
.axis-program-pane { flex: 0 0 auto; height: auto; min-height: 205px; grid-template-columns: 1fr; }
.axis-program-pane { flex: 0 0 auto; height: auto; min-height: 300px; grid-template-columns: 1fr; grid-template-rows: minmax(100px, 1fr) auto auto auto; }
.program-execution-line { grid-column: 1; grid-row: 2; }
.program-side { grid-column: 1; grid-row: 3; border-top: 1px solid #aaa; border-left: 0; }
.program-execution-trace { grid-column: 1; grid-row: 3; }
.program-side { grid-column: 1; grid-row: 4; border-top: 1px solid #aaa; border-left: 0; }
.active-gcodes-label textarea { min-height: 58px; }
}
@media (max-width: 620px) {

View File

@@ -180,6 +180,7 @@ export class SimSession {
status: statePayload.status,
cycles: statePayload.cycles,
motion_trace: trace,
execution_trace: Array.isArray(statePayload.execution_trace) ? statePayload.execution_trace : [],
motion_sample: motionSample,
snapshot_mode: "motion_delta",
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
@@ -1534,6 +1535,13 @@ export class SimSession {
return finish(this.error(input, "scheduler", String(error?.message ?? error)), "failed");
}
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null;
const execution_trace = program_steps
.map((step) => ({
line: step.line ?? step.program?.execution_line ?? null,
text: step.program?.execution_line_text ?? "",
program_status: step.program_status ?? step.program?.program_status ?? null,
}))
.filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0);
if (program) {
const status = this.xyzacProgram ? (program.status === 4 ? "completed" : program.status === 2 ? "running" : program.status === 3 ? "paused" : "loaded") : (program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status);
this.task.setProgram(status, this.xyzacProgram ? program.current_line : program.line);
@@ -1544,7 +1552,7 @@ export class SimSession {
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
const lightweightXyzac = Boolean(this.xyzacProgram);
return finish(this.envelope("COMMAND_RESULT", input, {
status: "verified", cycles, program_steps,
status: "verified", cycles, program_steps, execution_trace,
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
motion_authority: this.motionAuthority,
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,