mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
* feat(app): add pull request panel context actions Attach pull request comments, reviews, and failed check details to chat context. The PR panel now lives in its own module so the activity, query, and context policies have one home. * fix(server): mark capped check details as truncated * fix(app): preserve concurrent PR panel attachments * fix(server): preserve annotation truncation state * fix(app): keep matrix check attachments distinct * fix(app): streamline pull request context attachments * fix(app): simplify pull request feedback rendering * feat(app): group pull request review threads * fix(app): tidy pull request panel boundaries * fix: use worktree cwd for dev services * fix(app): keep git query keys UI-free * feat(app): collapse resolved comments and add chat actions to headers * fix: address PR feedback and Playwright flakes * fix(app): hide check chat action without attachment scope * fix(server): dedupe inline review timeline comments
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import type { MarkdownDisplayPart, MarkdownInlineImagePart } from "./html-ish";
|
|
|
|
export type MarkdownPartGroup =
|
|
| { kind: "part"; part: MarkdownDisplayPart }
|
|
| { kind: "imageText"; images: MarkdownInlineImagePart[]; lead: string; rest: string };
|
|
|
|
/**
|
|
* Pairs a run of inline images that flow with text (small status images at the
|
|
* start of a line, GitHub-style) with the lead paragraph of the following
|
|
* markdown part, so the renderer can lay them out side by side instead of
|
|
* stacking the images as their own blocks.
|
|
*
|
|
* Between consecutive flowing images the parser may emit whitespace-only
|
|
* markdown parts — these are consumed and discarded when building the group.
|
|
*/
|
|
export function groupMarkdownParts(parts: readonly MarkdownDisplayPart[]): MarkdownPartGroup[] {
|
|
const groups: MarkdownPartGroup[] = [];
|
|
let index = 0;
|
|
|
|
while (index < parts.length) {
|
|
const part = parts[index];
|
|
|
|
if (part.kind === "inlineImage" && part.flowsWithText) {
|
|
// Collect the maximal run of flowing images, skipping whitespace-only markdown between them.
|
|
const images: MarkdownInlineImagePart[] = [part];
|
|
let lookahead = index + 1;
|
|
|
|
while (lookahead < parts.length) {
|
|
const candidate = parts[lookahead];
|
|
if (candidate === undefined) {
|
|
break;
|
|
}
|
|
if (isWhitespaceOnlyMarkdown(candidate)) {
|
|
lookahead += 1;
|
|
continue;
|
|
}
|
|
if (candidate.kind === "inlineImage" && candidate.flowsWithText) {
|
|
images.push(candidate);
|
|
lookahead += 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const trailing = parts[lookahead];
|
|
if (trailing?.kind === "markdown") {
|
|
const { lead, rest } = splitLeadParagraph(trailing.text);
|
|
if (lead) {
|
|
groups.push({ kind: "imageText", images, lead, rest });
|
|
index = lookahead + 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// No usable lead — emit the images (and any whitespace parts) as plain parts.
|
|
for (let i = index; i < lookahead; i += 1) {
|
|
const p = parts[i];
|
|
if (p !== undefined) {
|
|
groups.push({ kind: "part", part: p });
|
|
}
|
|
}
|
|
index = lookahead;
|
|
continue;
|
|
}
|
|
|
|
groups.push({ kind: "part", part });
|
|
index += 1;
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
function isWhitespaceOnlyMarkdown(part: MarkdownDisplayPart): boolean {
|
|
return part.kind === "markdown" && part.text.trim() === "";
|
|
}
|
|
|
|
function splitLeadParagraph(text: string): { lead: string; rest: string } {
|
|
if (/^[ \t]*\r?\n/.test(text)) {
|
|
return { lead: "", rest: text };
|
|
}
|
|
const boundary = /\r?\n[ \t]*\r?\n/.exec(text);
|
|
if (!boundary) {
|
|
return { lead: text.trim(), rest: "" };
|
|
}
|
|
return {
|
|
lead: text.slice(0, boundary.index).trim(),
|
|
rest: text.slice(boundary.index + boundary[0].length),
|
|
};
|
|
}
|