diff --git a/packages/chat-ui/src/timeline.tsx b/packages/chat-ui/src/timeline.tsx index a1761f062..e5dc43866 100644 --- a/packages/chat-ui/src/timeline.tsx +++ b/packages/chat-ui/src/timeline.tsx @@ -507,11 +507,7 @@ function TextBubble({ /> )} -
+
{showHeader ? (
{display !== undefined && ( @@ -1045,7 +1041,7 @@ function StreamingMessageGroup({ isAgent={display.isAgent} /> )} -
+
{display !== undefined && (
diff --git a/packages/chat-ui/test/chat-bubble-flat.test.ts b/packages/chat-ui/test/chat-bubble-flat.test.ts new file mode 100644 index 000000000..31fddce5a --- /dev/null +++ b/packages/chat-ui/test/chat-bubble-flat.test.ts @@ -0,0 +1,42 @@ +// happy-dom does not apply stylesheets, so paint/chrome for `.chat-bubble` +// is locked against CSS source (CL-6466: leftover bubble class, no chrome). +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const css = readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "../src/styles.css"), + "utf8", +); + +function ruleBodies(selector: string): string[] { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return [ + ...css.matchAll( + new RegExp(`(?:^|\\n)\\s*${escaped}\\s*\\{([^}]+)\\}`, "g"), + ), + ].map((match) => match[1] ?? ""); +} + +describe("chat-bubble is visually flat", () => { + test(".chat-bubble has no border, fill, padding, or radius", () => { + const bodies = ruleBodies(".chat-bubble"); + expect(bodies.length).toBeGreaterThan(0); + const body = bodies[0] ?? ""; + expect(body).toMatch(/border:\s*0/); + expect(body).toMatch(/background:\s*transparent/); + expect(body).toMatch(/padding:\s*0/); + expect(body).toMatch(/border-radius:\s*0/); + }); + + test('.chat-bubble[data-own="true"] does not paint chrome', () => { + for (const body of ruleBodies('.chat-bubble[data-own="true"]')) { + expect(body).not.toMatch(/background\s*:/); + expect(body).not.toMatch(/border\s*:/); + expect(body).not.toMatch(/padding\s*:/); + expect(body).not.toMatch(/border-radius\s*:/); + expect(body).not.toMatch(/box-shadow\s*:/); + } + }); +}); diff --git a/packages/chat-ui/test/own-message-alignment.test.tsx b/packages/chat-ui/test/own-message-alignment.test.tsx index d1ca6eec4..568400be0 100644 --- a/packages/chat-ui/test/own-message-alignment.test.tsx +++ b/packages/chat-ui/test/own-message-alignment.test.tsx @@ -61,7 +61,7 @@ describe("own-message alignment is per viewer, not per message", () => { const bubble = el.querySelector(".chat-bubble"); expect(group?.getAttribute("data-own")).toBe("true"); expect(row?.getAttribute("data-own")).toBe("true"); - expect(bubble?.getAttribute("data-own")).toBe("true"); + expect(bubble?.hasAttribute("data-own")).toBe(false); }); test("the same message viewed by a different user renders left-aligned", async () => { @@ -73,7 +73,7 @@ describe("own-message alignment is per viewer, not per message", () => { const bubble = el.querySelector(".chat-bubble"); expect(group?.getAttribute("data-own")).toBe("false"); expect(row?.getAttribute("data-own")).toBe("false"); - expect(bubble?.getAttribute("data-own")).toBe("false"); + expect(bubble?.hasAttribute("data-own")).toBe(false); }); test("a system event line never gets own-message alignment, even when the current user caused it", async () => { @@ -134,4 +134,48 @@ describe("own-message alignment is per viewer, not per message", () => { el.querySelector(".chat-message-group")?.getAttribute("data-own"), ).toBe("false"); }); + + test("own-authored text plus tool activity plus a gen-UI block keeps tools and the block outside the bubble", async () => { + const items: MessageItem[] = [ + { + id: "m1", + createdAt: "2026-01-01T00:00:00.000Z", + sender: { name: "Sawyer", address: "sawyer@agents.example" }, + parts: [ + { kind: "text", text: "ship it" }, + { + kind: "tool-trace", + name: "search", + input: { q: "x" }, + status: "success", + }, + { + kind: "block", + block: { + type: "steps", + data: { + title: "Migration", + steps: [{ label: "Snapshot", state: "done" }], + }, + }, + }, + ], + }, + ]; + const el = await mount(items, { principalId: "sawyer" }); + const group = el.querySelector(".chat-message-group"); + const row = el.querySelector(".chat-bubble-row"); + const bubble = el.querySelector(".chat-bubble"); + const tool = el.querySelector(".chat-tool-activity"); + const block = el.querySelector(".chat-block"); + expect(group?.getAttribute("data-own")).toBe("true"); + expect(row?.getAttribute("data-own")).toBe("true"); + expect(bubble?.hasAttribute("data-own")).toBe(false); + expect(tool).not.toBeNull(); + expect(block).not.toBeNull(); + expect(bubble?.contains(tool)).toBe(false); + expect(bubble?.contains(block)).toBe(false); + expect(group?.contains(tool)).toBe(true); + expect(group?.contains(block)).toBe(true); + }); }); diff --git a/packages/chat/test/migrations.test.ts b/packages/chat/test/migrations.test.ts index b9b9bd985..b39807782 100644 --- a/packages/chat/test/migrations.test.ts +++ b/packages/chat/test/migrations.test.ts @@ -40,6 +40,10 @@ const migrationNames = [ "0017_message_client_ids", "0018_rename_channel_to_workbench", "0019_workbench_messages", + "0020_workbench_launch_current_run", + "0021_workbench_launch_prior_runs", + "0022_agent_turns", + "0023_drop_workbench_host_arm", ]; describeIfDb("applyChatMigrations", () => { @@ -91,7 +95,7 @@ describeIfDb("applyChatMigrations", () => { const tables = await sql.unsafe( `SELECT table_name FROM information_schema.tables ` + `WHERE table_schema = 'chat' AND table_name IN ` + - `('workbench_settings', 'workbench_read_state', 'workbench_launch', 'workbench_tenancy', 'chat_bench_settings', 'workbench_threads', 'workbench_thread_messages', 'message_reactions', 'pinned_messages', 'finalized_turn_write_claim', 'workbench_messages')`, + `('workbench_settings', 'workbench_read_state', 'workbench_launch', 'workbench_tenancy', 'chat_bench_settings', 'workbench_threads', 'workbench_thread_messages', 'message_reactions', 'pinned_messages', 'finalized_turn_write_claim', 'workbench_messages', 'agent_turns')`, ); expect(tables.map((row) => String(row["table_name"])).sort()).toEqual( [ @@ -106,6 +110,7 @@ describeIfDb("applyChatMigrations", () => { "pinned_messages", "finalized_turn_write_claim", "workbench_messages", + "agent_turns", ].sort(), );