From 3503f043983e049fc1feb565359baed008d46352 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:04:07 -0400 Subject: [PATCH 1/7] Rename devtools rebuild-remotely label to 'Rebuild from Main' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote rebuild pulls different tracked branches for the two repos (local/amicode for opencode, main for amicode), so no single label is fully precise for both sides. This reverses an earlier pass at this same label ('Pull & Rebuild', chosen because it read as accurate for both repos) back to 'Rebuild from Main' per explicit preference — it's the branch users actually think about when they look at this button, even though it's not the literal branch the opencode side pulls. TDD: extended the existing rebuild-label-rename regression test (the one that already scans the app package for the stale 'Rebuild from Latest' string and asserts en.ts's key value) by updating its NEW_LABEL constant to 'Rebuild from Main' — same test, same guarantee, new target string. i18n parity suite and typecheck both still pass. --- packages/app/src/i18n/ar.ts | 2 +- packages/app/src/i18n/br.ts | 2 +- packages/app/src/i18n/bs.ts | 2 +- packages/app/src/i18n/da.ts | 2 +- packages/app/src/i18n/de.ts | 2 +- packages/app/src/i18n/en.ts | 2 +- packages/app/src/i18n/es.ts | 2 +- packages/app/src/i18n/fr.ts | 2 +- packages/app/src/i18n/ja.ts | 2 +- packages/app/src/i18n/ko.ts | 2 +- packages/app/src/i18n/no.ts | 2 +- packages/app/src/i18n/pl.ts | 2 +- .../app/src/i18n/rebuild-label-rename.test.ts | 53 +++++++++++++++++++ packages/app/src/i18n/ru.ts | 2 +- packages/app/src/i18n/th.ts | 2 +- packages/app/src/i18n/tr.ts | 2 +- packages/app/src/i18n/uk.ts | 2 +- packages/app/src/i18n/zh.ts | 2 +- packages/app/src/i18n/zht.ts | 2 +- packages/app/src/utils/amicode-developer.ts | 2 +- 20 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 packages/app/src/i18n/rebuild-label-rename.test.ts diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 9de1b4365..bd3b4b387 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -774,7 +774,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "ملاحظات الإصدار", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index b18c9d90d..6b60ebbc5 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -786,7 +786,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Notas da versão", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 84cac1b09..90b22d6de 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -852,7 +852,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Bilješke o izdanju", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b7fbf543a..fb57a8089 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -844,7 +844,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Udgivelsesnoter", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index b455c97b4..b23ea6f68 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -798,7 +798,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Versionshinweise", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 32dcd9b0e..7c27ad16e 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1030,7 +1030,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 0cf7ee55a..5cfe7ccc2 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -857,7 +857,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Notas de la versión", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 97fc99a32..75241ba50 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -796,7 +796,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Notes de version", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index a83bb488b..38471937f 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -781,7 +781,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "リリースノート", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 136daba17..ee7ef6ef9 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -623,7 +623,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "릴리스 노트", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 74fb5d19c..3efd8df5e 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -697,7 +697,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Utgivelsesnotater", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 9a0a15b02..786d93afe 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -786,7 +786,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Informacje o wydaniu", diff --git a/packages/app/src/i18n/rebuild-label-rename.test.ts b/packages/app/src/i18n/rebuild-label-rename.test.ts new file mode 100644 index 000000000..6b9d0bf04 --- /dev/null +++ b/packages/app/src/i18n/rebuild-label-rename.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "bun:test" +import { readdirSync, readFileSync } from "node:fs" +import { join, dirname } from "node:path" +import { fileURLToPath } from "node:url" + +// ============================================================================ +// The devtools "rebuild from remote" button label was renamed away from its +// old wording (#940) — the old label was inaccurate (the remote rebuild +// pulls different tracked branches for the opencode vs. amicode repos, not +// "latest" in any single sense). This regression test guards against a +// partial rename: every locale's key must carry the new label, and no +// stale reference (including code comments) may remain anywhere in the +// app package's source tree. +// ============================================================================ + +const OLD_LABEL = ["Rebuild", "from", "Latest"].join(" ") +const NEW_LABEL = "Rebuild from Main" + +const i18nDir = dirname(fileURLToPath(import.meta.url)) +const appSrcDir = join(i18nDir, "..") +const selfFile = fileURLToPath(import.meta.url) + +function walkTsFiles(dir: string): string[] { + const out: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "node_modules" || entry.name.startsWith(".")) continue + const full = join(dir, entry.name) + if (entry.isDirectory()) { + out.push(...walkTsFiles(full)) + } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) { + out.push(full) + } + } + return out +} + +describe("devtools rebuild-remotely label rename (#940)", () => { + test("no source file under the app package still carries the old label", () => { + const offenders = walkTsFiles(appSrcDir).filter( + (file) => file !== selfFile && readFileSync(file, "utf8").includes(OLD_LABEL), + ) + expect(offenders).toEqual([]) + }) + + test("en.ts carries the new label for the rebuildRemotely key", async () => { + const module: unknown = await import("./en") + if (typeof module !== "object" || module === null || !("dict" in module)) { + throw new Error("Invalid en.ts dictionary module") + } + const dict = (module as { dict: Record }).dict + expect(dict["settings.general.row.devTools.rebuildRemotely"]).toBe(NEW_LABEL) + }) +}) diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 83257d76a..de193d70b 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -853,7 +853,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Примечания к выпуску", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 64c3bfba9..8b1562fe7 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -840,7 +840,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "บันทึกการอัปเดต", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index cf0359f9a..5d4d3ebf1 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -859,7 +859,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Sürüm notları", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index b41531c2c..8ac32e61c 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -946,7 +946,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "Нотатки до релізу", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 8bd203429..a5bdc386c 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -833,7 +833,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "发行说明", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 4ff353bfa..7c1aa6e48 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -830,7 +830,7 @@ export const dict = { "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", - "settings.general.row.devTools.rebuildRemotely": "Rebuild from Latest", + "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", "settings.general.row.releaseNotes.title": "發行說明", diff --git a/packages/app/src/utils/amicode-developer.ts b/packages/app/src/utils/amicode-developer.ts index 74978683a..dfba15580 100644 --- a/packages/app/src/utils/amicode-developer.ts +++ b/packages/app/src/utils/amicode-developer.ts @@ -1,6 +1,6 @@ // amicode developer-mode boot flag: the extension passes // `amicode_developer=1` in the iframe URL when `devAssetRoot` is configured -// (i.e. the user ran "Rebuild from Latest"). The app reads it once at boot +// (i.e. the user ran "Rebuild from Main"). The app reads it once at boot // (same convention as amicode-bug-report / amicode-hide-project) and uses it // to auto-enable settings.developer.enabled — without this, ephemeral ports // rotate the localStorage origin on every reload and the developer toggle From 369274091af24ff01cc314e5311fce362fb905e5 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:07:13 -0400 Subject: [PATCH 2/7] Fix premature 'Rebuilt!' status shown mid-build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rebuild() set the amicode:devtools-rebuilt flag at the same time as amicode:devtools-rebuilding, so reopening the settings dialog while a rebuild was still running showed 'Rebuilt!' instead of 'Rebuilding...'. Worse, simply removing that premature set (without a corresponding fix) would have regressed the success case entirely: the dev-tools-rebuild-status 'done' handler only ever cleared 'rebuilding' — it never set 'rebuilt' — so after a successful rebuild, onMount would see no flags at all and silently fall back to 'idle'. Extracted the flag lifecycle (start / done / failed) into a pure rebuildFlagMutation() function in developer-tools-rebuild-flags.ts, so the protocol is unit-testable without a SolidJS reactive root or settings context. rebuild() now only sets rebuilding+reopen at start; the 'done' handler now sets 'rebuilt' (and clears 'rebuilding'); the timeout and 'failed' paths share the same clear-only mutation. TDD: wrote the pure-function tests + a localStorage integration test reproducing both the original bug (start alone must never look like 'rebuilt') and the naive-fix regression (done must actually set the flag) before the module existed — confirmed RED (module not found), implemented, confirmed GREEN (7/7), then wired the controller and reran the full settings-v2 suite (9/9). --- .../settings-v2/developer-tools-controller.ts | 20 ++--- .../developer-tools-rebuild-flags.test.ts | 84 +++++++++++++++++++ .../developer-tools-rebuild-flags.ts | 58 +++++++++++++ 3 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 packages/app/src/components/settings-v2/developer-tools-rebuild-flags.test.ts create mode 100644 packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts diff --git a/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app/src/components/settings-v2/developer-tools-controller.ts index b15414690..948a57a39 100644 --- a/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -1,6 +1,7 @@ import { createSignal, onCleanup, onMount } from "solid-js" import { useSettings } from "@/context/settings" import { inAmicode } from "@/utils/amicode-bridge" +import { applyRebuildFlagMutation, rebuildFlagMutation } from "./developer-tools-rebuild-flags" export interface DevToolsStatus { opencodeValid: boolean @@ -48,7 +49,7 @@ export function createDeveloperToolsController() { // Safety timeout: clear after 5 min to avoid permanently stuck state setTimeout(() => { if (rebuildState() === "rebuilding") { - try { localStorage.removeItem("amicode:devtools-rebuilding") } catch {} + applyRebuildFlagMutation(rebuildFlagMutation("failed")) setRebuildState("failed") setRebuildError("Rebuild timed out") } @@ -97,12 +98,15 @@ export function createDeveloperToolsController() { setRebuildState("rebuilding") setRebuildError(undefined) } else if (d.state === "failed") { - try { localStorage.removeItem("amicode:devtools-rebuilding") } catch {} + applyRebuildFlagMutation(rebuildFlagMutation("failed")) setRebuildState("failed") setRebuildError(d.error ?? "Unknown error") } else if (d.state === "done") { - try { localStorage.removeItem("amicode:devtools-rebuilding") } catch {} - // The window reload follows shortly — "rebuilt" flag is read on next mount + // The extension host confirmed the build finished — set the + // "rebuilt" flag now (not at rebuild-start) so a dialog reopened + // after the window reload correctly shows "Rebuilt!" rather than + // "Rebuilding..." (#940). The window reload follows shortly. + applyRebuildFlagMutation(rebuildFlagMutation("done")) } } @@ -153,13 +157,7 @@ export function createDeveloperToolsController() { if (rebuildState() === "rebuilding") return // prevent double-clicks setRebuildState("rebuilding") setRebuildError(undefined) - try { - localStorage.setItem("amicode:devtools-rebuilding", "1") - localStorage.setItem("amicode:devtools-reopen", "1") - localStorage.setItem("amicode:devtools-rebuilt", "1") - } catch { - // non-critical - } + applyRebuildFlagMutation(rebuildFlagMutation("start")) window.parent.postMessage( { source: "amicode", diff --git a/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.test.ts b/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.test.ts new file mode 100644 index 000000000..589729f7b --- /dev/null +++ b/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from "bun:test" +import { rebuildFlagMutation, type RebuildFlagEvent } from "./developer-tools-rebuild-flags" + +// ============================================================================ +// The devtools rebuild flag protocol (#940): localStorage flags survive +// iframe reloads mid-build (git checkout in a watched workspace triggers a +// reload), so the app's onMount can tell "still rebuilding" apart from +// "finished while you were away". The bug: rebuild() used to set the +// "rebuilt" flag at the SAME time as "rebuilding", so reopening the dialog +// mid-build showed "Rebuilt!" instead of "Rebuilding...". This tests the +// pure decision of what to set/clear at each lifecycle event, independent +// of the SolidJS signal wiring around it. +// ============================================================================ + +describe("rebuildFlagMutation", () => { + test("start sets rebuilding+reopen but never rebuilt", () => { + const mutation = rebuildFlagMutation("start") + expect(mutation.set).toEqual({ rebuilding: "1", reopen: "1" }) + expect(mutation.clear).toEqual([]) + // The exact regression: "rebuilt" must not appear in the start mutation. + expect("rebuilt" in mutation.set).toBe(false) + }) + + test("done clears rebuilding and sets rebuilt — the success signal onMount needs", () => { + const mutation = rebuildFlagMutation("done") + expect(mutation.clear).toEqual(["rebuilding"]) + expect(mutation.set).toEqual({ rebuilt: "1" }) + }) + + test("failed clears rebuilding without ever setting rebuilt", () => { + const mutation = rebuildFlagMutation("failed") + expect(mutation.clear).toEqual(["rebuilding"]) + expect(mutation.set).toEqual({}) + }) + + test("every event kind produces a defined mutation (exhaustiveness)", () => { + const events: RebuildFlagEvent[] = ["start", "done", "failed"] + for (const event of events) { + expect(rebuildFlagMutation(event)).toBeDefined() + } + }) +}) + +describe("rebuild flag lifecycle — localStorage integration", () => { + const KEY = (suffix: string) => `amicode:devtools-${suffix}` + + function applyMutation(mutation: ReturnType) { + for (const k of mutation.clear) localStorage.removeItem(KEY(k)) + for (const [k, v] of Object.entries(mutation.set)) localStorage.setItem(KEY(k), v) + } + + test("reopening mid-build (before 'done' arrives) shows rebuilding, not rebuilt", () => { + localStorage.clear() + applyMutation(rebuildFlagMutation("start")) + + // Simulate the dialog reopening mid-build: onMount reads flags directly. + const wasRebuilding = localStorage.getItem(KEY("rebuilding")) === "1" + const didFinish = localStorage.getItem(KEY("rebuilt")) === "1" + + expect(wasRebuilding).toBe(true) + expect(didFinish).toBe(false) // this is the exact bug this fix prevents + }) + + test("full successful lifecycle: start -> done -> reopen shows rebuilt", () => { + localStorage.clear() + applyMutation(rebuildFlagMutation("start")) + applyMutation(rebuildFlagMutation("done")) + + const wasRebuilding = localStorage.getItem(KEY("rebuilding")) === "1" + const didFinish = localStorage.getItem(KEY("rebuilt")) === "1" + + expect(wasRebuilding).toBe(false) + expect(didFinish).toBe(true) + }) + + test("failed lifecycle: start -> failed leaves no success flag behind", () => { + localStorage.clear() + applyMutation(rebuildFlagMutation("start")) + applyMutation(rebuildFlagMutation("failed")) + + expect(localStorage.getItem(KEY("rebuilding"))).toBeNull() + expect(localStorage.getItem(KEY("rebuilt"))).toBeNull() + }) +}) diff --git a/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts b/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts new file mode 100644 index 000000000..0d62799cf --- /dev/null +++ b/packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts @@ -0,0 +1,58 @@ +/** + * The devtools rebuild flag protocol. + * + * localStorage flags (amicode:devtools-rebuilding / -rebuilt / -reopen) + * survive iframe reloads that happen mid-rebuild (e.g. a git checkout + * inside a watched workspace folder during a remote rebuild). The + * controller's onMount reads them to tell "still rebuilding" apart from + * "finished while the dialog was closed". + * + * This module is the pure decision of what to set/clear at each lifecycle + * event, kept separate from the SolidJS signal wiring so the protocol + * itself is directly testable. The critical invariant it encodes: "start" + * must never set "rebuilt" — only "done" may, and only once the extension + * host has actually reported completion. + */ + +export type RebuildFlagEvent = "start" | "done" | "failed" + +export type RebuildFlagKey = "rebuilding" | "reopen" | "rebuilt" + +export interface RebuildFlagMutation { + /** Flags to set to "1". */ + set: Partial> + /** Flags to remove. */ + clear: RebuildFlagKey[] +} + +export function rebuildFlagMutation(event: RebuildFlagEvent): RebuildFlagMutation { + switch (event) { + case "start": + // Rebuild kicks off: mark it in progress and ask the app to reopen + // settings at the devtools section after any reload. Do NOT set + // "rebuilt" here — that was the bug (#940): it made a mid-build + // dialog reopen show "Rebuilt!" instead of "Rebuilding...". + return { set: { rebuilding: "1", reopen: "1" }, clear: [] } + case "done": + // Extension host reported success: clear the in-progress flag and + // set the success flag onMount needs to show "Rebuilt!" after reload. + return { set: { rebuilt: "1" }, clear: ["rebuilding"] } + case "failed": + // Extension host reported failure: clear in-progress, no success flag. + return { set: {}, clear: ["rebuilding"] } + } +} + +const STORAGE_PREFIX = "amicode:devtools-" + +/** Apply a mutation to localStorage. Swallows errors (storage may be unavailable). */ +export function applyRebuildFlagMutation(mutation: RebuildFlagMutation): void { + try { + for (const key of mutation.clear) localStorage.removeItem(STORAGE_PREFIX + key) + for (const [key, value] of Object.entries(mutation.set)) { + if (value) localStorage.setItem(STORAGE_PREFIX + key, value) + } + } catch { + // non-critical + } +} From b4096a7afa41c11bea9c2e7b037c1e668707ea77 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:11:25 -0400 Subject: [PATCH 3/7] Fix devtools path-validation flicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sendUpdate() blanked `status` to undefined the instant a validation round-trip started, so any visible error/success indicator vanished and then snapped back once the reply arrived — a visible flash on every path field blur. The message handler also set multiple signals (status, pending) outside a batch, risking an intermediate render between them. Modeled the request lifecycle as a pure reducer (reduceDevToolsRequest) in developer-tools-request-state.ts: 'request-sent' now keeps the stale status and only flips pending; 'status-received' replaces status and clears pending. sendUpdate() and the message handler now go through it, and the handler's signal writes are wrapped in solid-js's batch(). Since keeping stale status without any visual cue would trade flicker for silently-stale data, added a 'Validating…' indicator (bound to the existing but previously-unused `pending` signal) and dim the stale error text while a new round-trip is in flight. Also added the missing .settings-v2-field-info CSS rule the 'Building extension…' text already referenced but never had defined. TDD: wrote reduceDevToolsRequest's tests first (the key regression case: a second request-sent must keep the FIRST round-trip's status visible, not blank it) — confirmed RED (module not found), implemented, confirmed GREEN (4/4), wired the controller, reran the full settings-v2 suite (13/13), typechecked clean. --- .../settings-v2/developer-tools-controller.ts | 47 +++++++++------ .../developer-tools-request-state.test.ts | 58 +++++++++++++++++++ .../developer-tools-request-state.ts | 35 +++++++++++ .../settings-v2/developer-tools.tsx | 12 +++- .../components/settings-v2/settings-v2.css | 14 +++++ 5 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 packages/app/src/components/settings-v2/developer-tools-request-state.test.ts create mode 100644 packages/app/src/components/settings-v2/developer-tools-request-state.ts diff --git a/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app/src/components/settings-v2/developer-tools-controller.ts index 948a57a39..01a11bcd1 100644 --- a/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -1,7 +1,8 @@ -import { createSignal, onCleanup, onMount } from "solid-js" +import { batch, createSignal, onCleanup, onMount } from "solid-js" import { useSettings } from "@/context/settings" import { inAmicode } from "@/utils/amicode-bridge" import { applyRebuildFlagMutation, rebuildFlagMutation } from "./developer-tools-rebuild-flags" +import { reduceDevToolsRequest } from "./developer-tools-request-state" export interface DevToolsStatus { opencodeValid: boolean @@ -68,27 +69,31 @@ export function createDeveloperToolsController() { const handleMessage = (event: MessageEvent) => { const d = event.data if (d && d.source === "amicode" && d.kind === "dev-tools-status") { - setStatus({ - opencodeValid: d.opencodeValid ?? true, - opencodeError: d.opencodeError, - amicodeValid: d.amicodeValid ?? true, - amicodeError: d.amicodeError, - serverRestarted: d.serverRestarted ?? false, - reloadNeeded: d.reloadNeeded ?? false, - building: d.building ?? false, - buildError: d.buildError, + const next = reduceDevToolsRequest( + { status: status(), pending: pending() }, + { + type: "status-received", + status: { + opencodeValid: d.opencodeValid ?? true, + opencodeError: d.opencodeError, + amicodeValid: d.amicodeValid ?? true, + amicodeError: d.amicodeError, + serverRestarted: d.serverRestarted ?? false, + reloadNeeded: d.reloadNeeded ?? false, + building: d.building ?? false, + buildError: d.buildError, + }, + }, + ) + batch(() => { + setStatus(next.status) + setPending(next.pending) }) - setPending(false) // When a reload is needed (extension was rebuilt), set a flag so the app // reopens settings at the developer tools section after the reload. if (d.reloadNeeded) { - try { - localStorage.setItem("amicode:devtools-reopen", "1") - localStorage.setItem("amicode:devtools-rebuilt", "1") - } catch { - // localStorage unavailable — non-critical - } + applyRebuildFlagMutation({ set: { reopen: "1", rebuilt: "1" }, clear: [] }) } } @@ -138,8 +143,12 @@ export function createDeveloperToolsController() { const sendUpdate = () => { if (!inAmicode()) return - setPending(true) - setStatus(undefined) + // Keep the stale status visible (dimmed by the UI via `pending`) instead + // of blanking it — clearing it here is what caused the validation + // flicker (#940): every path edit made the error/success indicator + // vanish and then snap back once the reply arrived. + const next = reduceDevToolsRequest({ status: status(), pending: pending() }, { type: "request-sent" }) + setPending(next.pending) window.parent.postMessage( { source: "amicode", diff --git a/packages/app/src/components/settings-v2/developer-tools-request-state.test.ts b/packages/app/src/components/settings-v2/developer-tools-request-state.test.ts new file mode 100644 index 000000000..df5efa397 --- /dev/null +++ b/packages/app/src/components/settings-v2/developer-tools-request-state.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test" +import { reduceDevToolsRequest, type DevToolsRequestState } from "./developer-tools-request-state" +import type { DevToolsStatus } from "./developer-tools-controller" + +// ============================================================================ +// The devtools path-validation flicker (#940): sendUpdate() used to clear +// `status` to undefined the instant a validation round-trip started, so any +// visible error/success indicator vanished and then snapped back when the +// reply arrived — a visible flash on every path edit. This models the +// request lifecycle as a pure reducer so the "don't blank the status while +// a request is in flight" invariant is directly testable. +// ============================================================================ + +const sampleStatus: DevToolsStatus = { + opencodeValid: false, + opencodeError: "Binary not found at this path", + amicodeValid: true, + serverRestarted: false, + reloadNeeded: false, +} + +const idle: DevToolsRequestState = { status: undefined, pending: false } + +describe("reduceDevToolsRequest", () => { + test("request-sent while idle marks pending without inventing a status", () => { + const next = reduceDevToolsRequest(idle, { type: "request-sent" }) + expect(next).toEqual({ status: undefined, pending: true }) + }) + + test("status-received clears pending and sets the new status", () => { + const sent = reduceDevToolsRequest(idle, { type: "request-sent" }) + const received = reduceDevToolsRequest(sent, { type: "status-received", status: sampleStatus }) + expect(received.pending).toBe(false) + expect(received.status).toEqual(sampleStatus) + }) + + test("REGRESSION: a second request-sent must keep the STALE status visible, not blank it", () => { + // First round-trip already completed and produced an error. + const afterFirst: DevToolsRequestState = { status: sampleStatus, pending: false } + + // User edits the path again — a new request goes out. + const midSecondRequest = reduceDevToolsRequest(afterFirst, { type: "request-sent" }) + + // This is the exact bug: status must stay visible (not undefined) while + // the second round-trip is in flight. Only `pending` should flip. + expect(midSecondRequest.status).toEqual(sampleStatus) + expect(midSecondRequest.pending).toBe(true) + }) + + test("the second round-trip's reply replaces the stale status once it lands", () => { + const afterFirst: DevToolsRequestState = { status: sampleStatus, pending: false } + const midSecondRequest = reduceDevToolsRequest(afterFirst, { type: "request-sent" }) + const newStatus: DevToolsStatus = { ...sampleStatus, opencodeValid: true, opencodeError: undefined } + const afterSecond = reduceDevToolsRequest(midSecondRequest, { type: "status-received", status: newStatus }) + expect(afterSecond.status).toEqual(newStatus) + expect(afterSecond.pending).toBe(false) + }) +}) diff --git a/packages/app/src/components/settings-v2/developer-tools-request-state.ts b/packages/app/src/components/settings-v2/developer-tools-request-state.ts new file mode 100644 index 000000000..56081102a --- /dev/null +++ b/packages/app/src/components/settings-v2/developer-tools-request-state.ts @@ -0,0 +1,35 @@ +import type { DevToolsStatus } from "./developer-tools-controller" + +/** + * The devtools path-validation request lifecycle, as a pure reducer. + * + * Kept separate from the SolidJS signal wiring so the exact invariant that + * was wrong (#940) is directly testable: starting a new validation + * round-trip must NOT blank the currently-visible status. The old status + * (an error or a success indicator) stays on screen — dimmed by the UI via + * `pending` — until the new reply actually arrives. Clearing it eagerly is + * what produced the flicker: every path edit made the indicator vanish and + * then snap back a moment later. + */ + +export interface DevToolsRequestState { + status: DevToolsStatus | undefined + pending: boolean +} + +export type DevToolsRequestEvent = + | { type: "request-sent" } + | { type: "status-received"; status: DevToolsStatus } + +export function reduceDevToolsRequest( + state: DevToolsRequestState, + event: DevToolsRequestEvent, +): DevToolsRequestState { + switch (event.type) { + case "request-sent": + // Keep the stale status visible; only the pending flag changes. + return { status: state.status, pending: true } + case "status-received": + return { status: event.status, pending: false } + } +} diff --git a/packages/app/src/components/settings-v2/developer-tools.tsx b/packages/app/src/components/settings-v2/developer-tools.tsx index 904244708..00a2fcdcf 100644 --- a/packages/app/src/components/settings-v2/developer-tools.tsx +++ b/packages/app/src/components/settings-v2/developer-tools.tsx @@ -54,6 +54,7 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> const building = () => props.controller.status()?.building ?? false const reloadNeeded = () => props.controller.status()?.reloadNeeded ?? false const isRebuilding = () => props.controller.rebuildState() === "rebuilding" + const validating = () => props.controller.pending() return ( @@ -135,7 +136,12 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> <> {language.t("settings.general.row.opencodePath.description")} - {opencodeError()} + + {opencodeError()} + + + + Validating… } @@ -169,7 +175,9 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> - {amicodeError()} + + {amicodeError()} + diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css index 0d9eec2ec..6b22f7c21 100644 --- a/packages/app/src/components/settings-v2/settings-v2.css +++ b/packages/app/src/components/settings-v2/settings-v2.css @@ -1122,3 +1122,17 @@ button.settings-v2-permissions-matrix-pattern:focus-visible { font-size: 12px; color: var(--v2-text-text-warning, #d29922); } + +.settings-v2-field-info { + display: block; + margin-top: 4px; + font-size: 12px; + color: var(--v2-text-text-muted); +} + +/* Applied to a stale error/warning while a new validation round-trip is in + flight (#940) — dims the previous result instead of blanking it, so path + edits don't flash empty-then-filled on every keystroke's blur. */ +.settings-v2-field-stale { + opacity: 0.55; +} From be8323de10381cceb29df05f65bc4d6ae66b4745 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:15:11 -0400 Subject: [PATCH 4/7] Move auto-accept permissions toggle to the Permissions tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PermissionScopeSetting (the 'Auto-accept permissions' switch) lived in the General tab, but it is a permissions control — it belongs with the other permission settings, not general app preferences. Three coordinated changes make the move real: - general.tsx: removed the component definition, its controller instantiation, and its render call, plus the now-unused createPermissionScopeController/PermissionScopeController imports. - permissions.tsx: added the component (moved verbatim), instantiates createPermissionScopeController with the tab's own sessionID prop, and renders it at the top of the tab body, above the trust tiers. - dialog-settings-v2.tsx: threads sessionID into SettingsPermissionsV2, which never received it before (the controller needs it to resolve the active session's auto-accept state). TDD: no @solidjs/testing-library render harness exists in this codebase, so — following the same source-scanning pattern already used elsewhere (the 'outer relay script forwards preview-file to the iframe' structural test) — wrote a test asserting the move via source content: general.tsx no longer references PermissionScopeSetting/createPermissionScopeController, permissions.tsx does, and the dialog's permissions tab content passes sessionID. Confirmed RED (4/4 failing — nothing moved yet), performed the move, confirmed GREEN (4/4). Typechecked clean; full app suite still passes (1103/1103). --- .../settings-v2/dialog-settings-v2.tsx | 2 +- .../src/components/settings-v2/general.tsx | 23 -------- .../permission-scope-relocation.test.ts | 54 +++++++++++++++++++ .../components/settings-v2/permissions.tsx | 30 ++++++++++- 4 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 packages/app/src/components/settings-v2/permission-scope-relocation.test.ts diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index bad2b965f..2b2a30407 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -149,7 +149,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index b7e11ba9e..c83751273 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -14,13 +14,11 @@ import { SettingsRowV2 } from "./parts/row" import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" import { createAppearanceSettingsController, - createPermissionScopeController, createShellOptions, createShellSettingsController, createSoundSettingsController, soundOptions, type AppearanceSettingsController, - type PermissionScopeController, type ShellSettingsController, type SoundSettingsController, } from "./general-controllers" @@ -70,24 +68,6 @@ const soundSettings = { }, } as const -const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { - const language = useLanguage() - return ( - -
- -
-
- ) -} - const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => { const language = useLanguage() const options = createMemo(() => @@ -257,7 +237,6 @@ export const SettingsGeneralV2: Component<{ const settings = useSettings() const mobile = createMediaQuery("(max-width: 767px)") const updater = useUpdaterAction() - const permissionScope = createPermissionScopeController(() => props.sessionID) const shell = createShellSettingsController() const appearance = createAppearanceSettingsController() const sounds = createSoundSettingsController() @@ -306,8 +285,6 @@ export const SettingsGeneralV2: Component<{ - - readFileSync(join(here, "general.tsx"), "utf8") +const permissionsSrc = () => readFileSync(join(here, "permissions.tsx"), "utf8") +const dialogSrc = () => readFileSync(join(here, "dialog-settings-v2.tsx"), "utf8") + +/** Extract the JSX block for a given TabsV2.Content value, for a scoped assertion. */ +function extractTabContent(src: string, tabValue: string): string { + const start = src.indexOf(`", start) + return src.slice(start, end) +} + +describe("auto-accept permissions toggle relocation (#940)", () => { + test("general.tsx no longer defines or renders PermissionScopeSetting", () => { + expect(generalSrc()).not.toContain("PermissionScopeSetting") + }) + + test("general.tsx no longer creates a permission-scope controller", () => { + expect(generalSrc()).not.toContain("createPermissionScopeController") + }) + + test("permissions.tsx defines and renders PermissionScopeSetting", () => { + const src = permissionsSrc() + expect(src).toContain("PermissionScopeSetting") + expect(src).toContain("createPermissionScopeController") + }) + + test("the dialog shell threads sessionID into the Permissions tab", () => { + const permissionsTab = extractTabContent(dialogSrc(), "permissions") + expect(permissionsTab).toContain("SettingsPermissionsV2") + expect(permissionsTab).toContain("sessionID") + }) +}) diff --git a/packages/app/src/components/settings-v2/permissions.tsx b/packages/app/src/components/settings-v2/permissions.tsx index 61dadf1f3..36ef39cf7 100644 --- a/packages/app/src/components/settings-v2/permissions.tsx +++ b/packages/app/src/components/settings-v2/permissions.tsx @@ -2,11 +2,15 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Tag } from "@opencode-ai/ui/v2/badge-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { SelectV2 } from "@opencode-ai/ui/v2/select-v2" +import { Switch } from "@opencode-ai/ui/v2/switch-v2" import { showToast } from "@/utils/toast" import { createMemo, createSignal, For, Show, type Component } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSync } from "@/context/server-sync" import { useModels } from "@/context/models" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" +import { createPermissionScopeController, type PermissionScopeController } from "./general-controllers" import "./settings-v2.css" type Effect = "allow" | "deny" | "ask" @@ -57,10 +61,30 @@ function badgeVariant(summary: string): "danger" | "warning" | "neutral" | "info return "warning" } -export const SettingsPermissionsV2: Component = () => { +/** Moved here from the General tab (#940) — auto-accept is a permissions control. */ +const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { + const language = useLanguage() + return ( + +
+ +
+
+ ) +} + +export const SettingsPermissionsV2: Component<{ sessionID?: string }> = (props) => { const language = useLanguage() const serverSync = useServerSync() const modelsCtx = useModels() + const permissionScope = createPermissionScopeController(() => props.sessionID) const rawConfig = createMemo(() => { const raw = (serverSync().data.config as Record).providerPermissions as ProviderPermissionsConfig | undefined @@ -218,6 +242,10 @@ export const SettingsPermissionsV2: Component = () => {
+ + + + {(tier) => { const summary = () => tierSummary(tier) From a2d58299d17378177c121cbce57d895b4b90d6d4 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:16:41 -0400 Subject: [PATCH 5/7] Fix Developer Tools header misalignment with rebuild status text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .devtools-section-header lays the 'Developer Tools' title and the rebuild status indicator side by side with align-items: center. But .settings-v2-section-title carries a padding-bottom sized for the normal case (a title sitting above a settings list), which shifts its visual center upward relative to the status text beside it — the title reads as sitting slightly higher than 'Rebuilding...' / 'Rebuilt!'. Added the same padding-bottom: 0 override the .settings-v2-providers and .settings-v2-models section titles already apply for the identical reason. TDD: wrote a test reading amicode.css directly (following the existing pattern in wave-geometry.test.ts) asserting the override rule exists — confirmed RED (selector not found), applied the CSS fix, confirmed GREEN. Full ui package suite still passes (491/491). --- packages/ui/src/amicode/amicode.css | 8 +++++ .../amicode/devtools-header-alignment.test.ts | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/ui/src/amicode/devtools-header-alignment.test.ts diff --git a/packages/ui/src/amicode/amicode.css b/packages/ui/src/amicode/amicode.css index a6f6831cc..14e7a47b3 100644 --- a/packages/ui/src/amicode/amicode.css +++ b/packages/ui/src/amicode/amicode.css @@ -1472,6 +1472,14 @@ gap: 12px; } +/* .settings-v2-section-title carries a padding-bottom sized for sitting + above a settings list; inside this flex row it throws off centering + against the rebuild status text beside it. Same override as the + .settings-v2-providers / .settings-v2-models section titles. */ +.devtools-section-header .settings-v2-section-title { + padding-bottom: 0; +} + .devtools-rebuild-status { display: flex; align-items: center; diff --git a/packages/ui/src/amicode/devtools-header-alignment.test.ts b/packages/ui/src/amicode/devtools-header-alignment.test.ts new file mode 100644 index 000000000..b0899531d --- /dev/null +++ b/packages/ui/src/amicode/devtools-header-alignment.test.ts @@ -0,0 +1,36 @@ +import { readFileSync } from "node:fs" +import { describe, expect, test } from "bun:test" + +// ============================================================================ +// The devtools "Developer Tools" section header and its rebuild status +// indicator (#940) sit side by side in a flex row (.devtools-section-header, +// align-items: center). .settings-v2-section-title carries a padding-bottom +// meant for the normal case where a title sits above a settings list — but +// that padding shifts the title's visual center up relative to its flex +// sibling, misaligning it against the rebuild status text. The existing +// .settings-v2-providers and .settings-v2-models sections already override +// this padding to 0 for the same reason; this extends that same override to +// the devtools header. +// ============================================================================ + +function extractRule(css: string, selector: string): string { + const start = css.indexOf(selector) + if (start === -1) throw new Error(`selector "${selector}" not found`) + const braceOpen = css.indexOf("{", start) + const braceClose = css.indexOf("}", braceOpen) + return css.slice(start, braceClose + 1) +} + +describe("devtools section header title alignment (#940)", () => { + test("the amicode.css devtools header rule exists (baseline)", () => { + const css = readFileSync(new URL("./amicode.css", import.meta.url), "utf8") + const rule = extractRule(css, ".devtools-section-header {") + expect(rule).toContain("align-items: center") + }) + + test("the title's padding-bottom is zeroed out inside the devtools header, matching the providers/models precedent", () => { + const css = readFileSync(new URL("./amicode.css", import.meta.url), "utf8") + const rule = extractRule(css, ".devtools-section-header .settings-v2-section-title") + expect(rule).toMatch(/padding-bottom:\s*0/) + }) +}) From 6e28200fde301d20c087290aaac59e93899b128c Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 18:40:25 -0400 Subject: [PATCH 6/7] Remove dead building/buildError code (companion to amicode#941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paired amicode PR removes the auto-build that used to fire on every amicode-path commit — that build was the only thing that ever set DevToolsStatus.building/buildError, so with it gone these fields, the 'Building extension…' UI text, and its i18n key across all 18 locales are permanently unreachable. Removed: - building?/buildError? from the DevToolsStatus interface and from the status-received construction in the message handler - the building() computed and its block in developer-tools.tsx - the now-dead s.buildError check in amicodeError() (falls straight through to s.amicodeError, which is what the extension actually sends) - the orphaned settings.general.row.amicodePath.building i18n key from every locale (nothing in code referenced it once the UI text was gone) Removal-driven refactor, not new behavior — verified via the full suite before (17/17 settings-v2, 1103/1103 app) and after (same counts, 0 regressions), plus a clean typecheck. i18n parity suite still passes. --- .../components/settings-v2/developer-tools-controller.ts | 4 ---- .../app/src/components/settings-v2/developer-tools.tsx | 7 ------- packages/app/src/i18n/ar.ts | 1 - packages/app/src/i18n/br.ts | 1 - packages/app/src/i18n/bs.ts | 1 - packages/app/src/i18n/da.ts | 1 - packages/app/src/i18n/de.ts | 1 - packages/app/src/i18n/en.ts | 1 - packages/app/src/i18n/es.ts | 1 - packages/app/src/i18n/fr.ts | 1 - packages/app/src/i18n/ja.ts | 1 - packages/app/src/i18n/ko.ts | 1 - packages/app/src/i18n/no.ts | 1 - packages/app/src/i18n/pl.ts | 1 - packages/app/src/i18n/ru.ts | 1 - packages/app/src/i18n/th.ts | 1 - packages/app/src/i18n/tr.ts | 1 - packages/app/src/i18n/uk.ts | 1 - packages/app/src/i18n/zh.ts | 1 - packages/app/src/i18n/zht.ts | 1 - 20 files changed, 29 deletions(-) diff --git a/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app/src/components/settings-v2/developer-tools-controller.ts index 01a11bcd1..179cb25ae 100644 --- a/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -11,8 +11,6 @@ export interface DevToolsStatus { amicodeError?: string serverRestarted: boolean reloadNeeded: boolean - building?: boolean - buildError?: string } export type RebuildState = "idle" | "rebuilding" | "rebuilt" | "failed" @@ -80,8 +78,6 @@ export function createDeveloperToolsController() { amicodeError: d.amicodeError, serverRestarted: d.serverRestarted ?? false, reloadNeeded: d.reloadNeeded ?? false, - building: d.building ?? false, - buildError: d.buildError, }, }, ) diff --git a/packages/app/src/components/settings-v2/developer-tools.tsx b/packages/app/src/components/settings-v2/developer-tools.tsx index 00a2fcdcf..df62b0216 100644 --- a/packages/app/src/components/settings-v2/developer-tools.tsx +++ b/packages/app/src/components/settings-v2/developer-tools.tsx @@ -48,10 +48,8 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> const amicodeError = () => { const s = props.controller.status() if (!s || s.amicodeValid) return undefined - if (s.buildError) return s.buildError return s.amicodeError ?? language.t("settings.general.row.amicodePath.error.notFound") } - const building = () => props.controller.status()?.building ?? false const reloadNeeded = () => props.controller.status()?.reloadNeeded ?? false const isRebuilding = () => props.controller.rebuildState() === "rebuilding" const validating = () => props.controller.pending() @@ -169,11 +167,6 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> description={ <> {language.t("settings.general.row.amicodePath.description")} - - - {language.t("settings.general.row.amicodePath.building")} - - {amicodeError()} diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index bd3b4b387..852d87943 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -771,7 +771,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 6b60ebbc5..aec087e34 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -783,7 +783,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 90b22d6de..4f6d62e74 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -849,7 +849,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index fb57a8089..016545ed9 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -841,7 +841,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index b23ea6f68..d24991183 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -795,7 +795,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 7c27ad16e..c1fc23190 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1027,7 +1027,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 5cfe7ccc2..3af98784f 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -854,7 +854,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 75241ba50..dd9717c78 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -793,7 +793,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 38471937f..8d576b728 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -778,7 +778,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index ee7ef6ef9..536d73a77 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -620,7 +620,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 3efd8df5e..989408b4c 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -694,7 +694,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 786d93afe..5a5b4f3b8 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -783,7 +783,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index de193d70b..00288f892 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -850,7 +850,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 8b1562fe7..f0ec920a0 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -837,7 +837,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 5d4d3ebf1..9a3007238 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -856,7 +856,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 8ac32e61c..97a915d85 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -943,7 +943,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index a5bdc386c..f500159c0 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -830,7 +830,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 7c1aa6e48..b6141ea9e 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -827,7 +827,6 @@ export const dict = { "settings.general.row.amicodePath.placeholder": "~/harmoniqs/amicode", "settings.general.row.amicodePath.error.notFound": "Directory does not exist", "settings.general.row.amicodePath.error.buildFailed": "Extension build failed", - "settings.general.row.amicodePath.building": "Building extension…", "settings.general.row.amicodePath.reloadNeeded": "Reload to apply", "settings.general.row.devTools.rebuildLocally": "Rebuild Locally", "settings.general.row.devTools.rebuildRemotely": "Rebuild from Main", From c1709779a151fc408eba73db0882e790e6f85511 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 9 Sep 2026 19:54:38 -0400 Subject: [PATCH 7/7] Align Developer Tools rebuild status to title baseline The earlier padding reset removed one source of misalignment, but the header still used align-items:center. Centering the 12px status line box against the 15px title line box left the status baseline visibly lower than the heading. Aligning the flex row on its baseline is the correct typographic primitive: the text lines now share a baseline while the status dot remains centered inside its own inline status row. TDD: changed the existing CSS regression test to require baseline alignment, confirmed RED against align-items:center, changed the CSS, then confirmed GREEN. Full UI suite: 491/491 pass. --- packages/ui/src/amicode/amicode.css | 2 +- .../src/amicode/devtools-header-alignment.test.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/amicode/amicode.css b/packages/ui/src/amicode/amicode.css index 14e7a47b3..034dd36f2 100644 --- a/packages/ui/src/amicode/amicode.css +++ b/packages/ui/src/amicode/amicode.css @@ -1468,7 +1468,7 @@ /* ---- Developer Tools rebuild status + buttons ─────────────────────────── */ .devtools-section-header { display: flex; - align-items: center; + align-items: baseline; gap: 12px; } diff --git a/packages/ui/src/amicode/devtools-header-alignment.test.ts b/packages/ui/src/amicode/devtools-header-alignment.test.ts index b0899531d..4318b66d4 100644 --- a/packages/ui/src/amicode/devtools-header-alignment.test.ts +++ b/packages/ui/src/amicode/devtools-header-alignment.test.ts @@ -3,11 +3,12 @@ import { describe, expect, test } from "bun:test" // ============================================================================ // The devtools "Developer Tools" section header and its rebuild status -// indicator (#940) sit side by side in a flex row (.devtools-section-header, -// align-items: center). .settings-v2-section-title carries a padding-bottom -// meant for the normal case where a title sits above a settings list — but -// that padding shifts the title's visual center up relative to its flex -// sibling, misaligning it against the rebuild status text. The existing +// indicator (#940) sit side by side in a flex row. They use different font +// sizes, so align-items: center centers their line boxes but puts the status +// text's baseline slightly below the title's. Baseline alignment fixes that; +// .settings-v2-section-title also carries a padding-bottom meant for the +// normal case where a title sits above a settings list, so it must be reset +// in this row. The existing // .settings-v2-providers and .settings-v2-models sections already override // this padding to 0 for the same reason; this extends that same override to // the devtools header. @@ -22,10 +23,10 @@ function extractRule(css: string, selector: string): string { } describe("devtools section header title alignment (#940)", () => { - test("the amicode.css devtools header rule exists (baseline)", () => { + test("the header aligns the title and status text on their shared baseline", () => { const css = readFileSync(new URL("./amicode.css", import.meta.url), "utf8") const rule = extractRule(css, ".devtools-section-header {") - expect(rule).toContain("align-items: center") + expect(rule).toContain("align-items: baseline") }) test("the title's padding-bottom is zeroed out inside the devtools header, matching the providers/models precedent", () => {