mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
fix(agents): style tables, headings and quotes in agent messages (#1324)
Agent replies only styled paragraphs, lists and code, so tables, headings, quotes, rules, images and task lists fell back to browser defaults: a 26px h1, a quote indented 40px, table columns squeezed to a few letters by overflow-wrap. They now get chat-sized styles from tokens; wide tables scroll sideways. Closes #1273 Co-authored-by: ntbang0901 <121003585+ntbang0901@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/** Markdown in agent replies, in the real conversation: browser defaults don't suit a narrow chat. */
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
const TASK = {
|
||||
id: "t1",
|
||||
title: "Repro task",
|
||||
status: "ready" as const,
|
||||
isolation: "local" as const,
|
||||
provider: "codex",
|
||||
model: null,
|
||||
cwd: "/dev/acme-api",
|
||||
branch: null,
|
||||
base: null,
|
||||
baseCommit: null,
|
||||
error: null,
|
||||
createdMs: 0,
|
||||
updatedMs: 0,
|
||||
config: [],
|
||||
commands: [],
|
||||
usage: null,
|
||||
queue: [],
|
||||
};
|
||||
|
||||
const PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
const REPLY = [
|
||||
"# Heading one",
|
||||
"",
|
||||
"## Heading two",
|
||||
"",
|
||||
"##### Heading five",
|
||||
"",
|
||||
"> A quote",
|
||||
"",
|
||||
"- [x] done",
|
||||
"- [ ] open",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
``,
|
||||
"",
|
||||
"| Path | Owner | Status | Notes |",
|
||||
"| --- | --- | --- | --- |",
|
||||
"| src/features/agents/conversation/very/long/path/file.tsx | someone | modified | a fairly long note |",
|
||||
"",
|
||||
"```ts",
|
||||
"const a = 1;",
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
async function openReply(page: Page, text: string) {
|
||||
await page.goto("/");
|
||||
await page.getByRole("tree", { name: "Project" }).waitFor();
|
||||
await page.evaluate(
|
||||
async ({ task, text }) => {
|
||||
// Paths go through variables: knip can't resolve these browser-absolute URLs.
|
||||
const storePath = "/src/features/agents/store.ts";
|
||||
const store = await import(storePath);
|
||||
store.useAgents.setState({
|
||||
loaded: true,
|
||||
error: null,
|
||||
providers: [],
|
||||
tasks: new Map([[task.id, task]]),
|
||||
entries: new Map([[task.id, [{ type: "agent", text }]]]),
|
||||
unread: new Set(),
|
||||
docked: null,
|
||||
});
|
||||
store.dockTask(task.id);
|
||||
},
|
||||
{ task: TASK, text },
|
||||
);
|
||||
const reply = page.locator("[class*='_markdown_']").first();
|
||||
await reply.locator("h1, p").first().waitFor();
|
||||
return reply;
|
||||
}
|
||||
|
||||
const css = (reply: ReturnType<Page["locator"]>, selector: string, property: string) =>
|
||||
reply
|
||||
.locator(selector)
|
||||
.first()
|
||||
.evaluate((el, name) => getComputedStyle(el).getPropertyValue(name), property);
|
||||
const px = (value: string) => Number.parseFloat(value);
|
||||
|
||||
test("styles tables so a reply reads as a table", async ({ page }) => {
|
||||
const reply = await openReply(page, REPLY);
|
||||
expect(await css(reply, "td", "border-top-width")).toBe("1px");
|
||||
expect(await css(reply, "td", "padding-left")).toBe("8px");
|
||||
expect(await css(reply, "th", "border-top-width")).toBe("1px");
|
||||
// A narrow chat must not squeeze a column to a few letters.
|
||||
const widths = await reply
|
||||
.locator("td")
|
||||
.evaluateAll((cells) => cells.map((cell) => cell.getBoundingClientRect().width));
|
||||
for (const width of widths) expect(width).toBeGreaterThanOrEqual(70);
|
||||
});
|
||||
|
||||
test("scrolls a wide table inside the reply", async ({ page }) => {
|
||||
const reply = await openReply(page, REPLY);
|
||||
const sizes = await reply.evaluate((el) => {
|
||||
const table = el.querySelector("table")!;
|
||||
return { reply: el.clientWidth, table: table.clientWidth, content: table.scrollWidth };
|
||||
});
|
||||
expect(sizes.table).toBeLessThanOrEqual(sizes.reply);
|
||||
expect(sizes.content).toBeGreaterThan(sizes.table);
|
||||
});
|
||||
|
||||
test("sizes headings for a chat and keeps small ones readable", async ({ page }) => {
|
||||
const reply = await openReply(page, REPLY);
|
||||
const body = px(await css(reply, "td", "font-size"));
|
||||
const h1 = px(await css(reply, "h1", "font-size"));
|
||||
expect(h1).toBeGreaterThan(body);
|
||||
expect(h1).toBeLessThanOrEqual(body * 1.2);
|
||||
expect(px(await css(reply, "h2", "font-size"))).toBeLessThan(h1);
|
||||
expect(px(await css(reply, "h5", "font-size"))).toBe(body);
|
||||
});
|
||||
|
||||
test("starts and ends a reply without stray margins", async ({ page }) => {
|
||||
const reply = await openReply(page, REPLY);
|
||||
expect(await css(reply, "> :first-child", "margin-top")).toBe("0px");
|
||||
expect(await css(reply, "> :last-child", "margin-bottom")).toBe("0px");
|
||||
});
|
||||
|
||||
test("styles quotes, rules, images and task lists", async ({ page }) => {
|
||||
const reply = await openReply(page, REPLY);
|
||||
expect(await css(reply, "blockquote", "margin-left")).toBe("0px");
|
||||
expect(await css(reply, "blockquote", "border-left-width")).toBe("3px");
|
||||
expect(await css(reply, "hr", "border-left-width")).toBe("0px");
|
||||
expect(await css(reply, "hr", "border-top-width")).toBe("1px");
|
||||
expect(await css(reply, "img", "max-width")).toBe("100%");
|
||||
// A checkbox stands in for the bullet.
|
||||
expect(await css(reply, "li:has(> input)", "list-style-type")).toBe("none");
|
||||
});
|
||||
@@ -262,6 +262,7 @@
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
.markdown :global(pre) {
|
||||
margin: 0 0 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--bg-elev);
|
||||
@@ -352,6 +353,60 @@
|
||||
.markdown :global(a) {
|
||||
color: var(--link);
|
||||
}
|
||||
.markdown :global(table) {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
overflow: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.markdown :global(th),
|
||||
.markdown :global(td) {
|
||||
/* `overflow-wrap: anywhere` from .markdown would otherwise squeeze a column to a few letters. */
|
||||
min-width: 6em;
|
||||
border: 1px solid var(--border);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.markdown :global(h1),
|
||||
.markdown :global(h2),
|
||||
.markdown :global(h3),
|
||||
.markdown :global(h4),
|
||||
.markdown :global(h5),
|
||||
.markdown :global(h6) {
|
||||
margin: 12px 0 6px;
|
||||
color: var(--fg-strong);
|
||||
font-size: 1em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.markdown :global(h1) {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
.markdown :global(h2) {
|
||||
font-size: 1.08em;
|
||||
}
|
||||
.markdown :global(blockquote) {
|
||||
margin: 0 0 8px;
|
||||
padding: 0 10px;
|
||||
border-left: 3px solid var(--border-strong);
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.markdown :global(hr) {
|
||||
margin: 8px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.markdown :global(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.markdown :global(li:has(> input[type="checkbox"])) {
|
||||
margin-left: -20px;
|
||||
list-style: none;
|
||||
}
|
||||
.markdown > :global(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown > :global(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.thought summary,
|
||||
.toolDetails > summary {
|
||||
|
||||
Reference in New Issue
Block a user