feat: keep UML focused on state navigation

This commit is contained in:
user
2026-07-17 14:40:32 +02:00
parent b36a40cc71
commit 126ae51d1e
2 changed files with 8 additions and 292 deletions
+8
View File
@@ -99,6 +99,14 @@ Wyłączenie `viewer.nvim.sync` automatycznie wyłącza również
`viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja `viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja
sterowania klawiaturą bez jawnego `control: true`. sterowania klawiaturą bez jawnego `control: true`.
### Podział odpowiedzialności widoków
Interaktywny diagram UML pokazuje przepływ i służy do wyboru kroku. Nie
renderuje pod SVG osobnych paneli kodu, terminala, rejestrów, pamięci ani ramki
stosu. Te dane prezentuje `Nvim view`, ustawiany przez snapshot wybranego
kroku. Snapshoty pozostają w JSON/API, mimo że nie są powielane wizualnie pod
diagramem.
## Postęp zajęć i raport ## Postęp zajęć i raport
| Metoda i endpoint | Znaczenie | | Metoda i endpoint | Znaczenie |
-292
View File
@@ -1991,7 +1991,6 @@ function SnapshotInteractiveUmlSequence({
phase.steps.map((step) => ({ phase, step })), phase.steps.map((step) => ({ phase, step })),
); );
const storageKey = `${config.storage_key ?? asset.label}-uml-step`; const storageKey = `${config.storage_key ?? asset.label}-uml-step`;
const viewStorageKey = `${config.storage_key ?? asset.label}-evidence-view`;
const focusStorageKey = `${config.storage_key ?? asset.label}-navigation-level`; const focusStorageKey = `${config.storage_key ?? asset.label}-navigation-level`;
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);
@@ -2001,10 +2000,6 @@ function SnapshotInteractiveUmlSequence({
const [svgMarkup, setSvgMarkup] = useState(""); const [svgMarkup, setSvgMarkup] = useState("");
const [svgFailed, setSvgFailed] = useState(false); const [svgFailed, setSvgFailed] = useState(false);
const [revealRevision, setRevealRevision] = useState(0); const [revealRevision, setRevealRevision] = useState(0);
const [approvalBusy, setApprovalBusy] = useState(false);
const [evidenceView, setEvidenceView] = useState<"code" | "terminal">(() =>
localStorage.getItem(viewStorageKey) === "terminal" ? "terminal" : "code",
);
const [stepIndex, setStepIndex] = useState(() => const [stepIndex, setStepIndex] = useState(() =>
Math.max( Math.max(
0, 0,
@@ -2026,10 +2021,6 @@ function SnapshotInteractiveUmlSequence({
() => localStorage.setItem(storageKey, String(stepIndex)), () => localStorage.setItem(storageKey, String(stepIndex)),
[stepIndex, storageKey], [stepIndex, storageKey],
); );
useEffect(
() => localStorage.setItem(viewStorageKey, evidenceView),
[evidenceView, viewStorageKey],
);
useEffect( useEffect(
() => localStorage.setItem(focusStorageKey, focusLevel), () => localStorage.setItem(focusStorageKey, focusLevel),
[focusLevel, focusStorageKey], [focusLevel, focusStorageKey],
@@ -2405,53 +2396,7 @@ function SnapshotInteractiveUmlSequence({
}, [active, entries, focusLevel, onSetStatus, progress]); }, [active, entries, focusLevel, onSetStatus, progress]);
if (!active) return null; if (!active) return null;
const { phase, step } = active; const { phase, step } = active;
const snapshot = step.snapshot;
const completed = Boolean(
step.progress_id &&
progress?.items?.[step.progress_id]?.status === "approved",
);
const phaseEntries = entries.filter((entry) => entry.phase.id === phase.id); const phaseEntries = entries.filter((entry) => entry.phase.id === phase.id);
const arena = snapshot.memory.arena;
const arenaPercent = arena?.offset == null
? 0
: Math.min(100, (arena.offset / arena.size_bytes) * 100);
const terminalView = config.terminal_view;
const visibleEvidenceView =
evidenceView === "terminal" && terminalView?.href ? "terminal" : "code";
const frameFields = [...snapshot.frame.fields].sort(
(left, right) =>
Number.parseInt(left.address, 16) - Number.parseInt(right.address, 16),
);
const frameStart = Number.parseInt(snapshot.frame.sp, 16);
const frameEnd = Number.parseInt(snapshot.frame.fp, 16);
const frameSegments: Array<
| { kind: "gap"; address: number; sizeBytes: number }
| { kind: "field"; field: UmlSnapshotFrameField; sizeBytes: number }
> = [];
let frameCursor = frameStart;
if (Number.isFinite(frameStart) && Number.isFinite(frameEnd)) {
frameFields.forEach((field) => {
const address = Number.parseInt(field.address, 16);
const sizeBytes = field.size_bytes ?? 4;
if (!Number.isFinite(address) || address < frameStart || address >= frameEnd) return;
if (address > frameCursor) {
frameSegments.push({
kind: "gap",
address: frameCursor,
sizeBytes: address - frameCursor,
});
}
frameSegments.push({ kind: "field", field, sizeBytes });
frameCursor = Math.max(frameCursor, address + sizeBytes);
});
if (frameCursor < frameEnd) {
frameSegments.push({
kind: "gap",
address: frameCursor,
sizeBytes: frameEnd - frameCursor,
});
}
}
return ( return (
<div <div
@@ -2523,243 +2468,6 @@ function SnapshotInteractiveUmlSequence({
)} )}
</div> </div>
</div> </div>
<section className="uml-evidence-view" aria-label="Dowód dla wybranego kroku">
<header>
<div>
<small>
{visibleEvidenceView === "terminal"
? "DOWÓD REFERENCYJNY · TERMDEBUG"
: `DOWÓD · KROK ${String(step.number).padStart(2, "0")}`}
</small>
<strong>
{visibleEvidenceView === "terminal"
? terminalView?.title ?? "Termdebug"
: step.label}
</strong>
</div>
<div className="uml-evidence-tabs" role="tablist" aria-label="Rodzaj dowodu">
<button
role="tab"
aria-selected={visibleEvidenceView === "code"}
onClick={() => setEvidenceView("code")}
>
Kod
</button>
<button
role="tab"
aria-selected={visibleEvidenceView === "terminal"}
disabled={!terminalView?.href}
title={terminalView?.href ? undefined : "Brak widoku terminala dla tej figury"}
onClick={() => setEvidenceView("terminal")}
>
Terminal
</button>
</div>
</header>
{visibleEvidenceView === "terminal" && terminalView?.href ? (
<figure className="uml-terminal-view" role="tabpanel">
<img src={terminalView.href} alt={terminalView.alt} />
{(terminalView.title || terminalView.caption) && (
<figcaption>
{terminalView.title && <strong>{terminalView.title}</strong>}
{terminalView.caption && <span>{terminalView.caption}</span>}
</figcaption>
)}
</figure>
) : (
<div className="uml-code-view" role="tabpanel">
<section>
<header>
<span>C</span>
<code>{snapshot.code?.source_ref ?? step.code_ref ?? "źródło"}</code>
</header>
<pre><code>{snapshot.code?.source ?? step.label}</code></pre>
</section>
<section>
<header>
<span>RV32</span>
<code>pc = {snapshot.pc}</code>
</header>
<pre><code>{snapshot.code?.assembly ?? step.code_ref ?? snapshot.pc}</code></pre>
</section>
{snapshot.code?.note && <p>{snapshot.code.note}</p>}
</div>
)}
</section>
<aside className="uml-snapshot-panel" aria-live="polite">
<header>
<div>
<span>{phase.label} · STAN HAZARD3</span>
<strong>
<code>{String(step.number).padStart(2, "0")}</code> {step.label}
</strong>
</div>
<div className="uml-snapshot-cursor">
<button
aria-label="Poprzedni krok"
disabled={stepIndex === 0}
onClick={() => selectIndex(stepIndex - 1)}
>
</button>
<output>{stepIndex + 1}/{entries.length}</output>
<button
aria-label="Następny krok"
disabled={stepIndex === entries.length - 1}
onClick={() => selectIndex(stepIndex + 1)}
>
</button>
</div>
</header>
<p className="uml-snapshot-description">{step.description}</p>
<div className="uml-state-grid">
<section className="uml-snapshot-registers">
<h4>Rejestry</h4>
<dl>
<div className="primary"><dt>pc</dt><dd>{snapshot.pc}</dd></div>
{snapshot.registers.map((register) => (
<div key={register.name} title={register.note}>
<dt>{register.name}</dt>
<dd>{register.value}</dd>
</div>
))}
</dl>
</section>
<section className="uml-snapshot-variables">
<h4>Zmienne · {snapshot.frame.name}</h4>
<dl>
{snapshot.frame.fields.map((field) => (
<div
key={`${field.name}-${field.address}`}
data-state={field.state}
title={field.note}
>
<dt>{field.name}<small>{field.address}</small></dt>
<dd>{field.value}</dd>
</div>
))}
</dl>
</section>
<section className="uml-snapshot-memory">
<h4>Pamięć statyczna · .bss</h4>
{arena && (
<div className="uml-arena-snapshot">
<div>
<span style={{ width: `${arenaPercent}%` }} />
{arena.offset != null && <i style={{ left: `${arenaPercent}%` }} />}
</div>
<p>
<code>{arena.base}</code>
<strong>offset {arena.offset ?? "—"}/{arena.size_bytes}</strong>
<code>{arena.cursor}</code>
</p>
<code className="uml-arena-bytes">{arena.bytes}</code>
</div>
)}
<dl>
{snapshot.memory.items.map((item) => (
<div key={`${item.name}-${item.address}`} title={item.note}>
<dt>{item.name} <small>{item.address}</small></dt>
<dd>{item.value}</dd>
</div>
))}
</dl>
</section>
</div>
<section className="uml-snapshot-stack">
<header>
<h4>Ramka stosu · {snapshot.frame.name} · {snapshot.frame.size_bytes} B</h4>
<p>
<code>sp {snapshot.frame.sp}</code>
<code>s0/fp {snapshot.frame.fp}</code>
{(snapshot.frame.stack_top || config.snapshot_context?.stack_top) && (
<code>
top {snapshot.frame.stack_top ?? config.snapshot_context?.stack_top}
</code>
)}
</p>
</header>
<div className="uml-stack-axis" aria-hidden="true">
<span>niższe adresy</span><span>stos rośnie w dół </span><span>wyższe adresy</span>
</div>
<div className="uml-stack-lane">
<strong className="uml-stack-boundary"><small>sp</small>{snapshot.frame.sp}</strong>
{frameSegments.map((segment, index) =>
segment.kind === "gap" ? (
<span
className="uml-stack-gap"
key={`gap-${segment.address}-${index}`}
style={{ flexGrow: Math.max(1, segment.sizeBytes / 4) }}
>
<strong>{segment.sizeBytes} B</strong>
<small>ABI / inne sloty</small>
</span>
) : (
<article
key={`${segment.field.name}-${segment.field.address}`}
data-state={segment.field.state}
title={segment.field.note}
style={{ flexGrow: Math.max(1, segment.sizeBytes / 4) }}
>
<strong>{segment.field.name}</strong>
<code>{segment.field.value}</code>
<small>{segment.field.address}</small>
</article>
),
)}
<strong className="uml-stack-boundary"><small>s0/fp</small>{snapshot.frame.fp}</strong>
</div>
</section>
<section className="uml-snapshot-observation">
<h4>Opis stanu</h4>
<p>{snapshot.description}</p>
<small>
{snapshot.target ?? config.snapshot_context?.target} ·{" "}
{snapshot.captured_at ?? config.snapshot_context?.captured_at}
{snapshot.captured_with || config.snapshot_context?.captured_with
? ` · ${snapshot.captured_with ?? config.snapshot_context?.captured_with}`
: ""}
</small>
</section>
</aside>
{step.progress_id && (
<footer className="uml-approval">
<span>
{completed
? "Ten krok jest zatwierdzony i zapisany w JSON-ie zajęć."
: "Kursor wybiera strzałkę; zatwierdzenie zapisuje krok i timestamp."}
</span>
<button
disabled={!progress || approvalBusy}
onClick={async () => {
if (!step.progress_id || !progress) return;
setApprovalBusy(true);
try {
await onSetStatus(
step.progress_id,
completed ? "pending" : "approved",
);
} finally {
setApprovalBusy(false);
}
}}
>
{approvalBusy
? "Zapisywanie…"
: completed
? "Cofnij zatwierdzenie"
: "Zatwierdź krok"}
</button>
</footer>
)}
</div> </div>
); );
} }