diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css
index 5b4606ade..cd482f46f 100644
--- a/ui/src/styles/chat/grouped.css
+++ b/ui/src/styles/chat/grouped.css
@@ -64,7 +64,10 @@
color: var(--muted);
opacity: 0;
pointer-events: none;
- transition: opacity 120ms ease-out, color 120ms ease-out, background 120ms ease-out;
+ transition:
+ opacity 120ms ease-out,
+ color 120ms ease-out,
+ background 120ms ease-out;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -77,7 +80,7 @@
.chat-group-footer button:hover {
opacity: 1 !important;
- background: var(--bg-hover, rgba(255,255,255,0.08));
+ background: var(--bg-hover, rgba(255, 255, 255, 0.08));
}
.chat-group-footer button svg {
@@ -371,7 +374,7 @@ img.chat-avatar {
}
.msg-meta__model {
- background: var(--bg-hover, rgba(255,255,255,0.06));
+ background: var(--bg-hover, rgba(255, 255, 255, 0.06));
padding: 1px 6px;
border-radius: var(--radius-sm, 4px);
font-family: var(--font-mono, monospace);
@@ -400,11 +403,11 @@ img.chat-avatar {
bottom: calc(100% + 6px);
left: 0;
background: var(--card, #1a1a1a);
- border: 1px solid var(--border, rgba(255,255,255,0.1));
+ border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
border-radius: var(--radius-md, 8px);
padding: 12px;
min-width: 200px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
z-index: 100;
animation: scale-in 0.15s ease-out;
}
@@ -452,12 +455,12 @@ img.chat-avatar {
}
.chat-delete-confirm__cancel {
- background: var(--bg-hover, rgba(255,255,255,0.08));
+ background: var(--bg-hover, rgba(255, 255, 255, 0.08));
color: var(--muted, #888);
}
.chat-delete-confirm__cancel:hover {
- background: rgba(255,255,255,0.12);
+ background: rgba(255, 255, 255, 0.12);
}
.chat-delete-confirm__yes {
diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css
index 6a16c013e..6d12698d6 100644
--- a/ui/src/styles/chat/layout.css
+++ b/ui/src/styles/chat/layout.css
@@ -920,7 +920,9 @@
background: var(--panel);
color: var(--foreground);
cursor: pointer;
- transition: background 0.15s, border-color 0.15s;
+ transition:
+ background 0.15s,
+ border-color 0.15s;
}
.agent-chat__suggestion:hover {
diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css
index e25edce48..2114ea256 100644
--- a/ui/src/styles/layout.css
+++ b/ui/src/styles/layout.css
@@ -65,7 +65,7 @@
padding-top: 0;
}
-.shell--chat-focus .content>*+* {
+.shell--chat-focus .content > * + * {
margin-top: 0;
}
@@ -805,7 +805,7 @@
overflow-x: hidden;
}
-.content>*+* {
+.content > * + * {
margin-top: 20px;
}
@@ -821,7 +821,7 @@
padding-bottom: 0;
}
-.content--chat>*+* {
+.content--chat > * + * {
margin-top: 0;
}
@@ -879,7 +879,7 @@
gap: 16px;
}
-.content--chat .content-header>div:first-child {
+.content--chat .content-header > div:first-child {
text-align: left;
}
diff --git a/ui/src/ui/chat/export.node.test.ts b/ui/src/ui/chat/export.node.test.ts
index fa4bb428b..807fba881 100644
--- a/ui/src/ui/chat/export.node.test.ts
+++ b/ui/src/ui/chat/export.node.test.ts
@@ -1,38 +1,26 @@
import { describe, expect, it } from "vitest";
-import { buildChatExportFilename, buildChatMarkdown, sanitizeFilenameComponent } from "./export.ts";
+import { buildChatMarkdown } from "./export.ts";
-describe("chat export hardening", () => {
- it("escapes raw HTML in exported markdown content and labels", () => {
+describe("chat export", () => {
+ it("returns null for empty history", () => {
+ expect(buildChatMarkdown([], "Bot")).toBeNull();
+ });
+
+ it("renders markdown headings and strips assistant thinking tags", () => {
const markdown = buildChatMarkdown(
[
{
role: "assistant",
- content: "
",
+ content: "scratchpadFinal answer",
timestamp: Date.UTC(2026, 2, 11, 12, 0, 0),
},
],
- "Bot ",
+ "Bot",
);
- expect(markdown).toContain(
- "# Chat with Bot </script><script>alert(3)</script>",
- );
- expect(markdown).toContain(
- "## Bot </script><script>alert(3)</script> (2026-03-11T12:00:00.000Z)",
- );
- expect(markdown).toContain(
- "<img src=x onerror=alert(1)><script>alert(2)</script>",
- );
- expect(markdown).not.toContain("")).toBe(
- "NUL scriptalert1-script",
- );
- expect(buildChatExportFilename("../NUL\t", 123)).toBe(
- "chat-NUL scriptalert1-script-123.md",
- );
+ expect(markdown).toContain("# Chat with Bot");
+ expect(markdown).toContain("## Bot (2026-03-11T12:00:00.000Z)");
+ expect(markdown).toContain("Final answer");
+ expect(markdown).not.toContain("scratchpad");
});
});
diff --git a/ui/src/ui/theme.test.ts b/ui/src/ui/theme.test.ts
index 68a855a44..b708abbf4 100644
--- a/ui/src/ui/theme.test.ts
+++ b/ui/src/ui/theme.test.ts
@@ -1,26 +1,24 @@
import { describe, expect, it, vi } from "vitest";
-import { colorSchemeForTheme, parseThemeSelection, resolveTheme } from "./theme.ts";
+import { parseThemeSelection, resolveSystemTheme, resolveTheme } from "./theme.ts";
describe("resolveTheme", () => {
- it("keeps the legacy mode-only signature working for existing callers", () => {
- expect(resolveTheme("dark")).toBe("dark");
- expect(resolveTheme("light")).toBe("light");
- });
-
it("resolves named theme families when mode is provided", () => {
expect(resolveTheme("knot", "dark")).toBe("openknot");
expect(resolveTheme("dash", "light")).toBe("dash-light");
});
- it("uses system preference when a named theme omits mode", () => {
+ it("uses system preference when mode is system", () => {
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: true }));
- expect(resolveTheme("knot")).toBe("openknot-light");
+ expect(resolveTheme("knot", "system")).toBe("openknot-light");
vi.unstubAllGlobals();
});
+});
- it("maps resolved theme families back to valid CSS color-scheme values", () => {
- expect(colorSchemeForTheme("openknot")).toBe("dark");
- expect(colorSchemeForTheme("dash-light")).toBe("light");
+describe("resolveSystemTheme", () => {
+ it("mirrors the active preferred color scheme", () => {
+ vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: true }));
+ expect(resolveSystemTheme()).toBe("light");
+ vi.unstubAllGlobals();
});
});
diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts
index d67acd774..4565aae8a 100644
--- a/ui/src/ui/views/chat.test.ts
+++ b/ui/src/ui/views/chat.test.ts
@@ -46,6 +46,9 @@ function createProps(overrides: Partial = {}): ChatProps {
onSend: () => undefined,
onQueueRemove: () => undefined,
onNewSession: () => undefined,
+ agentsList: null,
+ currentAgentId: "",
+ onAgentChange: () => undefined,
...overrides,
};
}
diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts
index 889d046f9..138c1654e 100644
--- a/ui/src/ui/views/config.browser.test.ts
+++ b/ui/src/ui/views/config.browser.test.ts
@@ -1,5 +1,6 @@
import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
+import type { ThemeMode, ThemeName } from "../theme.ts";
import { renderConfig } from "./config.ts";
describe("config view", () => {
@@ -35,6 +36,13 @@ describe("config view", () => {
onApply: vi.fn(),
onUpdate: vi.fn(),
onSubsectionChange: vi.fn(),
+ version: "2026.3.11",
+ theme: "claw" as ThemeName,
+ themeMode: "system" as ThemeMode,
+ setTheme: vi.fn(),
+ setThemeMode: vi.fn(),
+ gatewayUrl: "",
+ assistantName: "OpenClaw",
});
function findActionButtons(container: HTMLElement): {
diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts
index 453c21659..1fa654505 100644
--- a/ui/src/ui/views/sessions.test.ts
+++ b/ui/src/ui/views/sessions.test.ts
@@ -23,7 +23,18 @@ function buildProps(result: SessionsListResult): SessionsProps {
includeGlobal: false,
includeUnknown: false,
basePath: "",
+ searchQuery: "",
+ sortColumn: "updated",
+ sortDir: "desc",
+ page: 0,
+ pageSize: 10,
+ actionsOpenKey: null,
onFiltersChange: () => undefined,
+ onSearchChange: () => undefined,
+ onSortChange: () => undefined,
+ onPageChange: () => undefined,
+ onPageSizeChange: () => undefined,
+ onActionsOpenChange: () => undefined,
onRefresh: () => undefined,
onPatch: () => undefined,
onDelete: () => undefined,