From 1d90eaf602dc13e3c4b5475e42329576a8ca1def Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 4 Jul 2026 19:05:14 +0900 Subject: [PATCH] Relax over-specified UI contracts --- src/app-server/protocol/server-requests.ts | 41 +- src/domain/pending-requests/model.ts | 7 - .../pending-requests/view-model.ts | 22 - .../pending-request-block.dom.ts | 23 - .../message-stream/pending-request-block.tsx | 41 +- .../server-requests/mcp-elicitation.test.ts | 13 +- .../chat/app-server/inbound/handler.test.ts | 3 - .../message-stream/pending-requests.test.tsx | 14 +- tests/features/threads-view/shell.test.tsx | 24 +- tests/styles.test.ts | 417 ++++-------------- 10 files changed, 119 insertions(+), 486 deletions(-) diff --git a/src/app-server/protocol/server-requests.ts b/src/app-server/protocol/server-requests.ts index 17f9f953..72cc0fcc 100644 --- a/src/app-server/protocol/server-requests.ts +++ b/src/app-server/protocol/server-requests.ts @@ -53,16 +53,12 @@ type AppServerMcpElicitationPrimitiveSchema = title?: unknown; description?: unknown; default?: unknown; - minimum?: unknown; - maximum?: unknown; } | { type: "array"; title?: unknown; description?: unknown; default?: unknown; - minItems?: unknown; - maxItems?: unknown; items?: unknown; } | { @@ -70,12 +66,8 @@ type AppServerMcpElicitationPrimitiveSchema = title?: unknown; description?: unknown; default?: unknown; - format?: unknown; - minLength?: unknown; - maxLength?: unknown; oneOf?: unknown; enum?: unknown; - enumNames?: unknown; }; interface NormalizedMcpElicitationParams { @@ -542,6 +534,10 @@ function nullableString(value: unknown): string | null { return typeof value === "string" ? value : null; } +function numberOrNull(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + function asRecordOrNull(value: unknown): Record | null { return value && typeof value === "object" ? (value as Record) : null; } @@ -608,10 +604,6 @@ function normalizeMcpElicitationParams(params: McpElicitationParams): Normalized } } -function numberOrNull(value: unknown): number | null { - return typeof value === "number" && Number.isFinite(value) ? value : null; -} - function mcpElicitationFieldsFromRequestedSchema(schema: unknown): PendingMcpElicitationField[] { const record = asRecordOrNull(schema); const properties = asRecordOrNull(record?.["properties"]); @@ -651,17 +643,13 @@ function mcpElicitationFieldFromSchema( return { ...base, type: schema.type, - minimum: numberOrNull(schema.minimum), - maximum: numberOrNull(schema.maximum), - defaultValue: numberOrNull(schema.default), + defaultValue: typeof schema.default === "number" && Number.isFinite(schema.default) ? schema.default : null, }; case "array": return { ...base, type: "multi-select", options: multiSelectOptions(schema), - minItems: bigintToNumberOrNull(schema.minItems), - maxItems: bigintToNumberOrNull(schema.maxItems), defaultValue: stringArrayOrEmpty(schema.default), }; case "string": { @@ -677,9 +665,6 @@ function mcpElicitationFieldFromSchema( return { ...base, type: "string", - format: nullableString(schema.format), - minLength: numberOrNull(schema.minLength), - maxLength: numberOrNull(schema.maxLength), defaultValue: typeof schema.default === "string" ? schema.default : "", }; } @@ -694,7 +679,7 @@ function singleSelectOptions(schema: Extract 0) return options; } - return enumOptions(schema.enum, schema.enumNames); + return enumOptions(schema.enum); } function multiSelectOptions(schema: Extract): PendingMcpElicitationOption[] { @@ -707,7 +692,7 @@ function multiSelectOptions(schema: Extract 0) return options; } - return enumOptions(items?.["enum"], null); + return enumOptions(items?.["enum"]); } function stringArrayOrEmpty(value: unknown): readonly string[] { @@ -720,12 +705,11 @@ function stringArrayOrEmpty(value: unknown): readonly string[] { return strings; } -function enumOptions(values: unknown, labels: unknown): PendingMcpElicitationOption[] { +function enumOptions(values: unknown): PendingMcpElicitationOption[] { if (!Array.isArray(values)) return []; - const labelValues = Array.isArray(labels) ? labels : []; - return values.flatMap((value, index) => { + return values.flatMap((value) => { if (typeof value !== "string") return []; - return [{ value, label: nonEmptyString(labelValues[index]) ?? value }]; + return [{ value, label: value }]; }); } @@ -736,11 +720,6 @@ function selectOption(value: unknown): PendingMcpElicitationOption | null { return { value: optionValue, label: nonEmptyString(record?.["title"]) ?? optionValue }; } -function bigintToNumberOrNull(value: unknown): number | null { - if (typeof value === "bigint") return Number(value); - return typeof value === "number" && Number.isFinite(value) ? value : null; -} - function toJsonContent(content: Record | null): unknown { if (!content) return null; return Object.fromEntries( diff --git a/src/domain/pending-requests/model.ts b/src/domain/pending-requests/model.ts index 7fe96e3a..1b15f650 100644 --- a/src/domain/pending-requests/model.ts +++ b/src/domain/pending-requests/model.ts @@ -89,15 +89,10 @@ interface PendingMcpElicitationFieldBase { export type PendingMcpElicitationField = | (PendingMcpElicitationFieldBase & { type: "string"; - format: string | null; - minLength: number | null; - maxLength: number | null; defaultValue: string; }) | (PendingMcpElicitationFieldBase & { type: "number" | "integer"; - minimum: number | null; - maximum: number | null; defaultValue: number | null; }) | (PendingMcpElicitationFieldBase & { @@ -112,8 +107,6 @@ export type PendingMcpElicitationField = | (PendingMcpElicitationFieldBase & { type: "multi-select"; options: readonly PendingMcpElicitationOption[]; - minItems: number | null; - maxItems: number | null; defaultValue: readonly string[]; }); diff --git a/src/features/chat/presentation/pending-requests/view-model.ts b/src/features/chat/presentation/pending-requests/view-model.ts index f65bd81b..31153796 100644 --- a/src/features/chat/presentation/pending-requests/view-model.ts +++ b/src/features/chat/presentation/pending-requests/view-model.ts @@ -76,13 +76,6 @@ export interface PendingMcpElicitationFieldViewModel { defaultDraft: string; draftKey: string; options: readonly PendingMcpElicitationOptionViewModel[] | null; - format?: string | null; - minimum?: number | null; - maximum?: number | null; - minLength?: number | null; - maxLength?: number | null; - minItems?: number | null; - maxItems?: number | null; } export interface PendingMcpElicitationViewModel { @@ -205,22 +198,7 @@ function pendingMcpElicitationViewModel(elicitation: PendingMcpElicitation): Pen defaultDraft: mcpElicitationFieldDefaultDraft(field), draftKey: mcpElicitationDraftKey(elicitation.requestId, field.id), options: "options" in field ? field.options : null, - ...mcpElicitationFieldConstraints(field), })), url: null, }; } - -function mcpElicitationFieldConstraints(field: PendingMcpElicitationField): Partial { - switch (field.type) { - case "string": - return { format: field.format, minLength: field.minLength, maxLength: field.maxLength }; - case "number": - case "integer": - return { minimum: field.minimum, maximum: field.maximum }; - case "multi-select": - return { minItems: field.minItems, maxItems: field.maxItems }; - default: - return {}; - } -} diff --git a/src/features/chat/ui/message-stream/pending-request-block.dom.ts b/src/features/chat/ui/message-stream/pending-request-block.dom.ts index 5d5c846d..5ea530c7 100644 --- a/src/features/chat/ui/message-stream/pending-request-block.dom.ts +++ b/src/features/chat/ui/message-stream/pending-request-block.dom.ts @@ -1,8 +1,3 @@ -export interface McpElicitationValidityMessage { - fieldId: string; - message: string; -} - export function focusPendingRequestControl(container: HTMLElement | null): void { if (!container) return; for (const selector of [ @@ -22,21 +17,3 @@ export function focusPendingRequestControl(container: HTMLElement | null): void return; } } - -export function applyMcpElicitationFormValidity(form: HTMLFormElement | null, messages: readonly McpElicitationValidityMessage[]): boolean { - if (!form) return true; - clearMcpElicitationCustomValidity(form); - for (const { fieldId, message } of messages) { - const input = Array.from(form.querySelectorAll("input[data-mcp-elicitation-field]")).find( - (candidate) => candidate.dataset["mcpElicitationField"] === fieldId, - ); - input?.setCustomValidity(message); - } - return form.reportValidity(); -} - -function clearMcpElicitationCustomValidity(form: HTMLFormElement): void { - form.querySelectorAll(".codex-panel__mcp-elicitation-checkbox").forEach((input) => { - input.setCustomValidity(""); - }); -} diff --git a/src/features/chat/ui/message-stream/pending-request-block.tsx b/src/features/chat/ui/message-stream/pending-request-block.tsx index da407ece..cabe975b 100644 --- a/src/features/chat/ui/message-stream/pending-request-block.tsx +++ b/src/features/chat/ui/message-stream/pending-request-block.tsx @@ -11,11 +11,7 @@ import type { PendingUserInputViewModel, } from "../../presentation/pending-requests/view-model"; import type { PendingRequestBlockActions } from "./context"; -import { - applyMcpElicitationFormValidity, - focusPendingRequestControl, - type McpElicitationValidityMessage, -} from "./pending-request-block.dom"; +import { focusPendingRequestControl } from "./pending-request-block.dom"; import { createStatusMessageClassName } from "./status"; export function pendingRequestBlockNode( @@ -109,7 +105,7 @@ function McpElicitationCard({ }): UiNode { const formRef = useRef(null); const accept = () => { - if (elicitation.mode === "form" && !validateMcpElicitationForm(formRef.current, elicitation, mcpElicitationDrafts)) return; + if (elicitation.mode === "form" && formRef.current && !formRef.current.reportValidity()) return; actions.resolveMcpElicitation(elicitation.requestId, "accept"); }; return ( @@ -155,32 +151,6 @@ function McpElicitationCard({ ); } -function validateMcpElicitationForm( - form: HTMLFormElement | null, - elicitation: PendingMcpElicitationViewModel, - drafts: ReadonlyMap, -): boolean { - const messages: McpElicitationValidityMessage[] = []; - for (const field of elicitation.fields) { - if (field.type !== "multi-select") continue; - const selectedCount = selectedMcpElicitationValues(drafts.get(field.draftKey) ?? field.defaultDraft).size; - const message = mcpElicitationMultiSelectValidationMessage(field, selectedCount); - if (!message) continue; - messages.push({ fieldId: field.id, message }); - } - return applyMcpElicitationFormValidity(form, messages); -} - -function mcpElicitationMultiSelectValidationMessage(field: PendingMcpElicitationFieldViewModel, selectedCount: number): string | null { - if (typeof field.minItems === "number" && selectedCount < field.minItems) { - return `Select at least ${String(field.minItems)} option${field.minItems === 1 ? "" : "s"}.`; - } - if (typeof field.maxItems === "number" && selectedCount > field.maxItems) { - return `Select no more than ${String(field.maxItems)} option${field.maxItems === 1 ? "" : "s"}.`; - } - return null; -} - function ApprovalCard({ approval, approvalDetails, @@ -458,7 +428,6 @@ function McpElicitationFieldControl({ className="codex-panel__mcp-elicitation-checkbox" type="checkbox" value={option.value} - data-mcp-elicitation-field={field.id} checked={selected.has(option.value)} onChange={(event) => { const next = new Set(selected); @@ -484,8 +453,6 @@ function McpElicitationFieldControl({ className="codex-panel__mcp-elicitation-input" type="number" step={field.type === "integer" ? "1" : "any"} - min={field.minimum ?? undefined} - max={field.maximum ?? undefined} required={field.required} value={current} onInput={(event) => { @@ -498,9 +465,7 @@ function McpElicitationFieldControl({ { diff --git a/tests/app-server/protocol/server-requests/mcp-elicitation.test.ts b/tests/app-server/protocol/server-requests/mcp-elicitation.test.ts index e4ebc7d3..28c6b17f 100644 --- a/tests/app-server/protocol/server-requests/mcp-elicitation.test.ts +++ b/tests/app-server/protocol/server-requests/mcp-elicitation.test.ts @@ -42,8 +42,6 @@ describe("MCP elicitation request model", () => { expect.objectContaining({ id: "labels", type: "multi-select", - minItems: 1, - maxItems: 2, options: [ { value: "bug", label: "Bug" }, { value: "docs", label: "Docs" }, @@ -116,14 +114,13 @@ describe("MCP elicitation request model", () => { }); expect(input.params.fields.find((field) => field.id === "brokenSelect")).toMatchObject({ type: "string", - format: null, defaultValue: "", }); expect(input.params.fields.find((field) => field.id === "enumSelect")).toMatchObject({ type: "single-select", options: [ - { value: "low", label: "Low" }, - { value: "high", label: "High" }, + { value: "low", label: "low" }, + { value: "high", label: "high" }, ], }); expect(input.params.fields.find((field) => field.id === "labels")).toMatchObject({ @@ -165,8 +162,6 @@ function formRequest(): ServerRequest { labels: { type: "array", title: "Labels", - minItems: 1, - maxItems: 2, items: { anyOf: [ { const: "bug", title: "Bug" }, @@ -238,8 +233,8 @@ function malformedSchemaRequest(): ServerRequest { properties: { unsupported: { type: "object", title: "Unsupported" }, badDefault: { type: "boolean", title: "Bad default", default: "yes" }, - brokenSelect: { type: "string", title: "Broken select", oneOf: { const: "low", title: "Low" }, format: 1 }, - enumSelect: { type: "string", title: "Enum select", enum: ["low", 1, "high"], enumNames: ["Low", "One", "High"] }, + brokenSelect: { type: "string", title: "Broken select", oneOf: { const: "low", title: "Low" } }, + enumSelect: { type: "string", title: "Enum select", enum: ["low", 1, "high"] }, labels: { type: "array", title: "Labels", diff --git a/tests/features/chat/app-server/inbound/handler.test.ts b/tests/features/chat/app-server/inbound/handler.test.ts index b36a6ebd..dbd9fd60 100644 --- a/tests/features/chat/app-server/inbound/handler.test.ts +++ b/tests/features/chat/app-server/inbound/handler.test.ts @@ -1174,9 +1174,6 @@ describe("ChatInboundHandler", () => { description: null, type: "string", required: true, - format: null, - minLength: null, - maxLength: null, defaultValue: "", }, ], diff --git a/tests/features/chat/ui/message-stream/pending-requests.test.tsx b/tests/features/chat/ui/message-stream/pending-requests.test.tsx index b639e865..7630402f 100644 --- a/tests/features/chat/ui/message-stream/pending-requests.test.tsx +++ b/tests/features/chat/ui/message-stream/pending-requests.test.tsx @@ -547,7 +547,7 @@ describe("pending request renderer decisions", () => { unmountUiRootInAct(parent); }); - it("does not accept MCP multi-select fields below minItems", () => { + it("accepts MCP multi-select fields without panel-side cardinality validation", () => { const parent = document.createElement("div"); const resolveMcpElicitation = vi.fn(); @@ -568,15 +568,12 @@ describe("pending request renderer decisions", () => { }), ); - const options = expectPresent(parent.querySelector(".codex-panel__mcp-elicitation-options")); - const label = expectPresent(parent.querySelector(".codex-panel__mcp-elicitation-label")); - expect(options.tagName).toBe("FIELDSET"); - expect(options.getAttribute("aria-labelledby")).toBe(label.id); + expect(parent.querySelectorAll(".codex-panel__mcp-elicitation-checkbox")).toHaveLength(2); actEvent(() => { expectPresent(parent.querySelector(".codex-panel__pending-request-button.mod-cta")).click(); }); - expect(resolveMcpElicitation).not.toHaveBeenCalled(); + expect(resolveMcpElicitation).toHaveBeenCalledWith(53, "accept"); unmountUiRootInAct(parent); }); @@ -722,9 +719,6 @@ function pendingMcpElicitation({ description: "Issue title", type: "string", required: true, - format: null, - minLength: null, - maxLength: null, defaultValue, }, ], @@ -779,8 +773,6 @@ function pendingMcpMultiSelectElicitation(): PendingRequestBlockSnapshot["pendin { value: "bug", label: "Bug" }, { value: "docs", label: "Docs" }, ], - minItems: 1, - maxItems: 2, defaultValue: [], }, ], diff --git a/tests/features/threads-view/shell.test.tsx b/tests/features/threads-view/shell.test.tsx index 543e7166..9f14e2d3 100644 --- a/tests/features/threads-view/shell.test.tsx +++ b/tests/features/threads-view/shell.test.tsx @@ -111,33 +111,21 @@ describe("threads view renderer decisions", () => { renderThreadsViewShell(parent, { status: "2 threads", loading: false, rows }, actions); - expect(parent.querySelector(".codex-panel-threads__badge")).toBeNull(); const main = expectPresent(parent.querySelector(".codex-panel-threads__row--pending")); const row = expectPresent(main.closest(".codex-panel-threads__row")); expect(row.classList.contains("codex-panel-ui__nav-row")).toBe(true); - expect(main.classList.contains("codex-panel-ui__nav-item")).toBe(true); - expect(row.classList.contains("codex-panel-threads__row--selected")).toBe(true); expect(row.classList.contains("is-selected")).toBe(true); - expect(parent.querySelector(".codex-panel-threads__list")?.getAttribute("role")).toBeNull(); - expect(main.getAttribute("role")).toBeNull(); - expect(main.getAttribute("tabindex")).toBeNull(); - expect(main.getAttribute("aria-current")).toBeNull(); - expect(row.getAttribute("title")).toBeNull(); + expect(row.classList.contains("codex-panel-threads__row--selected")).toBe(true); + expect(main.classList.contains("codex-panel-ui__nav-item")).toBe(true); const toolbarButtons = [...parent.querySelectorAll(".codex-panel-threads__toolbar-button")]; expect(toolbarButtons.map((button) => button.getAttribute("aria-label"))).toEqual(["Open new panel", "Refresh threads"]); - expect(toolbarButtons.map((button) => button.tagName)).toEqual(["DIV", "DIV"]); - expect(toolbarButtons.map((button) => button.getAttribute("role"))).toEqual([null, null]); const refresh = expectPresent(parent.querySelector('[aria-label="Refresh threads"]')); - expect(refresh.classList.contains("codex-panel-threads__toolbar-button")).toBe(true); expect(refresh.classList.contains("nav-action-button")).toBe(true); expect(refresh.classList.contains("codex-panel-ui__toolbar-action")).toBe(true); expect(refresh.classList.contains("codex-panel-ui__nav-row-action")).toBe(false); refresh.click(); expect(actions.refresh).toHaveBeenCalledOnce(); const openNewPanel = expectPresent(parent.querySelector('[aria-label="Open new panel"]')); - expect(openNewPanel.classList.contains("codex-panel-threads__toolbar-button")).toBe(true); - expect(openNewPanel.classList.contains("codex-panel-threads__row-button")).toBe(false); - expect(openNewPanel.dataset["icon"]).toBe("message-square-plus"); openNewPanel.click(); expect(actions.openNewPanel).toHaveBeenCalledOnce(); const rename = expectPresent(parent.querySelector('[aria-label="Rename thread"]')); @@ -145,8 +133,6 @@ describe("threads view renderer decisions", () => { expect(rename.classList.contains("codex-panel-ui__nav-row-action")).toBe(true); main.click(); expect(actions.openThread).toHaveBeenCalledWith("open"); - expect(parent.querySelector('[aria-label="Focus open panel"]')).toBeNull(); - expect(parent.querySelector('[aria-label="Open in new panel"]')).toBeNull(); }); it("renders threads view archive confirmation with the default action on the right", () => { @@ -222,10 +208,6 @@ describe("threads view renderer decisions", () => { renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, actions); expect(parent.querySelector(".codex-panel-threads__rename-form")).toBeTruthy(); - const actionsGroup = expectPresent(parent.querySelector(".codex-panel-threads__rename-actions")); - expect(actionsGroup.querySelectorAll(".codex-panel-threads__row-button")).toHaveLength(1); - expect(parent.querySelector('[aria-label="Save thread name"]')).toBeNull(); - expect(parent.querySelector('[aria-label="Cancel rename"]')).toBeNull(); parent.querySelector('[aria-label="Auto-name thread"]')?.click(); expect(actions.autoNameThread).toHaveBeenCalledWith("thread"); @@ -241,8 +223,6 @@ describe("threads view renderer decisions", () => { renderThreadsViewShell(parent, { status: "1 thread", loading: false, rows: [row] }, threadsViewActions()); expect(parent.querySelector(".codex-panel-threads__rename-input")?.disabled).toBe(false); - expect(parent.querySelector('[aria-label="Save thread name"]')).toBeNull(); expect(parent.querySelector('[aria-label="Auto-name thread"]')?.disabled).toBe(true); - expect(parent.querySelector('[aria-label="Cancel rename"]')).toBeNull(); }); }); diff --git a/tests/styles.test.ts b/tests/styles.test.ts index 03242ddc..883b758c 100644 --- a/tests/styles.test.ts +++ b/tests/styles.test.ts @@ -7,7 +7,17 @@ const sourceDir = path.join("src", "styles"); const sourceFiles = JSON.parse(readFileSync(path.join(sourceDir, "order.json"), "utf8")) as string[]; const styles = `${sourceFiles.map((file) => readFileSync(path.join(sourceDir, file), "utf8").trimEnd()).join("\n\n")}\n`; -describe("panel CSS token scope", () => { +function ruleBody(selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`(?:^|\\n)${escapedSelector} \\{(?[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? ""; +} + +function standaloneRuleBody(selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`(?:^|\\n\\n)${escapedSelector} \\{(?[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? ""; +} + +describe("panel CSS boundaries", () => { it("defines design tokens on every standalone UI root", () => { const tokenScope = /^(?(?:\.[^{]+,\n)*\.[^{]+) \{/m.exec(styles)?.groups?.["selectors"] ?? ""; @@ -17,34 +27,25 @@ describe("panel CSS token scope", () => { expect(tokenScope).toContain(".codex-panel-threads"); expect(tokenScope).toContain(".codex-panel-selection-rewrite"); }); + + it("keeps selectors shallow enough for Obsidian theme compatibility", () => { + expect(styles).not.toContain(":has("); + expect(styles).not.toMatch(/:where\([^)]*[.#[]/); + expect(styles).not.toMatch(/(^|[\s,{])#[\w-]+/); + }); }); -describe("chat toolbar CSS", () => { - it("lets the message region size come from its grid row", () => { - const messages = /\.codex-panel__messages \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; +describe("panel CSS layout invariants", () => { + it("lets the message stream scroll inside the shell grid", () => { + const messages = ruleBody(".codex-panel__messages"); expect(messages).toContain("overflow-y: auto"); - expect(messages).not.toContain("overflow-anchor: none"); expect(messages).not.toMatch(/^\s+height:/m); }); - it("keeps toolbar panels visually separated from the following body content", () => { - const toolbarPanel = /\.codex-panel__toolbar-panel \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(toolbarPanel).toContain("margin-bottom: var(--codex-panel-panel-gap)"); - }); - - it("aligns goal banner spacing with the message rhythm", () => { - const goalSlot = /\.codex-panel__region--goal:not\(:empty\) \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const goal = /\.codex-panel__goal \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(goalSlot).toContain("padding-bottom: var(--codex-panel-edge-padding-x)"); - expect(goal).toContain("margin: var(--codex-panel-edge-padding-x) var(--codex-panel-edge-padding-x) 0"); - }); - - it("lets icon-only toolbar actions use Obsidian nav action geometry", () => { - const toolbarAction = /\.codex-panel-ui__toolbar-action \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const toolbar = /\.codex-panel__toolbar \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; + it("keeps icon-only toolbar actions on Obsidian toolbar geometry", () => { + const toolbar = ruleBody(".codex-panel__toolbar"); + const toolbarAction = ruleBody(".codex-panel-ui__toolbar-action"); expect(toolbarAction).toContain("--icon-size: var(--codex-panel-control-icon-size)"); expect(toolbarAction).toContain("--icon-stroke: var(--icon-m-stroke-width, 1.75px)"); @@ -53,335 +54,111 @@ describe("chat toolbar CSS", () => { expect(toolbarAction).not.toContain("padding:"); expect(toolbarAction).not.toContain("border-radius:"); expect(toolbar).not.toContain("padding:"); - expect(styles).not.toContain(".codex-panel__runtime-area"); - expect(styles).not.toContain(".codex-panel__runtime-strip"); }); - it("keeps mouse-focus reset less specific than active toolbar actions", () => { - const toolbarActionMouseFocus = - /\.codex-panel-ui__toolbar-action:where\(:focus:not\(:hover\):not\(:focus-visible\)\) \{(?[^}]+)\}/.exec(styles)?.groups?.[ - "body" - ] ?? ""; + it("keeps shared nav rows aligned with Obsidian nav item geometry", () => { + const navItem = ruleBody(".codex-panel-ui__nav-item"); + const navRow = ruleBody(".codex-panel-ui__nav-row"); - expect(toolbarActionMouseFocus).toContain("background: transparent"); - expect(toolbarActionMouseFocus).toContain("color: var(--icon-color)"); + expect(navItem).toContain("min-height: var(--nav-item-size"); + expect(navItem).toContain("padding: var(--nav-item-padding"); + expect(navItem).toContain("border-radius: var(--nav-item-radius"); + expect(navRow).toContain("padding-inline-end:"); }); - it("does not color toolbar panel items as hovered for retained click focus", () => { - const hoverSelectors = [ - ...styles.matchAll( - /\.codex-panel__toolbar-panel-item:where\((?[^)]+)\):not\(:disabled\):not\(\.is-disabled\)\s+\.codex-panel__toolbar-panel-(?:label|meta) \{/g, - ), - ].map((match) => match.groups?.["states"] ?? ""); + it("keeps selected nav items stable when hovered or focused", () => { + const selectedNavItem = ruleBody(".codex-panel-ui__nav-item.is-selected:where(:hover, :focus, :focus-visible, :active, :focus-within)"); + const selectedNavRow = ruleBody( + [ + ".codex-panel-ui__nav-row.is-selected,", + ".codex-panel-ui__nav-row.is-selected:hover,", + ".codex-panel-ui__nav-row.is-selected:focus-within", + ].join("\n"), + ); - expect(hoverSelectors).toEqual([":hover, :focus-visible, :active", ":hover, :focus-visible, :active"]); - }); - - it("keeps toolbar panel rows aligned to Obsidian nav items", () => { - const toolbarPanelNavItem = - /\.codex-panel-ui__nav-item\.codex-panel__toolbar-panel-item \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(toolbarPanelNavItem).toContain("justify-content: flex-start"); - expect(toolbarPanelNavItem).toContain("height: auto"); - expect(toolbarPanelNavItem).toContain("background: transparent"); - expect(toolbarPanelNavItem).toContain("box-shadow: none"); - }); - - it("uses the shared active state for toolbar actions", () => { - const toolbarActionActive = - /\.codex-panel-ui__toolbar-action\.is-active,\n\.codex-panel-ui__toolbar-action\.is-active:hover,\n\.codex-panel-ui__toolbar-action\.is-active:focus-visible,\n\.codex-panel-ui__toolbar-action\.is-active:active \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - - expect(toolbarActionActive).toContain("background: var(--background-modifier-active-hover)"); - expect(toolbarActionActive).toContain("color: var(--icon-color-active)"); - expect(styles).not.toContain(".codex-panel__runtime-model.is-active"); - expect(styles).not.toContain(".codex-panel__runtime-model-value"); - }); - - it("keeps class selectors out of zero-specificity :where selectors", () => { - expect(styles).not.toMatch(/:where\([^)]*[.#[]/); - }); - - it("keeps selected toolbar rows stable while hovered", () => { - const navItem = /\.codex-panel-ui__nav-item \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const selectedNavItem = - /\.codex-panel-ui__nav-item\.is-selected:where\(:hover, :focus, :focus-visible, :active, :focus-within\) \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - const selectedNavRow = - /\.codex-panel-ui__nav-row\.is-selected,\n\.codex-panel-ui__nav-row\.is-selected:hover,\n\.codex-panel-ui__nav-row\.is-selected:focus-within \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - - expect(navItem).toContain("min-height: var(--nav-item-size, var(--codex-panel-size-nav-item))"); - expect(navItem).toContain("padding: var(--nav-item-padding, var(--size-4-1) var(--size-4-2))"); - expect(navItem).toContain("border-radius: var(--nav-item-radius, var(--radius-s))"); - expect(navItem).toContain("color: var(--nav-item-color, var(--text-muted))"); - expect(styles).toContain("background: var(--codex-panel-nav-item-background-hover, var(--background-modifier-hover))"); expect(selectedNavItem).toContain("background: var(--nav-item-background-active, var(--background-modifier-active))"); expect(selectedNavRow).toContain("background: var(--nav-item-background-active, var(--background-modifier-active))"); expect(selectedNavItem).not.toContain("nav-item-background-active-hover"); expect(selectedNavRow).not.toContain("nav-item-background-active-hover"); }); - it("keeps chat thread row actions inset from the row edge", () => { - const threadList = /\.codex-panel__threads \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const navRow = /\.codex-panel-ui__nav-row \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const statusPanelItems = /\.codex-panel__status-panel-items \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(threadList).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))"); - expect(statusPanelItems).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))"); - expect(navRow).toContain("--codex-panel-nav-item-background-hover: transparent"); - expect(navRow).toContain("padding-inline-end: var(--size-4-2)"); - }); - - it("keeps the composer context status text fixed-width", () => { - const contextMeter = /\.codex-panel__composer-meta-context \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const contextDots = /\.codex-panel__composer-meta-context-dots \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const placeholderDot = - /\.codex-panel__composer-meta-context-dot\.is-placeholder \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const contextPercent = /\.codex-panel__composer-meta-context-percent \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(contextMeter).toContain("font-variant-numeric: tabular-nums"); - expect(contextMeter).toContain("white-space: nowrap"); - expect(contextDots).toContain("display: inline-flex"); - expect(placeholderDot).toContain("color: var(--text-faint)"); - expect(contextPercent).toContain("white-space: pre"); - expect(styles).not.toContain(".codex-panel__composer-meta-context::before"); - expect(styles).not.toContain("conic-gradient"); - }); - - it("aligns composer status text with the input text inset", () => { - const composerFrame = /\.codex-panel__composer-frame \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const composerMeta = /\.codex-panel__composer-meta \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const composerMetaStatus = /\.codex-panel__composer-meta-status \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const composerMetaFatal = /\.codex-panel__composer-meta-fatal \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(composerFrame).toContain("background: var(--background-modifier-form-field)"); - expect(composerMeta).toContain("padding: 0 var(--size-4-2) var(--size-4-1)"); - expect(composerMetaStatus).toContain("position: relative"); - expect(composerMetaStatus).not.toContain("padding-inline-start"); - expect(composerMetaFatal).not.toContain("padding-inline-start"); - }); - - it("lets the shell provide Obsidian status bar composer clearance", () => { - const tokenScope = /^(?(?:\.[^{]+,\n)*\.[^{]+) \{(?[^}]+)\}/m.exec(styles)?.groups?.["body"] ?? ""; - const clearanceLine = tokenScope - .split("\n") - .find((line) => line.trim().startsWith("--codex-panel-status-bar-clearance:")) - ?.trim(); - - expect(clearanceLine).toBe("--codex-panel-status-bar-clearance: 0px;"); - }); - - it("keeps composer status accessible summary visually hidden", () => { - const summary = /\.codex-panel__composer-meta-summary \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const visual = /\.codex-panel__composer-meta-status-visual \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(summary).toContain("position: absolute"); - expect(summary).toContain("width: var(--border-width)"); - expect(summary).toContain("height: var(--border-width)"); - expect(summary).toContain("pointer-events: none"); - expect(summary).toContain("opacity: 0"); - expect(summary).not.toContain("clip-path:"); - expect(summary).not.toContain("clip:"); - expect(visual).toContain("display: flex"); - expect(visual).toContain("flex: 0 0 auto"); - expect(visual).toContain("gap: var(--size-4-2)"); - }); - - it("keeps composer runtime mode icons compact and state-colored", () => { - const modes = /\.codex-panel__composer-meta-modes \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const field = /\.codex-panel__composer-meta-field \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const labels = - /\.codex-panel__composer-meta-value\.codex-panel__composer-meta-value \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const icon = /\.codex-panel__composer-meta-icon \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const activeIcon = /\.codex-panel__composer-meta-icon\.is-active \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const iconSvg = /\.codex-panel__composer-meta-icon svg \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(modes).toContain("gap: var(--codex-panel-control-gap)"); - expect(field).toContain("display: inline-flex"); - expect(field).toContain("flex: 0 0 auto"); - expect(field).toContain("gap: var(--size-4-2)"); - expect(labels).toContain("white-space: nowrap"); - expect(labels).toContain("display: inline-flex"); - expect(labels).not.toContain("text-overflow"); - expect(icon).toContain("width: var(--codex-panel-size-icon-xs)"); - expect(icon).toContain("height: var(--codex-panel-size-icon-xs)"); - expect(activeIcon).toContain("color: var(--icon-color-active)"); - expect(iconSvg).toContain("width: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)"); - expect(iconSvg).toContain("height: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)"); - expect(styles).toContain( - ".codex-panel__composer-meta-status.is-effort-hidden .codex-panel__composer-meta-field--effort {\n display: none;", + it("keeps nav inline inputs as native inline editors", () => { + const navInlineInput = ruleBody(".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input"); + const navInlineInputFocus = ruleBody( + [ + ".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:focus,", + ".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:focus-visible,", + ".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:hover,", + ".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:active", + ].join("\n"), ); - expect(styles).toContain( - ".codex-panel__composer-meta-status.is-model-hidden .codex-panel__composer-meta-field--model {\n display: none;", - ); - }); - - it("keeps composer status triggers visually unstyled", () => { - const popover = /\.codex-panel__composer-meta-popover \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const option = /\.codex-panel__composer-meta-option \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(styles).not.toContain(".codex-panel__composer-meta-trigger {"); - expect(styles).not.toContain(".codex-panel__composer-meta-trigger:hover"); - expect(styles).not.toContain("button.codex-panel__composer-meta-trigger"); - expect(popover).toContain("position: absolute"); - expect(popover).toContain("bottom: calc(100% + var(--codex-panel-panel-gap))"); - expect(popover).toContain("width: max-content"); - expect(popover).toContain("max-width: calc(100% - var(--codex-panel-composer-meta-popover-left))"); - expect(styles).not.toContain(".codex-panel__composer-meta-option.is-selected"); - expect(option).not.toContain("padding-left"); - expect(option).not.toContain("grid-template-columns"); - }); - - it("keeps nav inline input reset in the shared primitive", () => { - const navInlineInput = - /\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const navInlineInputFocus = - /\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input:focus,\n\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input:focus-visible,\n\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input:hover,\n\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input:active \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - const chatRenameInput = /\.codex-panel__thread-rename-input \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const threadsRenameInput = /\.codex-panel-threads__rename-input \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; expect(navInlineInput).toContain("appearance: none"); - expect(navInlineInput).toContain("color: var(--nav-item-color-active, var(--text-normal))"); + expect(navInlineInput).toContain("border: 0"); + expect(navInlineInput).toContain("background: transparent"); expect(navInlineInputFocus).toContain("background: transparent"); - expect(chatRenameInput).toContain("width: 100%"); - expect(chatRenameInput).not.toContain("appearance: none"); - expect(threadsRenameInput).toContain("flex: 1 1 auto"); - expect(threadsRenameInput).not.toContain("appearance: none"); }); - it("lets the usage limit meter absorb panel width changes", () => { - const limitList = /\.codex-panel__limit-panel-list \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const limitRow = /\.codex-panel__limit-panel-row \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const limitMeterCell = /\.codex-panel__limit-panel-meter-cell \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const limitMeter = /\.codex-panel__limit-panel-meter \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const limitValue = /\.codex-panel__limit-panel-value \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; + it("keeps compact status rows from wrapping or stealing pointer interaction", () => { + const contextMeter = ruleBody(".codex-panel__composer-meta-context"); + const summary = ruleBody(".codex-panel__composer-meta-summary"); + + expect(contextMeter).toContain("white-space: nowrap"); + expect(contextMeter).toContain("font-variant-numeric: tabular-nums"); + expect(summary).toContain("position: absolute"); + expect(summary).toContain("pointer-events: none"); + }); + + it("lets usage limit meters absorb available width", () => { + const limitMeterCell = ruleBody(".codex-panel__limit-panel-meter-cell"); + const limitValue = ruleBody(".codex-panel__limit-panel-value"); + const limitReset = standaloneRuleBody(".codex-panel__limit-panel-reset"); - expect(limitList).toContain("display: table"); - expect(limitList).toContain("width: 100%"); - expect(limitList).toContain("border-spacing: 0 var(--codex-panel-panel-gap)"); - expect(limitRow).toContain("display: table-row"); - expect(limitRow).not.toContain("display: contents"); - expect(limitMeterCell).toContain("display: table-cell"); expect(limitMeterCell).toContain("width: 100%"); expect(limitMeterCell).toContain("vertical-align: middle"); - expect(limitMeter).toContain("margin: 0"); expect(limitValue).toContain("font-variant-numeric: tabular-nums"); - expect(limitValue).not.toContain("text-align: right"); - }); -}); - -describe("chat message CSS", () => { - it("uses hover color instead of a pointer cursor for the inline turn diff action", () => { - const openTurnDiff = /\.codex-panel__open-turn-diff \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const openTurnDiffSvg = /\.codex-panel__open-turn-diff svg \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const openTurnDiffHover = /\.codex-panel__open-turn-diff:hover \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(openTurnDiff).toContain("cursor: default"); - expect(openTurnDiffSvg).toContain("width: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)"); - expect(openTurnDiffSvg).toContain("height: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)"); - expect(openTurnDiffHover).toContain("color: var(--nav-item-color-hover, var(--text-normal))"); - }); -}); - -describe("selection rewrite CSS", () => { - it("uses a composer-style instruction frame without the old action stack", () => { - const frame = /\.codex-panel-selection-rewrite__composer-frame \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const instruction = /\.codex-panel-selection-rewrite__instruction \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const iconButton = /\.codex-panel-selection-rewrite__icon-button \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(frame).toContain("grid-template-columns: minmax(0, 1fr) auto"); - expect(frame).toContain("background: var(--background-modifier-form-field)"); - expect(frame).toContain("border: var(--input-border-width, var(--border-width, 1px)) solid var(--background-modifier-border)"); - expect(instruction).toContain("min-height: var(--codex-panel-size-user-input-text-min-height)"); - expect(instruction).toContain("background: transparent"); - expect(instruction).toContain("border: 0"); - expect(iconButton).toContain("min-width: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-panel-gap) * 2)"); - expect(iconButton).toContain("margin: var(--codex-panel-panel-gap)"); - expect(iconButton).toContain("padding: var(--codex-panel-panel-gap)"); - expect(styles).not.toContain(".codex-panel-selection-rewrite__controls"); + expect(limitReset).toContain("white-space: nowrap"); }); - it("keeps the apply action inside the selection rewrite result frame", () => { - const result = /\.codex-panel-selection-rewrite__result \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const actions = /\.codex-panel-selection-rewrite__result-actions \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const diffBody = /\.codex-panel-selection-rewrite__diff-body \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; + it("keeps threads view row titles ellipsized while actions stay collapsed until interaction", () => { + const rowTitleBase = ruleBody(".codex-panel-threads__row-title"); + const rowTitleDisplay = standaloneRuleBody(".codex-panel-threads__row-title"); + const actions = ruleBody(".codex-panel-threads__actions"); + const actionReveal = ruleBody( + [ + ".codex-panel-threads__row:hover .codex-panel-threads__actions,", + ".codex-panel-threads__row:focus-within .codex-panel-threads__actions", + ].join("\n"), + ); - expect(result).toContain("grid-template-columns: minmax(0, 1fr) auto"); - expect(result).toContain("border: var(--codex-panel-border)"); - expect(result).toContain("background: var(--background-modifier-form-field)"); - expect(actions).toContain("padding: 0"); - expect(diffBody).toContain("margin: 0"); - expect(diffBody).toContain("border-radius: 0"); - expect(diffBody).toContain("background: transparent"); + expect(rowTitleDisplay).toContain("display: block"); + expect(rowTitleBase).toContain("white-space: nowrap"); + expect(rowTitleBase).toContain("text-overflow: ellipsis"); + expect(actions).toContain("width: 0"); + expect(actions).toContain("pointer-events: none"); + expect(actions).toContain("opacity: 0"); + expect(actionReveal).toContain("width: auto"); + expect(actionReveal).toContain("pointer-events: auto"); + expect(actionReveal).toContain("opacity: 1"); }); - it("aligns generation status text with the instruction input text inset", () => { - const status = /\.codex-panel-selection-rewrite__status \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; + it("keeps threads view inline rename aligned to nav row height", () => { + const renameForm = ruleBody(".codex-panel-threads__rename-form"); + const renameField = ruleBody(".codex-panel-threads__rename-field"); + const renameInput = ruleBody(".codex-panel-threads__rename-input"); - expect(status).toContain("padding-inline-start: calc(var(--size-4-1) / 2)"); - }); -}); - -describe("threads view CSS", () => { - it("keeps long row titles clear of trailing actions", () => { - const list = /\.codex-panel-threads__list \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const rowMain = /\.codex-panel-threads__row-main \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const renameForm = /\.codex-panel-threads__rename-form \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const renameField = /\.codex-panel-threads__rename-field \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const renameInput = /\.codex-panel-threads__rename-input \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const title = /(?:^|\n\n)\.codex-panel-threads__row-title \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(list).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))"); - expect(list).toContain("padding: var(--size-4-1) var(--size-4-3)"); - expect(rowMain).toContain("box-sizing: border-box"); - expect(rowMain).not.toContain("padding-right:"); - expect(renameForm).toContain("min-height: var(--nav-item-size, var(--codex-panel-size-nav-item))"); - expect(renameField).toContain("min-height: var(--nav-item-size, var(--codex-panel-size-nav-item))"); + expect(renameForm).toContain("min-height: var(--nav-item-size"); + expect(renameField).toContain("min-height: var(--nav-item-size"); expect(renameInput).toContain("line-height: var(--line-height-tight)"); - expect(title).toContain("display: block"); }); - it("keeps toolbar action hover color separate from row action hover color", () => { - const toolbarHover = - /\.codex-panel-ui__toolbar-action:hover:not\(\.is-disabled\),\n\.codex-panel-ui__toolbar-action:focus-visible:not\(\.is-disabled\) \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - const toolbarMouseFocus = - /\.codex-panel-ui__toolbar-action:where\(:focus:not\(:hover\):not\(:focus-visible\)\) \{(?[^}]+)\}/.exec(styles)?.groups?.[ - "body" - ] ?? ""; - const rowHover = - /\.codex-panel-ui__nav-row-action:hover,\n\.codex-panel-ui__nav-row-action:focus,\n\.codex-panel-ui__nav-row-action:focus-visible,\n\.codex-panel-ui__nav-row-action:active \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; + it("keeps selection rewrite controls framed as a compact edit-and-review surface", () => { + const composerFrame = ruleBody(".codex-panel-selection-rewrite__composer-frame"); + const result = ruleBody(".codex-panel-selection-rewrite__result"); - expect(styles).not.toContain(".codex-panel-threads__toolbar-actions {"); - expect(toolbarHover).toContain("background: var(--background-modifier-hover)"); - expect(toolbarHover).toContain("color: var(--icon-color)"); - expect(toolbarHover).not.toContain("var(--icon-color-active)"); - expect(toolbarMouseFocus).toContain("background: transparent"); - expect(toolbarMouseFocus).toContain("color: var(--icon-color)"); - expect(rowHover).toContain("color: var(--icon-color-active)"); - }); - - it("does not rely on :has() to avoid hover-highlighting action rows", () => { - const rowHover = - /\.codex-panel-ui__nav-row:hover,\n\.codex-panel-ui__nav-row:focus-within \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - const titleHover = - /\.codex-panel-threads__row-main:hover \.codex-panel-threads__row-title,\n\.codex-panel-threads__row-main:focus \.codex-panel-threads__row-title \{(?[^}]+)\}/.exec( - styles, - )?.groups?.["body"] ?? ""; - const title = /(?:^|\n\n)\.codex-panel-threads__row-title \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? ""; - - expect(styles).not.toContain(":has("); - expect(rowHover).not.toContain("--codex-panel-threads-row-title-color"); - expect(titleHover).toContain("color: var(--nav-item-color-hover, var(--text-normal))"); - expect(title).toContain("color: var(--codex-panel-threads-row-title-color)"); + expect(composerFrame).toContain("grid-template-columns:"); + expect(composerFrame).toContain("background: var(--background-modifier-form-field)"); + expect(result).toContain("grid-template-columns:"); + expect(result).toContain("border: var(--codex-panel-border)"); }); });