diff --git a/frontend/app.js b/frontend/app.js index df9e3c6..fd92129 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1322,6 +1322,96 @@ function renderSelected() { el.netEditor.classList.add("hidden"); } +function issueById(issueId) { + const issues = [...(state.compile?.errors ?? []), ...(state.compile?.warnings ?? [])]; + return issues.find((i) => i.id === issueId) ?? null; +} + +function parseRefPinFromIssueMessage(issue) { + const m = /'([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z0-9_]+)'/.exec(String(issue?.message ?? "")); + if (m) { + return { ref: m[1], pin: m[2] }; + } + return null; +} + +function ensureNet(name, netClass = "signal") { + if (!state.model) { + return null; + } + let net = netByName(name); + if (!net) { + net = { name, class: netClass, nodes: [] }; + state.model.nets.push(net); + } + return net; +} + +function issueFixAction(issue) { + if (!issue?.code) { + return null; + } + if (issue.code === "ground_net_missing") { + return { label: "Create GND Net", action: "create_ground" }; + } + if (issue.code === "floating_input") { + const rp = parseRefPinFromIssueMessage(issue); + if (rp) { + return { label: "Create Signal Net", action: "connect_signal", ref: rp.ref, pin: rp.pin }; + } + } + if (issue.code === "required_power_unconnected") { + const rp = parseRefPinFromIssueMessage(issue); + if (rp) { + return { label: "Connect Power", action: "connect_power", ref: rp.ref, pin: rp.pin }; + } + } + return null; +} + +async function applyIssueFix(issueId) { + if (!state.model) { + return; + } + const issue = issueById(issueId); + const fix = issueFixAction(issue); + if (!issue || !fix) { + el.jsonFeedback.textContent = "No automatic fix available for this issue."; + return; + } + + pushHistory("issue-fix"); + let applied = false; + + if (fix.action === "create_ground") { + ensureNet("GND", "ground"); + applied = true; + } else if (fix.action === "connect_signal") { + const name = normalizeNetName(`NET_${fix.ref}_${fix.pin}`); + applied = connectPinToNet(fix.ref, fix.pin, name || nextAutoNetName(), { netClass: "signal" }).ok; + } else if (fix.action === "connect_power") { + const upperPin = String(fix.pin).toUpperCase(); + if (upperPin.includes("GND")) { + applied = connectPinToNet(fix.ref, fix.pin, "GND", { netClass: "ground" }).ok; + } else if (netByName("3V3")) { + applied = connectPinToNet(fix.ref, fix.pin, "3V3", { netClass: "power" }).ok; + } else if (netByName("5V")) { + applied = connectPinToNet(fix.ref, fix.pin, "5V", { netClass: "power" }).ok; + } else { + applied = connectPinToNet(fix.ref, fix.pin, normalizeNetName(`PWR_${fix.ref}_${fix.pin}`), { netClass: "power" }).ok; + } + } + + if (!applied) { + el.jsonFeedback.textContent = "Automatic fix could not be applied safely."; + return; + } + + await compileModel(state.model, { keepView: true, source: "issue-fix" }); + el.jsonFeedback.textContent = `Applied fix for ${issue.code}.`; + focusIssue(issueId); +} + function renderIssues() { const errors = state.compile?.errors ?? []; const warnings = state.compile?.warnings ?? []; @@ -1333,12 +1423,16 @@ function renderIssues() { const rows = [ ...errors.map( - (issue) => - `
[E] ${issue.message}
${issue.code} · ${issue.path ?? "-"}
${issue.suggestion ?? ""}
` + (issue) => { + const fix = issueFixAction(issue); + return `
[E] ${issue.message}
${issue.code} · ${issue.path ?? "-"}
${issue.suggestion ?? ""}
${fix ? `
` : ""}
`; + } ), ...warnings.map( - (issue) => - `
[W] ${issue.message}
${issue.code} · ${issue.path ?? "-"}
${issue.suggestion ?? ""}
` + (issue) => { + const fix = issueFixAction(issue); + return `
[W] ${issue.message}
${issue.code} · ${issue.path ?? "-"}
${issue.suggestion ?? ""}
${fix ? `
` : ""}
`; + } ) ]; @@ -2394,6 +2488,12 @@ function setupEvents() { }); el.issues.addEventListener("click", (evt) => { + const fixBtn = evt.target.closest("[data-fix-issue]"); + if (fixBtn) { + evt.stopPropagation(); + void applyIssueFix(fixBtn.getAttribute("data-fix-issue")); + return; + } const row = evt.target.closest("[data-issue-id]"); if (!row) { return; diff --git a/frontend/styles.css b/frontend/styles.css index 09f008a..2605b4e 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -575,6 +575,15 @@ textarea { color: var(--ink-muted); } +.issueActions { + margin-top: 6px; +} + +.issueActions button { + font-size: 0.74rem; + padding: 3px 8px; +} + .modal { position: fixed; inset: 0; diff --git a/tests/baselines/ui/explicit-mode-auto-tidy.png b/tests/baselines/ui/explicit-mode-auto-tidy.png index fc30f58..241794f 100644 Binary files a/tests/baselines/ui/explicit-mode-auto-tidy.png and b/tests/baselines/ui/explicit-mode-auto-tidy.png differ diff --git a/tests/baselines/ui/post-migration-apply.png b/tests/baselines/ui/post-migration-apply.png index ca4d544..c6c0788 100644 Binary files a/tests/baselines/ui/post-migration-apply.png and b/tests/baselines/ui/post-migration-apply.png differ