/** * lib/scaffolding.ts * * Document scaffolding helpers for the showcase demo. * * The scaffold is what lets 3 personas type concurrently without * stepping on each other's paragraphs: one persona pre-creates a * Heading 2 + N empty
"slots" per section, then agents target * specific slots by (heading, index) instead of blindly pressing Enter. * Pressing Enter during the parallel phase is what causes agent * overlap (one persona's trailing text gets absorbed into another's * code fence). */ import type { Page } from "playwright"; import { humanType, pause } from "../human-typing.js"; import { humanMove } from "./mouse-cursor.js"; export interface ScaffoldSection { heading: string; /** Number of empty
slots to pre-create below the heading. */ slots: number; } /** * Build the article scaffold in one persona pass: each section gets a * Heading 2 and `slots` pre-created empty paragraphs underneath. * * Example output for [{ heading: "The recipe", slots: 3 }, ...]: *
; each subsequent Enter appends another empty
. // For a non-last section we also need ONE extra trailing
that
// will be consumed by the next heading's "## " input rule, otherwise
// we'd lose the section's last slot. The last section doesn't need
// that extra trailing paragraph.
const enters = isLast ? section.slots : section.slots + 1;
for (let i = 0; i < enters; i++) {
await page.keyboard.press("Enter");
await pause(40, 80);
}
}
}
export async function waitForHeading2(
page: Page,
text: string,
timeout = 60_000,
): Promise directly after
* the heading whose text matches `headingText`. Used so each persona can
* type into its OWN section concurrently without stepping on the others.
*
* Returns false if the heading or following paragraph cannot be found.
*/
export async function focusParagraphAfterHeading(
page: Page,
headingText: string,
): Promise -> -> math block, etc.).
*
* Uses synthesized mousedown+mouseup+click events on the target's
* bounding rect - ProseMirror listens to these natively and updates its
* internal selection state. Also sets the DOM Range as a belt-and-braces
* fallback. Single page.evaluate call: no shared CSS marker class to
* race on between concurrent personas.
*/
export async function focusNthParagraphInSection(
page: Page,
headingText: string,
slotIndex: number,
): Promise` code
* block can still be reshaping the doc during the drag, which
* shifts the Intuition paragraph out from under the measured
* rects and the selection sweep picks up the heading above.
*
* The markers below are the LAST things each peer writes:
* - Bob: `.ProseMirror math-display` node (the attention
* formula in The recipe[1])
* - Carol: `.ProseMirror pre code` with "import torch" as text
* (the eight-line PyTorch snippet)
*
* Both are idempotent: if the peer finished earlier, the locator
* resolves immediately. The extra `layoutSettleMs` beat after
* resolution lets Tiptap's decoration plugin repaint so
* `getClientRects()` returns the final geometry.
*/
export async function waitForPeersSettled(
page: Page,
opts: {
timeout?: number;
layoutSettleMs?: number;
} = {},
): Promise for code fences,
*