feat(transcription): add shared voice input support (#4232)

* feat(webui): add voice transcription input

* feat(webui): render ANSI output in code blocks

* refactor(webui): isolate voice recorder logic

* refactor(transcription): keep websocket ingress thin

* refactor(transcription): resolve channel audio settings on demand

* style(webui): neutralize voice waveform color

* feat(webui): add voice input tooltip

* feat(webui): add voice input keyboard shortcut

* fix(webui): distinguish voice shortcut platforms

* fix(webui): place voice button after model selector

* refactor(webui): share voice hold recording helpers

* fix(desktop): allow microphone voice input

* fix(webui): stabilize token usage month labels

* feat(webui): show voice input on settings overview

* fix(webui): label voice capability as recognition

* fix(webui): align capability overview status

* refactor(webui): isolate transcription socket handling

* fix(webui): soften silent voice waveform

* refactor(audio): clarify transcription service location

* docs(transcription): clarify audio and provider boundaries

* fix(exec): reduce session output polling flake
This commit is contained in:
Xubin Ren
2026-06-09 01:08:49 +08:00
committed by GitHub
parent 06d454a225
commit 9c81280300
49 changed files with 3071 additions and 257 deletions
+59
View File
@@ -1,4 +1,5 @@
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { CodeBlock } from "@/components/CodeBlock";
@@ -87,6 +88,64 @@ describe("CodeBlock", () => {
expect(screen.getByText("const value = 1;")).toBeInTheDocument();
});
it("renders ANSI output without mounting the syntax highlighter", () => {
render(
<ThemeProvider theme="dark">
<CodeBlock
language="ansi"
code={"\x1b[32mPASS\x1b[0m <script>alert(1)</script>"}
/>
</ThemeProvider>,
);
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
expect(screen.getByTestId("ansi-code")).toBeInTheDocument();
expect(screen.getByTestId("ansi-code").closest(".not-prose")).toBeTruthy();
expect(screen.getByText("ansi")).toBeInTheDocument();
expect(screen.getByText("PASS")).toHaveStyle({ color: "#0dbc79" });
expect(screen.getByText("<script>alert(1)</script>")).toBeInTheDocument();
expect(document.querySelector("script")).toBeNull();
});
it("detects ANSI sequences in regular code blocks", () => {
render(
<ThemeProvider theme="light">
<CodeBlock
language="text"
code={"\x1b[38;2;35;209;139mtruecolor\x1b[0m"}
/>
</ThemeProvider>,
);
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
expect(screen.getByText("truecolor")).toHaveStyle({
color: "rgb(35, 209, 139)",
});
});
it("copies ANSI output as clean text", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
try {
render(
<ThemeProvider theme="dark">
<CodeBlock language="ansi" code={"\x1b[32mPASS\x1b[0m"} />
</ThemeProvider>,
);
await user.click(screen.getByRole("button", { name: /copy/i }));
expect(writeText).toHaveBeenCalledWith("PASS");
} finally {
Reflect.deleteProperty(navigator, "clipboard");
}
});
it("reads theme from context without creating per-block observers", async () => {
const originalMutationObserver = globalThis.MutationObserver;
const observer = vi.fn();