(null);
+ useLayoutEffect(() => {
+ const action = ref.current;
+ if (!action) return;
+ action.replaceChildren();
+ setIcon(action, icon);
+ }, [icon]);
+ return (
+ // biome-ignore lint/a11y: Obsidian core toolbar icons are div.clickable-icon nav-action-button elements with aria-label tooltips, not native buttons.
+ {
+ ref.current = element;
+ if (typeof actionRef === "function") {
+ actionRef(element);
+ } else if (actionRef) {
+ actionRef.current = element;
+ }
+ }}
+ className={[className, disabled ? "is-disabled" : ""].filter(Boolean).join(" ")}
+ aria-label={label}
+ onClick={disabled ? undefined : onClick}
+ />
+ );
+}
+
export function ObsidianDropdown({
value,
options,
diff --git a/src/styles/10-shared-ui.css b/src/styles/10-shared-ui.css
index a6a812b9..30c64493 100644
--- a/src/styles/10-shared-ui.css
+++ b/src/styles/10-shared-ui.css
@@ -47,13 +47,17 @@
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
}
-.codex-panel-ui__toolbar-action:hover,
-.codex-panel-ui__toolbar-action:focus-visible {
+.codex-panel-ui__toolbar-action:hover:not(.is-disabled),
+.codex-panel-ui__toolbar-action:focus-visible:not(.is-disabled) {
background: var(--background-modifier-hover);
box-shadow: none;
color: var(--icon-color);
}
+.codex-panel-ui__toolbar-action.is-disabled {
+ opacity: 0.45;
+}
+
.codex-panel-ui__icon-button svg,
.codex-panel-ui__toolbar-action svg,
.codex-panel-ui__nav-row-action svg {
diff --git a/src/styles/21-chat-toolbar.css b/src/styles/21-chat-toolbar.css
index 6b268eef..49ebde69 100644
--- a/src/styles/21-chat-toolbar.css
+++ b/src/styles/21-chat-toolbar.css
@@ -24,6 +24,14 @@
text-align: left;
}
+.codex-panel-ui__nav-item.codex-panel__toolbar-panel-item {
+ justify-content: flex-start;
+ height: auto;
+ background: transparent;
+ box-shadow: none;
+ color: var(--nav-item-color, var(--text-muted));
+}
+
.codex-panel__toolbar-panel-label,
.codex-panel__toolbar-panel-meta {
min-width: 0;
diff --git a/src/styles/25-chat-pending-requests.css b/src/styles/25-chat-pending-requests.css
index 4ae69db3..8c54c9eb 100644
--- a/src/styles/25-chat-pending-requests.css
+++ b/src/styles/25-chat-pending-requests.css
@@ -179,6 +179,10 @@
.codex-panel__mcp-elicitation-options {
display: grid;
gap: var(--codex-panel-control-gap);
+ min-inline-size: 0;
+ margin: 0;
+ border: 0;
+ padding: 0;
}
.codex-panel__mcp-elicitation-option {
diff --git a/tests/features/chat/panel/surface/projection.test.ts b/tests/features/chat/panel/surface/projection.test.ts
index ac588870..bb2b91a0 100644
--- a/tests/features/chat/panel/surface/projection.test.ts
+++ b/tests/features/chat/panel/surface/projection.test.ts
@@ -47,14 +47,17 @@ describe("chat panel surface projections", () => {
);
expect(parent.querySelector('[data-codex-panel-toolbar-panel="history"]')).not.toBeNull();
- expect(parent.querySelector(".codex-panel__new-chat")?.disabled).toBe(true);
+ expect(parent.querySelector(".codex-panel__new-chat")?.tagName).toBe("DIV");
+ expect(parent.querySelector(".codex-panel__new-chat")?.classList.contains("is-disabled")).toBe(true);
expect(parent.querySelector(".codex-panel__thread-row--selected .codex-panel__thread-rename-input")?.value).toBe(
"Active",
);
expect(parent.querySelector(".codex-panel__thread-row--archive-confirming .codex-panel__toolbar-panel-label")?.textContent).toBe(
"Other",
);
- expect(parent.querySelectorAll(".codex-panel__thread")[1]?.getAttribute("aria-disabled")).toBe("true");
+ const archivedThread = parent.querySelectorAll(".codex-panel__thread")[1];
+ expect(archivedThread?.tagName).toBe("DIV");
+ expect(archivedThread?.classList.contains("is-disabled")).toBe(true);
unmountUiRoot(parent);
});
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 9ffe3513..b639e865 100644
--- a/tests/features/chat/ui/message-stream/pending-requests.test.tsx
+++ b/tests/features/chat/ui/message-stream/pending-requests.test.tsx
@@ -499,7 +499,11 @@ describe("pending request renderer decisions", () => {
);
expect(parent.querySelector(".codex-panel__pending-request-title")?.textContent).toBe("MCP request from github");
- changeInputValue(expectPresent(parent.querySelector(".codex-panel__mcp-elicitation-input")), "Updated");
+ const input = expectPresent(parent.querySelector(".codex-panel__mcp-elicitation-input"));
+ const label = expectPresent(parent.querySelector(".codex-panel__mcp-elicitation-label"));
+ expect(label.tagName).toBe("LABEL");
+ expect(label.getAttribute("for")).toBe(input.id);
+ changeInputValue(input, "Updated");
actEvent(() => {
expectPresent(parent.querySelector(".codex-panel__pending-request-button.mod-cta")).click();
});
@@ -564,6 +568,10 @@ 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);
actEvent(() => {
expectPresent(parent.querySelector(".codex-panel__pending-request-button.mod-cta")).click();
});
diff --git a/tests/features/chat/ui/renderers/toolbar.test.ts b/tests/features/chat/ui/renderers/toolbar.test.ts
index a39a79e2..80b67b49 100644
--- a/tests/features/chat/ui/renderers/toolbar.test.ts
+++ b/tests/features/chat/ui/renderers/toolbar.test.ts
@@ -36,24 +36,30 @@ describe("Toolbar decisions", () => {
"Show chat actions",
"Show status",
]);
+ expect([...expectPresent(navButtons).children].map((button) => button.tagName)).toEqual(["DIV", "DIV", "DIV"]);
+ expect([...expectPresent(navButtons).children].map((button) => button.getAttribute("role"))).toEqual([null, null, null]);
expect(parent.querySelector(".codex-panel__plan-toggle")).toBeNull();
expect(parent.querySelector(".codex-panel__auto-review-toggle")).toBeNull();
expect(parent.querySelector(".codex-panel__runtime-model")).toBeNull();
- const newChatButton = parent.querySelector(".codex-panel__new-chat");
+ const newChatButton = parent.querySelector(".codex-panel__new-chat");
expect(newChatButton?.getAttribute("aria-label")).toBe("Show chat actions");
- expect(newChatButton?.disabled).toBe(false);
+ expect(newChatButton?.getAttribute("aria-expanded")).toBeNull();
+ expect(newChatButton?.classList.contains("is-disabled")).toBe(false);
newChatButton?.click();
expect(toggleChatActions).toHaveBeenCalled();
expect(startNewThread).not.toHaveBeenCalled();
const statusButton = parent.querySelector(".codex-panel__status-menu-toggle");
expect(statusButton?.getAttribute("aria-label")).toBe("Show status");
- const historyButton = parent.querySelector(".codex-panel__history-toggle");
+ expect(statusButton?.getAttribute("aria-expanded")).toBeNull();
+ const historyButton = parent.querySelector(".codex-panel__history-toggle");
expect(historyButton?.getAttribute("aria-label")).toBe("Show thread list");
+ expect(historyButton?.getAttribute("aria-pressed")).toBeNull();
historyButton?.click();
expect(toggleHistory).toHaveBeenCalled();
parent.empty();
mountToolbar(parent, toolbarModel({ newChatDisabled: true }), toolbarActions());
- expect(parent.querySelector(".codex-panel__new-chat")?.disabled).toBe(true);
+ expect(parent.querySelector(".codex-panel__new-chat")?.tagName).toBe("DIV");
+ expect(parent.querySelector(".codex-panel__new-chat")?.classList.contains("is-disabled")).toBe(true);
parent.empty();
mountToolbar(parent, toolbarModel({ chatActionsOpen: true, historyOpen: true, statusPanelOpen: true }), toolbarActions());
@@ -78,8 +84,11 @@ describe("Toolbar decisions", () => {
);
const items = [...parent.querySelectorAll(".codex-panel__chat-actions-panel-item")];
+ expect(parent.querySelector(".codex-panel__chat-actions-panel-items")?.tagName).toBe("DIV");
+ expect(parent.querySelector(".codex-panel__chat-actions-panel-items")?.getAttribute("aria-label")).toBeNull();
expect(items.map((item) => item.textContent)).toEqual(["Start new chat", "Compact conversation", "Set goal..."]);
- expect(items.map((item) => item.getAttribute("role"))).toEqual(["menuitem", "menuitem", "menuitem"]);
+ expect(items.map((item) => item.tagName)).toEqual(["DIV", "DIV", "DIV"]);
+ expect(items.map((item) => item.getAttribute("role"))).toEqual([null, null, null]);
items[0]?.click();
items[1]?.click();
items[2]?.click();
@@ -168,7 +177,10 @@ describe("Toolbar decisions", () => {
expect(parent.textContent).toContain("codex-cli/1.2.3");
expect(parent.querySelector(".codex-panel__connection-diagnostics-row--error")?.textContent).toContain("model/list failed");
const statusItems = [...parent.querySelectorAll(".codex-panel__status-panel-item")];
- expect(statusItems.map((item) => item.getAttribute("role"))).toEqual(["menuitem", "menuitem", "menuitem"]);
+ expect(parent.querySelector(".codex-panel__status-panel-items")?.tagName).toBe("DIV");
+ expect(parent.querySelector(".codex-panel__status-panel-items")?.getAttribute("aria-label")).toBeNull();
+ expect(statusItems.map((item) => item.tagName)).toEqual(["DIV", "DIV", "DIV"]);
+ expect(statusItems.map((item) => item.getAttribute("role"))).toEqual([null, null, null]);
expect(statusItems.every((item) => item.getAttribute("aria-selected") === null)).toBe(true);
statusItems.find((item) => item.textContent.includes("Refresh"))?.click();
expect(refreshStatus).toHaveBeenCalled();
diff --git a/tests/features/threads-view/renderer.test.ts b/tests/features/threads-view/renderer.test.ts
index 33bcbfea..7bb26a97 100644
--- a/tests/features/threads-view/renderer.test.ts
+++ b/tests/features/threads-view/renderer.test.ts
@@ -155,18 +155,23 @@ describe("threads view renderer decisions", () => {
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(main.getAttribute("aria-current")).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();
- const toolbarButtons = [...parent.querySelectorAll(".codex-panel-threads__toolbar-button")];
+ const toolbarButtons = [...parent.querySelectorAll(".codex-panel-threads__toolbar-button")];
expect(toolbarButtons.map((button) => button.getAttribute("aria-label"))).toEqual(["Open new panel", "Refresh threads"]);
- const refresh = expectPresent(parent.querySelector('[aria-label="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"]'));
+ 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");
@@ -177,8 +182,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");
- main.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
- expect(actions.openThread).toHaveBeenCalledTimes(2);
expect(parent.querySelector('[aria-label="Focus open panel"]')).toBeNull();
expect(parent.querySelector('[aria-label="Open in new panel"]')).toBeNull();
});
diff --git a/tests/styles.test.ts b/tests/styles.test.ts
index 8f1ce244..77c5658c 100644
--- a/tests/styles.test.ts
+++ b/tests/styles.test.ts
@@ -77,6 +77,16 @@ describe("chat toolbar CSS", () => {
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(
@@ -339,9 +349,9 @@ describe("threads view CSS", () => {
it("keeps toolbar action hover color separate from row action hover color", () => {
const toolbarHover =
- /\.codex-panel-ui__toolbar-action:hover,\n\.codex-panel-ui__toolbar-action:focus-visible \{(?[^}]+)\}/.exec(styles)?.groups?.[
- "body"
- ] ?? "";
+ /\.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"