Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/chat-ui/src/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,7 @@ function TextBubble({
/>
</button>
)}
<div
className="chat-bubble"
data-own={isOwn}
data-pending={pendingStatus}
>
<div className="chat-bubble" data-pending={pendingStatus}>
{showHeader ? (
<div className="chat-bubble-head">
{display !== undefined && (
Expand Down Expand Up @@ -1045,7 +1041,7 @@ function StreamingMessageGroup({
isAgent={display.isAgent}
/>
)}
<div className="chat-bubble" data-own="false">
<div className="chat-bubble">
{display !== undefined && (
<div className="chat-bubble-head">
<span className="chat-bubble-sender">
Expand Down
42 changes: 42 additions & 0 deletions packages/chat-ui/test/chat-bubble-flat.test.ts
Original file line number Diff line number Diff line change
@@ -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*:/);
}
});
});
48 changes: 46 additions & 2 deletions packages/chat-ui/test/own-message-alignment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
7 changes: 6 additions & 1 deletion packages/chat/test/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
[
Expand All @@ -106,6 +110,7 @@ describeIfDb("applyChatMigrations", () => {
"pinned_messages",
"finalized_turn_write_claim",
"workbench_messages",
"agent_turns",
].sort(),
);

Expand Down
Loading