mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
* fix(app): find text in assistant messages that streamed in the open chat Chat find failed with the generic "check the host connection" text whenever the oldest hit sat in a response that streamed while the chat was open and had more than one Markdown block. Presentation rendered such a message as one row per block with ids `<messageId>:block:n`, while hydrated history stayed one row with the raw message id, and every message-addressed DOM lookup (find reveal, scroll-to-message, history reveal, the render-cap expansion) only knew the raw id. The reveal step polled for a row that never existed and gave up after five seconds. The defect was the dual rendering, so this removes it: every assistant message is a block group however it arrived, single-paragraph messages included, so a row id is never a message id. Rows carry `data-message-id`, `getStreamItemMessageId` is the one way from a row to its message, and find, scroll-to-message, history reveal and selection copy address messages through it. Chat find force-mounts every row of the selected message under virtualization so counts span the whole message, and pins the chosen occurrence to its row while the scroll settles. Link reference definitions fold into their neighbouring block so history never paints an empty row. Find failures now say what went wrong (connection, history changed, could not show the match) instead of blaming the connection for everything. * fix(app): let the Markdown parser decide which blocks are link definitions A hand-rolled CommonMark regex rejected destinations markdown-it accepts, such as escaped spaces, so those definitions still became their own empty row and the neighbouring paragraph lost its link. The block splitter now parses the block with the same parser: no tokens plus registered references means definitions only. The two new chat find journeys move their mechanics into named helpers so the test bodies read as intent.
128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { splitMarkdownBlocks } from "../split-markdown-blocks";
|
|
|
|
describe("splitMarkdownBlocks", () => {
|
|
it("returns a single block for a single paragraph", () => {
|
|
expect(splitMarkdownBlocks("Hello world")).toEqual(["Hello world"]);
|
|
});
|
|
|
|
it("splits two paragraphs separated by a double newline", () => {
|
|
expect(splitMarkdownBlocks("First paragraph\n\nSecond paragraph")).toEqual([
|
|
"First paragraph",
|
|
"Second paragraph",
|
|
]);
|
|
});
|
|
|
|
it("keeps a fenced code block with internal double newlines as one block", () => {
|
|
expect(splitMarkdownBlocks("```ts\nconst a = 1;\n\nconst b = 2;\n```")).toEqual([
|
|
"```ts\nconst a = 1;\n\nconst b = 2;\n```",
|
|
]);
|
|
});
|
|
|
|
it("does not treat 4-space-indented backticks as a fence", () => {
|
|
expect(splitMarkdownBlocks("Before\n\n ```\n code\n ```\n\nAfter")).toEqual([
|
|
"Before",
|
|
" ```\n code\n ```",
|
|
"After",
|
|
]);
|
|
});
|
|
|
|
it("handles tilde fences", () => {
|
|
expect(splitMarkdownBlocks("Before\n\n~~~\ncode\n~~~\n\nAfter")).toEqual([
|
|
"Before",
|
|
"~~~\ncode\n~~~",
|
|
"After",
|
|
]);
|
|
});
|
|
|
|
it("splits mixed paragraph, code fence, and paragraph content into three blocks", () => {
|
|
expect(
|
|
splitMarkdownBlocks(
|
|
"Intro paragraph\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro paragraph",
|
|
),
|
|
).toEqual(["Intro paragraph", "```ts\nconst a = 1;\n\nconst b = 2;\n```", "Outro paragraph"]);
|
|
});
|
|
|
|
it("keeps everything from an unclosed fence start as one block for streaming content", () => {
|
|
expect(splitMarkdownBlocks("Before fence\n\n```ts\nconst a = 1;\n\nconst b = 2;")).toEqual([
|
|
"Before fence",
|
|
"```ts\nconst a = 1;\n\nconst b = 2;",
|
|
]);
|
|
});
|
|
|
|
it("returns an empty array for empty input", () => {
|
|
expect(splitMarkdownBlocks("")).toEqual([]);
|
|
});
|
|
|
|
it("splits a heading followed by a paragraph into two blocks", () => {
|
|
expect(splitMarkdownBlocks("# Heading\n\nParagraph text")).toEqual([
|
|
"# Heading",
|
|
"Paragraph text",
|
|
]);
|
|
});
|
|
|
|
it("keeps consecutive list items together when there is no double newline", () => {
|
|
expect(splitMarkdownBlocks("- First item\n- Second item\n- Third item")).toEqual([
|
|
"- First item\n- Second item\n- Third item",
|
|
]);
|
|
});
|
|
|
|
it("keeps a loose nested list in its outer list block", () => {
|
|
expect(
|
|
splitMarkdownBlocks(
|
|
"Before\n\n3. Outer three\n\n 7. Inner seven\n 8. Inner eight\n\nAfter",
|
|
),
|
|
).toEqual(["Before", "3. Outer three\n\n 7. Inner seven\n 8. Inner eight", "After"]);
|
|
});
|
|
|
|
it("keeps a link reference definition with the paragraph that uses it", () => {
|
|
expect(splitMarkdownBlocks("See the [docs][d].\n\n[d]: https://example.com")).toEqual([
|
|
"See the [docs][d].\n\n[d]: https://example.com",
|
|
]);
|
|
});
|
|
|
|
it("folds a leading definition-only block into the block below it", () => {
|
|
expect(splitMarkdownBlocks('[d]: https://example.com "Docs"\n\nSee the [docs][d].')).toEqual([
|
|
'[d]: https://example.com "Docs"\n\nSee the [docs][d].',
|
|
]);
|
|
});
|
|
|
|
it("folds several definition lines and several definition blocks into one block", () => {
|
|
expect(
|
|
splitMarkdownBlocks(
|
|
"See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: <https://example.com/b>\n\n[c]: https://example.com/c 'Third'\n\nAfter",
|
|
),
|
|
).toEqual([
|
|
"See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: <https://example.com/b>\n\n[c]: https://example.com/c 'Third'",
|
|
"After",
|
|
]);
|
|
});
|
|
|
|
it("recognizes every destination the renderer accepts, including escaped spaces", () => {
|
|
expect(splitMarkdownBlocks("See [docs].\n\n[docs]: docs\\ folder/readme")).toEqual([
|
|
"See [docs].\n\n[docs]: docs\\ folder/readme",
|
|
]);
|
|
expect(splitMarkdownBlocks("See [docs].\n\n[docs]: <docs folder/readme> 'Title'")).toEqual([
|
|
"See [docs].\n\n[docs]: <docs folder/readme> 'Title'",
|
|
]);
|
|
});
|
|
|
|
it("leaves a definition-only message as its own block", () => {
|
|
expect(splitMarkdownBlocks("[d]: https://example.com")).toEqual(["[d]: https://example.com"]);
|
|
});
|
|
|
|
it("does not fold a paragraph that merely starts with a bracketed link", () => {
|
|
expect(splitMarkdownBlocks("Intro\n\n[Link](https://example.com) and more prose")).toEqual([
|
|
"Intro",
|
|
"[Link](https://example.com) and more prose",
|
|
]);
|
|
});
|
|
|
|
it("treats triple newlines as a split point and filters empty blocks", () => {
|
|
expect(splitMarkdownBlocks("First paragraph\n\n\nSecond paragraph")).toEqual([
|
|
"First paragraph",
|
|
"Second paragraph",
|
|
]);
|
|
});
|
|
});
|