Skip to content
Open
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
4 changes: 4 additions & 0 deletions official-plugins/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ provider, and directive are all Docs.
`.markdown` files, so it can be selected under Settings → File openers or
chosen from a file link's Open with menu. Workspace and absolute host files
retain compare-and-swap saves even when they are outside a Docs vault.
- **YAML frontmatter:** fenced frontmatter supplies the document title when it
has a string `title`, stays out of the rendered body and search preview, and
is preserved byte-for-byte when the rich editor saves body changes. Docs
also leaves frontmatter-managed filenames unchanged on save.
- **Tables:** GitHub-flavored Markdown tables render as editable cells. Use Tab
and Shift+Tab to move between cells (Tab from the final cell adds a row), and
drag column boundaries to resize them. Saves remain portable Markdown.
Expand Down
71 changes: 71 additions & 0 deletions official-plugins/docs/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ describe("Docs nav panel", () => {
});
});

it("keeps the vault loading state clear of top-left app chrome", () => {
const slot = renderSlot(
app.navPanels[0]!,
{ subPath: "" },
{ rpc: { listNotes: () => new Promise(() => undefined) } },
);

const loading = slot.getByText("Loading vaults…");
expect(loading.className.split(/\s+/)).toEqual(
expect.arrayContaining([
"flex",
"flex-1",
"items-center",
"justify-center",
]),
);
});

