Skip to content
110 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The docs service answers every artifacts call: docs can be made, listed, shared, moved, trashed, restored, searched, versioned and edited live in their own rooms, agents read, write and recall them only where their person and everyone in the conversation can, and folio events go out on the bus, while Docs' pages keep working as before.1/**
2 * The doc kind: BlockNote on `XmlFragment("document-store")`, comments in
3 * the `threads` map, Markdown as its text rendition. The same document,
4 * code and behaviour as Docs' pages (src/blocks.ts, src/markdown.ts,
5 * src/edits.ts, src/citations.ts, src/threads.ts), which PageRoom still
6 * uses until Phase 7 retires it; those modules move here then. Pure.
7 */
8import type { DocEditTarget } from "@g1t/contracts";
9import * as Y from "yjs";
10
11import { seed as seedBlocks } from "../../blocks.ts";
12import { chunkMarkdown } from "../../chunks.ts";
13import { bodyCitations } from "../../citations.ts";
14import { applyEdit, findTarget, rangeIds, rangeMarkdown, restoreFrom } from "../../edits.ts";
15import { citationNodes, documentMarkdown, mentionedIds, outline, searchText } from "../../markdown.ts";
16import type { FolioOrigin, KindModel, Rendition } from "../types.ts";
17
18/** Every folio id a text links to, for backlinks (as `linkedFolioIds` in @g1t/contracts; kept here so this module stays pure for Node's tests). */
19export function linkedFolioIds(text: string): string[] {
20 return [...new Set(text.match(/fol_[0-9a-hjkmnp-tv-z]{26}/g) ?? [])];
21}
22
23/** Where BlockNote keeps the document. */
24export const DOC_FRAGMENT = "document-store";
25/** The most a doc's Markdown may be. */
26export const DOC_MAX_TEXT = 512 * 1024;
27/** Lines a card shows. */
28const PREVIEW_LINES = 4;
29
30export function docFragment(doc: Y.Doc): Y.XmlFragment {
31 return doc.getXmlFragment(DOC_FRAGMENT);
32}
33
34/** The first lines of a doc, as plain text, for its card. */
35export function previewLines(markdown: string, max = PREVIEW_LINES): string[] {
36 return searchText(markdown)
37 .split("\n")
38 .map((l) => l.replace(/\s+/g, " ").trim())
39 .filter(Boolean)
40 .slice(0, max)
41 .map((l) => (l.length > 120 ? `${l.slice(0, 119)}…` : l));
42}
43
44/** A target's current Markdown and blocks, or null when it is gone. */
45export function docTarget(doc: Y.Doc, target: DocEditTarget): { markdown: string; block_ids: string[] } | null {
46 const fragment = docFragment(doc);
47 const range = findTarget(fragment, target);
48 if (!range) return null;
49 return { markdown: rangeMarkdown(fragment, range), block_ids: rangeIds(fragment, range) };
50}
51
52/** Where each target is now, for marking open suggestions in the editor (null: gone). */
53export function docTargets(doc: Y.Doc, targets: DocEditTarget[]): (string[] | null)[] {
54 const fragment = docFragment(doc);
55 return targets.map((t) => {
56 const range = findTarget(fragment, t);
57 return range ? rangeIds(fragment, range) : null;
58 });
59}
60
61function describe(target: DocEditTarget): string {
62 switch (target.kind) {
63 case "append":
64 return "Added to the end";
65 case "document":
66 return "Rewrote the doc";
67 case "section":
68 return `Changed the section "${target.heading}"`;
69 case "blocks":
70 return "Changed some blocks";
71 }
72}
73
74export const doc: KindModel = {
75 kind: "doc",
76 isEmpty: (d) => docFragment(d).length === 0,
77 seed(d, init) {
78 seedBlocks(d, docFragment(d), String(init.text ?? "").slice(0, DOC_MAX_TEXT));
79 },
80 render(d): Rendition {
81 const fragment = docFragment(d);
82 const text = documentMarkdown(fragment);
83 return {
84 text,
85 mentions: mentionedIds(fragment).users,
86 links: linkedFolioIds(text),
87 citations: bodyCitations(citationNodes(fragment), text),
88 preview: { kind: "doc", lines: previewLines(text) },
89 };
90 },
91 chunks: (text, title) => chunkMarkdown(text, title),
92 read(d) {
93 const fragment = docFragment(d);
94 return { content: documentMarkdown(fragment), blocks: outline(fragment) };
95 },
96 applyAgentEdit(d, edit, origin: FolioOrigin) {
97 if (edit.kind !== "doc") return { applied: false, summary: "That edit is for another kind of artifact." };
98 const applied = applyEdit(d, docFragment(d), edit.target, String(edit.markdown ?? "").slice(0, DOC_MAX_TEXT), origin);
99 return { applied, summary: applied ? describe(edit.target) : "That part of the doc isn't there." };
100 },
101 restore(d, old, origin) {
102 restoreFrom(d, docFragment(d), docFragment(old), origin);
103 },
104 restoreText(d, text, origin) {
105 applyEdit(d, docFragment(d), { kind: "document" }, text, origin);
106 },
107 validate(d) {
108 return documentMarkdown(docFragment(d)).length > DOC_MAX_TEXT * 2 ? "This doc is too long." : null;
109 },
110};