惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
H
Help Net Security
Jina AI
Jina AI
V
V2EX
G
Google Developers Blog
B
Blog
GbyAI
GbyAI
U
Unit 42
爱范儿
爱范儿
腾讯CDC
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
小众软件
小众软件
D
DataBreaches.Net
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
博客园 - 聂微东
The Cloudflare Blog
I
InfoQ
Microsoft Azure Blog
Microsoft Azure Blog
Hugging Face - Blog
Hugging Face - Blog
大猫的无限游戏
大猫的无限游戏

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(control-ui): wire slash menu accessibility · openclaw...
BunsDev · 2026-04-30 · via Recent Commits to openclaw:main

@@ -19,7 +19,7 @@ import { renderChatSessionSelect } from "../chat/session-controls.ts";

1919

import type { GatewayBrowserClient } from "../gateway.ts";

2020

import type { ModelCatalogEntry } from "../types.ts";

2121

import type { ChatQueueItem } from "../ui-types.ts";

22-

import { renderChat } from "./chat.ts";

22+

import { renderChat, resetChatViewState } from "./chat.ts";

23232424

const refreshVisibleToolsEffectiveForCurrentSessionMock = vi.hoisted(() =>

2525

vi.fn(async (state: AppViewState) => {

@@ -392,6 +392,7 @@ function renderChatView(overrides: Partial<Parameters<typeof renderChat>[0]> = {

392392

afterEach(() => {

393393

loadSessionsMock.mockClear();

394394

refreshVisibleToolsEffectiveForCurrentSessionMock.mockClear();

395+

resetChatViewState();

395396

resetChatAttachmentPayloadStoreForTest();

396397

vi.unstubAllGlobals();

397398

});

@@ -452,6 +453,134 @@ describe("chat voice controls", () => {

452453

});

453454

});

454455456+

describe("chat slash menu accessibility", () => {

457+

function inputDraft(container: HTMLElement, value: string) {

458+

const textarea = container.querySelector<HTMLTextAreaElement>("textarea");

459+

expect(textarea).not.toBeNull();

460+

textarea!.value = value;

461+

textarea!.dispatchEvent(new Event("input", { bubbles: true }));

462+

}

463+464+

function keydownComposer(container: HTMLElement, key: string) {

465+

const textarea = container.querySelector<HTMLTextAreaElement>("textarea");

466+

expect(textarea).not.toBeNull();

467+

textarea!.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));

468+

}

469+470+

it("wires command suggestions to the composer with stable active option ids", () => {

471+

let draft = "";

472+

const onDraftChange = vi.fn((next: string) => {

473+

draft = next;

474+

});

475+

let container = renderChatView({ draft, onDraftChange });

476+477+

inputDraft(container, "/");

478+

container = renderChatView({ draft, onDraftChange });

479+480+

const wrapper = container.querySelector<HTMLElement>(".agent-chat__composer-combobox");

481+

const textarea = container.querySelector<HTMLTextAreaElement>("textarea");

482+

const listbox = container.querySelector<HTMLElement>("#chat-slash-menu-listbox");

483+

const activeId = textarea?.getAttribute("aria-activedescendant");

484+485+

expect(wrapper?.hasAttribute("role")).toBe(false);

486+

expect(wrapper?.hasAttribute("aria-expanded")).toBe(false);

487+

expect(wrapper?.hasAttribute("aria-haspopup")).toBe(false);

488+

expect(wrapper?.hasAttribute("aria-controls")).toBe(false);

489+

expect(textarea?.hasAttribute("role")).toBe(false);

490+

expect(textarea?.hasAttribute("aria-expanded")).toBe(false);

491+

expect(textarea?.hasAttribute("aria-haspopup")).toBe(false);

492+

expect(textarea?.getAttribute("aria-controls")).toBe("chat-slash-menu-listbox");

493+

expect(textarea?.getAttribute("aria-autocomplete")).toBe("list");

494+

expect(listbox?.getAttribute("role")).toBe("listbox");

495+

expect(activeId).toMatch(/^chat-slash-option-command-/u);

496+

expect(listbox?.querySelector(`#${activeId}`)?.getAttribute("role")).toBe("option");

497+

});

498+499+

it("updates the active descendant and live announcement during command navigation", () => {

500+

let draft = "";

501+

const onDraftChange = vi.fn((next: string) => {

502+

draft = next;

503+

});

504+

let container = renderChatView({ draft, onDraftChange });

505+506+

inputDraft(container, "/");

507+

container = renderChatView({ draft, onDraftChange });

508+

const initialActiveId = container

509+

.querySelector<HTMLTextAreaElement>("textarea")

510+

?.getAttribute("aria-activedescendant");

511+512+

keydownComposer(container, "ArrowDown");

513+

container = renderChatView({ draft, onDraftChange });

514+515+

const textarea = container.querySelector<HTMLTextAreaElement>("textarea");

516+

const nextActiveId = textarea?.getAttribute("aria-activedescendant");

517+

const activeOption = nextActiveId

518+

? container.querySelector<HTMLElement>(`#${nextActiveId}`)

519+

: null;

520+

const status = container.querySelector<HTMLElement>("#chat-slash-active-announcement");

521+522+

expect(nextActiveId).toBeTruthy();

523+

expect(nextActiveId).not.toBe(initialActiveId);

524+

expect(activeOption?.getAttribute("aria-selected")).toBe("true");

525+

expect(status?.getAttribute("aria-live")).toBe("polite");

526+

expect(status?.textContent?.trim()).toBeTruthy();

527+

expect(status?.textContent).toContain(activeOption?.textContent?.trim().split(/\s+/u)[0]);

528+

});

529+530+

it("wires fixed argument suggestions with command-and-argument option ids", () => {

531+

let draft = "";

532+

const onDraftChange = vi.fn((next: string) => {

533+

draft = next;

534+

});

535+

let container = renderChatView({ draft, onDraftChange });

536+537+

inputDraft(container, "/tools ");

538+

container = renderChatView({ draft, onDraftChange });

539+540+

const textarea = container.querySelector<HTMLTextAreaElement>("textarea");

541+

const listbox = container.querySelector<HTMLElement>("#chat-slash-menu-listbox");

542+

const activeId = textarea?.getAttribute("aria-activedescendant");

543+544+

expect(listbox?.getAttribute("aria-label")).toBe("Command arguments");

545+

expect(activeId).toBe("chat-slash-option-arg-tools-compact");

546+

expect(listbox?.querySelector(`#${activeId}`)?.getAttribute("aria-selected")).toBe("true");

547+

});

548+549+

it("clears active descendant when suggestions close", () => {

550+

let draft = "";

551+

const onDraftChange = vi.fn((next: string) => {

552+

draft = next;

553+

});

554+

let container = renderChatView({ draft, onDraftChange });

555+556+

inputDraft(container, "/");

557+

container = renderChatView({ draft, onDraftChange });

558+

expect(

559+

container

560+

.querySelector<HTMLTextAreaElement>("textarea")

561+

?.getAttribute("aria-activedescendant"),

562+

).toBeTruthy();

563+564+

inputDraft(container, "plain message");

565+

container = renderChatView({ draft, onDraftChange });

566+567+

expect(container.querySelector(".slash-menu")).toBeNull();

568+

expect(

569+

container.querySelector<HTMLTextAreaElement>("textarea")?.hasAttribute("aria-expanded"),

570+

).toBe(false);

571+

expect(

572+

container

573+

.querySelector<HTMLElement>(".agent-chat__composer-combobox")

574+

?.hasAttribute("aria-expanded"),

575+

).toBe(false);

576+

expect(

577+

container

578+

.querySelector<HTMLTextAreaElement>("textarea")

579+

?.hasAttribute("aria-activedescendant"),

580+

).toBe(false);

581+

});

582+

});

583+455584

describe("chat attachment picker", () => {

456585

it("accepts and previews non-video file attachments", async () => {

457586

const onAttachmentsChange = vi.fn();