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
11 changes: 11 additions & 0 deletions apps/web/src/chat/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ test("Chat Markdown admits formatting without document features", () => {
expect(markdown("Planner wrote **strongly**.")).toContain("<strong>strongly</strong>");
});

test("indentation keeps nested lists and code blocks intact", () => {
let nested =
/<li>parent\s*<ul>\s*<li>child<\/li>\s*<li>sibling<\/li>\s*<\/ul>\s*<\/li>\s*<li>next<\/li>/;

expect(markdown("- parent\n - child\n - sibling\n- next")).toMatch(nested);
expect(markdown("- parent\n - child\n - sibling\n- next")).toMatch(nested);
expect(markdown("1. parent\n - child\n - sibling\n2. next")).toMatch(nested);
expect(markdown("```py\nif ready:\n run()\n```")).toContain("if ready:\n run()");
expect(markdown("Ask @chopin about it")).toContain("Ask about it");
});

test("authoritative ranges render internal chips inside ordinary Markdown", () => {
let source = "@chopin Read **#Release: v2 (API)** and [the docs](https://example.com).";
let markup = markdown(source, [reference(source, "#Release: v2 (API)")]);
Expand Down
27 changes: 10 additions & 17 deletions apps/web/src/chat/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,15 @@ function projectedDisplayUnits(source: string, references: Chat.Reference[]): Di
&& !word(source[offset + MENTION.length])
) {
offset += MENTION.length - 1;
// Whitespace is Markdown structure (indentation, code, hard breaks), so
// only the space left doubled by the removed mention is dropped.
let previous = units.at(-1);
if (previous && !previous.protected && /[^\S\n]/.test(previous.value)) {
while (
/[^\S\n]/.test(source[offset + 1] ?? "")
&& !protectedRange(references, offset + 1, offset + 2)
) offset++;
}
continue;
}
units.push({
Expand All @@ -499,23 +508,7 @@ function projectedDisplayUnits(source: string, references: Chat.Reference[]): Di
});
}

let collapsed: DisplayUnit[] = [];
for (let index = 0; index < units.length;) {
let current = units[index]!;
if (current.protected || !/[^\S\n]/.test(current.value)) {
collapsed.push(current);
index++;
continue;
}
let end = index + 1;
while (
end < units.length
&& !units[end]!.protected
&& /[^\S\n]/.test(units[end]!.value)
) end++;
collapsed.push(end - index >= 2 ? { ...current, value: " " } : current);
index = end;
}
let collapsed = units;
while (collapsed.length > 0 && /\s/.test(collapsed[0]!.value)) collapsed.shift();
while (collapsed.length > 0 && /\s/.test(collapsed.at(-1)!.value)) collapsed.pop();

Expand Down
18 changes: 18 additions & 0 deletions e2e/chat-composer.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ test("switching to Chat removes multiple addresses while preserving a document m
await expect(reference).toHaveAttribute("data-document-id", target);
});

test("a sent message keeps its nested list nested", async ({ join }) => {
let chat = chatPane(await join("ana"));
let input = chatInput(chat);
await fillChat(input, "Plan:\n\n- parent\n - child\n - grandchild\n- next");
await input.press("Enter");

let message = chat.locator("[data-chat-markdown]").filter({ hasText: "grandchild" });
let parent = message.getByRole("listitem").filter({ hasText: "parent" }).first();
let child = parent.getByRole("list").getByRole("listitem").filter({ hasText: "child" }).first();
let grandchild = child.getByRole("list").getByRole("listitem").filter({ hasText: "grandchild" });
await expect(grandchild).toHaveCount(1);
await expect(message.getByRole("listitem").filter({ hasText: "next" }).last())
.not.toContainText("child");
let left = async (item: typeof parent) => (await item.boundingBox())!.x;
expect(await left(child)).toBeGreaterThan(await left(parent));
expect(await left(grandchild)).toBeGreaterThan(await left(child));
});

test("context-menu paste undo restores the replaced native selection", async ({ join, page }) => {
let input = chatInput(chatPane(await join("ana")));
await input.fill("hello world");
Expand Down
Loading