Relax over-specified UI contracts

This commit is contained in:
murashit 2026-07-04 19:05:14 +09:00
parent ba5e292487
commit 1d90eaf602
10 changed files with 119 additions and 486 deletions

View file

@ -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<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : 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<AppServerMcpElicitationPrimitiveSch
});
if (options.length > 0) return options;
}
return enumOptions(schema.enum, schema.enumNames);
return enumOptions(schema.enum);
}
function multiSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSchema, { type: "array" }>): PendingMcpElicitationOption[] {
@ -707,7 +692,7 @@ function multiSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSche
});
if (options.length > 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<string, McpElicitationContentValue> | null): unknown {
if (!content) return null;
return Object.fromEntries(

View file

@ -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[];
});

View file

@ -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<PendingMcpElicitationFieldViewModel> {
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 {};
}
}

View file

@ -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<HTMLInputElement>("input[data-mcp-elicitation-field]")).find(
(candidate) => candidate.dataset["mcpElicitationField"] === fieldId,
);
input?.setCustomValidity(message);
}
return form.reportValidity();
}
function clearMcpElicitationCustomValidity(form: HTMLFormElement): void {
form.querySelectorAll<HTMLInputElement>(".codex-panel__mcp-elicitation-checkbox").forEach((input) => {
input.setCustomValidity("");
});
}

View file

@ -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<HTMLFormElement | null>(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<string, string>,
): 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({
<input
id={controlId}
className="codex-panel__mcp-elicitation-input"
type={field.format === "email" ? "email" : field.format === "uri" ? "url" : field.format === "date" ? "date" : "text"}
minLength={field.minLength ?? undefined}
maxLength={field.maxLength ?? undefined}
type="text"
required={field.required}
value={current}
onInput={(event) => {

View file

@ -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",

View file

@ -1174,9 +1174,6 @@ describe("ChatInboundHandler", () => {
description: null,
type: "string",
required: true,
format: null,
minLength: null,
maxLength: null,
defaultValue: "",
},
],

View file

@ -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<HTMLElement>(".codex-panel__mcp-elicitation-options"));
const label = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__mcp-elicitation-label"));
expect(options.tagName).toBe("FIELDSET");
expect(options.getAttribute("aria-labelledby")).toBe(label.id);
expect(parent.querySelectorAll<HTMLInputElement>(".codex-panel__mcp-elicitation-checkbox")).toHaveLength(2);
actEvent(() => {
expectPresent(parent.querySelector<HTMLButtonElement>(".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: [],
},
],

View file

@ -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<HTMLElement>(".codex-panel-threads__row--pending"));
const row = expectPresent(main.closest<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>('[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<HTMLElement>('[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<HTMLButtonElement>('[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<HTMLButtonElement>('[aria-label="Focus open panel"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[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<HTMLElement>(".codex-panel-threads__rename-form")).toBeTruthy();
const actionsGroup = expectPresent(parent.querySelector<HTMLElement>(".codex-panel-threads__rename-actions"));
expect(actionsGroup.querySelectorAll(".codex-panel-threads__row-button")).toHaveLength(1);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
parent.querySelector<HTMLButtonElement>('[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<HTMLInputElement>(".codex-panel-threads__rename-input")?.disabled).toBe(false);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.disabled).toBe(true);
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
});
});

View file

@ -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} \\{(?<body>[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? "";
}
function standaloneRuleBody(selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(?:^|\\n\\n)${escapedSelector} \\{(?<body>[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? "";
}
describe("panel CSS boundaries", () => {
it("defines design tokens on every standalone UI root", () => {
const tokenScope = /^(?<selectors>(?:\.[^{]+,\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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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\) \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const goal = /\.codex-panel__goal \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const toolbar = /\.codex-panel__toolbar \{(?<body>[^}]+)\}/.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\)\) \{(?<body>[^}]+)\}/.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\((?<states>[^)]+)\):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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const selectedNavItem =
/\.codex-panel-ui__nav-item\.is-selected:where\(:hover, :focus, :focus-visible, :active, :focus-within\) \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const navRow = /\.codex-panel-ui__nav-row \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const statusPanelItems = /\.codex-panel__status-panel-items \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const contextDots = /\.codex-panel__composer-meta-context-dots \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const placeholderDot =
/\.codex-panel__composer-meta-context-dot\.is-placeholder \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const contextPercent = /\.codex-panel__composer-meta-context-percent \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const composerMeta = /\.codex-panel__composer-meta \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const composerMetaStatus = /\.codex-panel__composer-meta-status \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const composerMetaFatal = /\.codex-panel__composer-meta-fatal \{(?<body>[^}]+)\}/.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 = /^(?<selectors>(?:\.[^{]+,\n)*\.[^{]+) \{(?<body>[^}]+)\}/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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const visual = /\.codex-panel__composer-meta-status-visual \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const field = /\.codex-panel__composer-meta-field \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const labels =
/\.codex-panel__composer-meta-value\.codex-panel__composer-meta-value \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const icon = /\.codex-panel__composer-meta-icon \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const activeIcon = /\.codex-panel__composer-meta-icon\.is-active \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const iconSvg = /\.codex-panel__composer-meta-icon svg \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const option = /\.codex-panel__composer-meta-option \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(
styles,
)?.groups?.["body"] ?? "";
const chatRenameInput = /\.codex-panel__thread-rename-input \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const threadsRenameInput = /\.codex-panel-threads__rename-input \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitRow = /\.codex-panel__limit-panel-row \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitMeterCell = /\.codex-panel__limit-panel-meter-cell \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitMeter = /\.codex-panel__limit-panel-meter \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const limitValue = /\.codex-panel__limit-panel-value \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const openTurnDiffSvg = /\.codex-panel__open-turn-diff svg \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const openTurnDiffHover = /\.codex-panel__open-turn-diff:hover \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const instruction = /\.codex-panel-selection-rewrite__instruction \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const iconButton = /\.codex-panel-selection-rewrite__icon-button \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const actions = /\.codex-panel-selection-rewrite__result-actions \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const diffBody = /\.codex-panel-selection-rewrite__diff-body \{(?<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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const rowMain = /\.codex-panel-threads__row-main \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const renameForm = /\.codex-panel-threads__rename-form \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const renameField = /\.codex-panel-threads__rename-field \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const renameInput = /\.codex-panel-threads__rename-input \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const title = /(?:^|\n\n)\.codex-panel-threads__row-title \{(?<body>[^}]+)\}/.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\) \{(?<body>[^}]+)\}/.exec(
styles,
)?.groups?.["body"] ?? "";
const toolbarMouseFocus =
/\.codex-panel-ui__toolbar-action:where\(:focus:not\(:hover\):not\(:focus-visible\)\) \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.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 \{(?<body>[^}]+)\}/.exec(
styles,
)?.groups?.["body"] ?? "";
const title = /(?:^|\n\n)\.codex-panel-threads__row-title \{(?<body>[^}]+)\}/.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)");
});
});