it("moves the sidebar toggle into the shared panel header", async () => {
const panel = app.navPanels[0]!;
const slot = renderSlot(
Expand Down Expand Up @@ -429,6 +447,59 @@ describe("Docs nav panel", () => {
});
});

it("hides and preserves YAML frontmatter when editing a document", async () => {
const frontmatter = [
"---\r\n",
"title: Wiki page\r\n",
"type: knowledge\r\n",
"---\r\n",
].join("");
const saveNote = vi.fn((_input: unknown) => ({
outcome: "written",
sha256: "next-sha",
}));
const slot = renderSlot(
app.navPanels[0]!,
{ subPath: "personal/wiki-page.md" },
{
rpc: {
listNotes: () =>
listNotesResult([
{
path: "wiki-page.md",
title: "Wiki page",
preview: "Original body.",
modifiedAtMs: 1,
},
]),
readNote: () => ({
content: `${frontmatter}# Wiki page\r\n\r\nOriginal body.`,
sha256: "sha",
}),
preparePreview: () => preview,
renameToTitle: () => ({ path: "wiki-page.md" }),
saveNote,
},
},
);

const body = await slot.findByText("Original body.");
const editor = slot.container.querySelector(".tiptap");
expect(editor?.textContent).not.toContain("type: knowledge");
expect(editor?.querySelector("hr")).toBeNull();

body.textContent = "Edited body.";
fireEvent.input(body);
await waitFor(() => expect(saveNote).toHaveBeenCalled(), {
timeout: 2_000,
});
expect(saveNote.mock.calls.at(-1)?.[0]).toMatchObject({
content: expect.stringMatching(
/^---\r\ntitle: Wiki page\r\ntype: knowledge\r\n---\r\n# Wiki page\n\nEdited body\./,
),
});
});

it("renders nested folders, images, and sandboxed HTML directives", async () => {
const slot = renderSlot(
app.navPanels[0]!,
Expand Down
12 changes: 10 additions & 2 deletions official-plugins/docs/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
type PluginThreadPanelProps,
} from "@bb/plugin-sdk/app";
import type { docsRpcContract } from "./server.js";
import { parseMarkdownDocument } from "./markdown-document.js";
import {
Editor,
Extension,
Expand Down Expand Up @@ -503,6 +504,7 @@ function TiptapEditor({
useEffect(() => {
ensureEditorStyles();
if (!rootRef.current) return;
const markdownDocument = parseMarkdownDocument(initialValue);
let editor: Editor;
const upload = async (file: File) => {
if (!file.type.startsWith("image/")) return false;
Expand Down Expand Up @@ -539,7 +541,7 @@ function TiptapEditor({
linkify: true,
}),
],
content: displayMarkdown(initialValue, previewBaseUrl, notePath),
content: displayMarkdown(markdownDocument.body, previewBaseUrl, notePath),
autofocus: "end",
editorProps: {
handlePaste(_view, event) {
Expand All @@ -562,6 +564,7 @@ function TiptapEditor({
},
});
const getMarkdown = () =>
markdownDocument.frontmatter +
storedMarkdown(
editor.storage.markdown.getMarkdown(),
previewBaseUrl,
Expand Down Expand Up @@ -1941,7 +1944,12 @@ function NotesPanel({ subPath }: PluginNavPanelProps) {

if (!data || !activeVaultId)
return (
<div className="p-6 text-sm text-muted-foreground">Loading vaults…</div>
<div
className="flex min-h-0 flex-1 items-center justify-center p-6 text-sm text-muted-foreground"
role="status"
>
Loading vaults…
</div>
);

const selectedFolder = filePath ? dirname(filePath) : "";
Expand Down
66 changes: 66 additions & 0 deletions official-plugins/docs/markdown-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { parse } from "yaml";

export interface MarkdownDocument {
frontmatter: string;
body: string;
title: string | null;
}

interface MarkdownLine {
start: number;
nextStart: number;
text: string;
}

function readLine(content: string, start: number): MarkdownLine {
const newline = content.indexOf("\n", start);
const end = newline === -1 ? content.length : newline;
const textEnd = end > start && content[end - 1] === "\r" ? end - 1 : end;
return {
start,
nextStart: newline === -1 ? content.length : newline + 1,
text: content.slice(start, textEnd),
};
}

function parseFrontmatterTitle(source: string): string | null {
try {
const metadata: unknown = parse(source, { maxAliasCount: 20 });
if (
typeof metadata !== "object" ||
metadata === null ||
Array.isArray(metadata)
) {
return null;
}
const title = (metadata as Record<string, unknown>).title;
return typeof title === "string" && title.trim() ? title.trim() : null;
} catch {
return null;
}
}

export function parseMarkdownDocument(content: string): MarkdownDocument {
const firstLineStart = content.startsWith("\uFEFF") ? 1 : 0;
const firstLine = readLine(content, firstLineStart);
if (firstLine.text !== "---") {
return { frontmatter: "", body: content, title: null };
}

let lineStart = firstLine.nextStart;
while (lineStart < content.length) {
const line = readLine(content, lineStart);
if (line.text === "---" || line.text === "...") {
const frontmatterSource = content.slice(firstLine.nextStart, line.start);
return {
frontmatter: content.slice(0, line.nextStart),
body: content.slice(line.nextStart),
title: parseFrontmatterTitle(frontmatterSource),
};
}
if (line.nextStart === lineStart) break;
lineStart = line.nextStart;
}

return { frontmatter: "", body: content, title: null };
}
3 changes: 2 additions & 1 deletion official-plugins/docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bb-plugin-simple-notes",
"version": "0.2.0",
"version": "0.2.2",
"description": "Create and edit Markdown documents across local and connected-host vaults.",
"private": true,
"license": "MIT",
Expand Down Expand Up @@ -80,6 +80,7 @@
"@tiptap/pm": "^2.27.2",
"@tiptap/starter-kit": "^2.27.2",
"tiptap-markdown": "^0.8.10",
"yaml": "^2.8.2",
"zod": "^4.3.6"
}
}
84 changes: 84 additions & 0 deletions official-plugins/docs/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,70 @@ describe("Docs mention provider", () => {
]);
});

it("ignores YAML frontmatter when deriving note titles and previews", async () => {
const { harness } = await loadNotebook({
"defensibility.md": [
"---",
"type: knowledge",
"summary: Product strategy metadata",
"---",
"# AI application-layer defensibility",
"",
"Durable product strategy.",
].join("\n"),
});
const provider = harness.registrations.mentionProviders[0]!;

await expect(
provider.search({
trigger: "@",
query: "defensibility",
projectId: null,
threadId: null,
}),
).resolves.toEqual([
{
id: "personal:defensibility.md",
title: "AI application-layer defensibility",
subtitle: "Personal · Durable product strategy.",
icon: "FileText",
},
]);
});

it("uses a YAML frontmatter title when the body has no document heading", async () => {
const { harness } = await loadNotebook({
"california-report.md": [
"---",
'title: "6th Annual Report: Evaluation of California\'s Caregiver Services"',
"type: knowledge",
"---",
"## Key findings used in wiki",
"",
"CareNav assessments identify unmet caregiver needs.",
].join("\n"),
});
const provider = harness.registrations.mentionProviders[0]!;

await expect(
provider.search({
trigger: "@",
query: "california",
projectId: null,
threadId: null,
}),
).resolves.toEqual([
{
id: "personal:california-report.md",
title:
"6th Annual Report: Evaluation of California's Caregiver Services",
subtitle:
"Personal · CareNav assessments identify unmet caregiver needs.",
icon: "FileText",
},
]);
});

it("resolves the note's current content at send time", async () => {
const { harness } = await loadNotebook({
"ideas.md": "# Fresh Ideas\n\nBuild the mention flow.",
Expand Down Expand Up @@ -422,6 +486,26 @@ describe("Docs vault operations", () => {
).rejects.toThrow('unknown setting "directory"');
});

it("keeps a frontmatter-managed document at its existing path", async () => {
const { harness } = await loadNotebook({
"stable-wiki-slug.md": [
"---",
"title: A different display title",
"type: knowledge",
"---",
"# A different display title",
].join("\n"),
});

await expect(
harness.callRpc("renameToTitle", {
vaultId: "personal",
path: "stable-wiki-slug.md",
}),
).resolves.toEqual({ path: "stable-wiki-slug.md" });
expect(harness.sdk.callsTo("files.move")).toEqual([]);
});

it("registers the agent-discoverable Docs CLI", async () => {
const { harness } = await loadNotebook({ "plan.md": "# Plan" });
expect(harness.registrations.cli).toMatchObject({
Expand Down
Loading