fix(web): wrap long lines in markdown chat messages (#469)
This commit is contained in:
@@ -600,6 +600,7 @@
|
||||
font-size: 13px;
|
||||
line-height: 1.625;
|
||||
color: var(--foreground);
|
||||
overflow-wrap: anywhere;
|
||||
font-family:
|
||||
-apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji";
|
||||
@@ -637,6 +638,11 @@
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.25rem;
|
||||
@@ -1345,7 +1351,8 @@
|
||||
.shiki-code-block pre.shiki {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: var(--font-mono-size);
|
||||
line-height: 1.5;
|
||||
background-color: var(--background) !important;
|
||||
|
||||
@@ -40,7 +40,7 @@ export function CodeMirrorCodeBlock({ children, className }: CodeBlockProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group/code-block my-4 w-fit max-w-full min-w-[50%]">
|
||||
<div className="relative group/code-block my-4 max-w-full">
|
||||
{/* Copy button */}
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function MonacoCodeBlock({ children, className }: MonacoCodeBlockProps) {
|
||||
const height = Math.min(Math.max(lineCount * 16 + 8, 32), 500);
|
||||
|
||||
return (
|
||||
<div className="relative group/code-block my-4 w-fit max-w-full min-w-[50%]">
|
||||
<div className="relative group/code-block my-4 max-w-full">
|
||||
<button
|
||||
onClick={() => copy(code)}
|
||||
className={cn(
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ShikiCodeBlock({ children, className }: ShikiCodeBlockProps) {
|
||||
}, [code, lang, isDark]);
|
||||
|
||||
return (
|
||||
<div className="relative group/code-block my-4 w-fit max-w-full min-w-[50%]">
|
||||
<div className="relative group/code-block my-4 max-w-full">
|
||||
<button
|
||||
onClick={() => copy(code)}
|
||||
className={cn(
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { test, expect } from "../fixtures/test-base";
|
||||
import { KanbanPage } from "../pages/kanban-page";
|
||||
import { SessionPage } from "../pages/session-page";
|
||||
|
||||
const LONG_WORD = "abcdefghij".repeat(30); // 300 chars, no spaces
|
||||
|
||||
/** Create a task with a mock agent message containing the given text. */
|
||||
async function createTaskWithMessage(
|
||||
testPage: import("@playwright/test").Page,
|
||||
apiClient: import("../helpers/api-client").ApiClient,
|
||||
seedData: import("../fixtures/test-base").SeedData,
|
||||
title: string,
|
||||
messageText: string,
|
||||
): Promise<SessionPage> {
|
||||
await apiClient.createTaskWithAgent(seedData.workspaceId, title, seedData.agentProfileId, {
|
||||
description: `e2e:message("${messageText}")`,
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
});
|
||||
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
|
||||
const card = kanban.taskCardByTitle(title);
|
||||
await expect(card).toBeVisible({ timeout: 30_000 });
|
||||
await card.click();
|
||||
await expect(testPage).toHaveURL(/\/s\//, { timeout: 15_000 });
|
||||
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Assert no .markdown-body element overflows horizontally. */
|
||||
async function expectNoMarkdownOverflow(testPage: import("@playwright/test").Page) {
|
||||
const overflows = await testPage.evaluate(() => {
|
||||
const els = document.querySelectorAll(".markdown-body");
|
||||
return Array.from(els).some((el) => el.scrollWidth > el.clientWidth + 1);
|
||||
});
|
||||
expect(overflows).toBe(false);
|
||||
}
|
||||
|
||||
test.describe("Markdown text wrapping", () => {
|
||||
test("long plain text wraps within the chat message container", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const session = await createTaskWithMessage(
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
"Wrap Plain Text",
|
||||
`Here is a finding: ${LONG_WORD} - end of finding`,
|
||||
);
|
||||
|
||||
await expect(session.chat.getByText("end of finding").last()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await expectNoMarkdownOverflow(testPage);
|
||||
});
|
||||
|
||||
test("long inline code wraps within the chat message container", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const session = await createTaskWithMessage(
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
"Wrap Inline Code",
|
||||
`Check this path: \`${LONG_WORD}\` for details`,
|
||||
);
|
||||
|
||||
await expect(session.chat.getByText("for details").last()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await expectNoMarkdownOverflow(testPage);
|
||||
});
|
||||
|
||||
test("long lines in code blocks do not overflow the message container", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
// Fenced code block with a long line — should be contained via
|
||||
// horizontal scroll (shiki) or line wrapping (codemirror)
|
||||
const session = await createTaskWithMessage(
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
"Wrap Code Block",
|
||||
`Code block:\\n\`\`\`\\nconst x = "${LONG_WORD}";\\n\`\`\`\\nAfter code block`,
|
||||
);
|
||||
|
||||
await expect(session.chat.getByText("After code block").last()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await expectNoMarkdownOverflow(testPage);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user