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
5 changes: 5 additions & 0 deletions .changeset/readable-comment-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-slide/core": patch
---

Add human-readable `note` attribute to @slide-comment markers so agents can read comments directly without base64url decoding
15 changes: 9 additions & 6 deletions packages/core/skills/apply-comments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ Your job: read those markers, perform the described edits, and delete the marker
## Marker format

```
{/* @slide-comment id="c-<8hex>" ts="<ISO>" text="<base64url(JSON)>" */}
{/* @slide-comment id="c-<8hex>" ts="<ISO>" text="<base64url(JSON)>" note="<escaped-text>" */}
```

- Always sits on its own line as the **first child inside** the JSX element it refers to (i.e. between that element's opening `>` and its other children). The marker is dropped *into* its target, not floated above it.
- `text` is base64url-encoded JSON: `{"note": "...", "hint"?: "..."}`.
- `text` is base64url-encoded JSON: `{"note": "...", "hint"?: "..."}` — the canonical payload.
- **`note` is the human-readable raw comment text** (JSON-string-escaped for safety — `"` → `\"`, `\` → `\\`, `*/` → `*\/`). Read it directly; no decoding needed. The `text` attribute remains for backwards compatibility and for extracting `hint`.
- For **legacy markers** (no `note` attribute), fall back to base64url-decoding `text`.
- Detection regex (authoritative — use exactly this):

```
/\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_\-]+={0,2})"\s*\*\/\}/g
/\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_\-]+={0,2})"(?:\s+note="((?:[^"\\]|\\.)*)")?\s*\*\/\}/g
```

## Procedure
Expand All @@ -33,7 +35,8 @@ Your job: read those markers, perform the described edits, and delete the marker

2. **Read the file and find all markers.**
- Run the regex above against the whole file.
- For each match, base64url-decode `text` and `JSON.parse` it to get `{ note, hint? }`.
- **Preferred path:** If `note` is captured (group 4), `JSON.parse(`"${noteRaw}"`)` to get the comment text directly — no base64url decode needed. Optionally extract `hint` from the base64url `text` payload if the note is ambiguous.
- **Fallback path (legacy markers):** If `note` is not captured, base64url-decode `text` and `JSON.parse` it to get `{ note, hint? }`.
- Record each hit as `{ id, lineIndex (0-based), indent, note, hint }`.
- If there are no markers, tell the user and stop.

Expand All @@ -57,7 +60,7 @@ Your job: read those markers, perform the described edits, and delete the marker
7. **Report.**
- Summarise: `N applied, 0 remaining` plus a one-line description of each change (including the slide id).

## base64url decoding helper
## base64url decoding (legacy markers only)

```js
function decode(s) {
Expand All @@ -66,7 +69,7 @@ function decode(s) {
}
```

You can run this inline via `node -e '...'` if you need to inspect a payload; otherwise just reason about the decoded string.
You can run this inline via `node -e '...'` if you need to inspect a legacy payload. New markers include the `note` attribute which is directly readable — no decode step needed.

## Edge cases

Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/vite/comments-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ describe('parseMarkers', () => {
expect(comments.map((c) => c.note)).toEqual(['one', 'two']);
expect(comments.map((c) => c.line)).toEqual([1, 3]);
});

it('reads note from the human-readable note attribute directly', () => {
const payload = b64urlEncode(JSON.stringify({ note: 'make this red' }));
const source = `{/* @slide-comment id="c-deadbeef" ts="2026-04-25T00:00:00.000Z" text="${payload}" note="make this red" */}`;
const [c] = parseMarkers(source);
expect(c.note).toBe('make this red');
});

it('handles special characters in the note attribute value', () => {
const payload = b64urlEncode(JSON.stringify({ note: 'change "Title" to "Heading" — okay?' }));
const source = `{/* @slide-comment id="c-deadbeef" ts="2026-04-25T00:00:00.000Z" text="${payload}" note="change \\"Title\\" to \\"Heading\\" \\u2014 okay?" */}`;
const [c] = parseMarkers(source);
expect(c.note).toBe('change "Title" to "Heading" — okay?');
});

it('falls back to base64url decode for legacy markers without note attr', () => {
const payload = b64urlEncode(JSON.stringify({ note: 'old format' }));
const source = `{/* @slide-comment id="c-12345678" ts="2026-04-25T00:00:00.000Z" text="${payload}" */}`;
const [c] = parseMarkers(source);
expect(c.note).toBe('old format');
});
});

describe('applyEdit / set-style', () => {
Expand Down
27 changes: 21 additions & 6 deletions packages/core/src/vite/comments-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Connect, Plugin, ViteDevServer } from 'vite';
import { walkAll, walkJsx } from './babel-walk.ts';

const MARKER_RE =
/\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
/\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"(?:\s+note="((?:[^"\\]|\\.)*)")?\s*\*\/\}/g;

const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;

Expand Down Expand Up @@ -83,10 +83,24 @@ export function parseMarkers(source: string): Comment[] {
MARKER_RE.lastIndex = 0;
const m = MARKER_RE.exec(line);
if (!m) continue;
const [, id, ts, textB64] = m;
const [, id, ts, textB64, noteRaw] = m;
try {
const payload = JSON.parse(b64urlDecode(textB64)) as { note: string; hint?: string };
comments.push({ id, line: i + 1, ts, note: payload.note, hint: payload.hint });
if (noteRaw !== undefined) {
// Fast path: raw note attribute is directly readable — no decode needed.
// JSON.parse unwraps the JSON-string-escaped value (handles \", \\, \n, etc.).
const note = JSON.parse(`"${noteRaw}"`);
// Extract hint from base64url payload (best-effort)
let hint: string | undefined;
try {
const payload = JSON.parse(b64urlDecode(textB64)) as { note: string; hint?: string };
hint = payload.hint;
} catch {}
comments.push({ id, line: i + 1, ts, note, hint });
} else {
// Fallback: legacy markers without note attribute
const payload = JSON.parse(b64urlDecode(textB64)) as { note: string; hint?: string };
comments.push({ id, line: i + 1, ts, note: payload.note, hint: payload.hint });
}
} catch {}
}
return comments;
Expand Down Expand Up @@ -1037,7 +1051,8 @@ export function commentsPlugin(opts: CommentsPluginOptions): Plugin {
const id = newId();
const ts = new Date().toISOString();
const payload = b64urlEncode(JSON.stringify({ note: body.text, hint: body.hint }));
const marker = `\n${plan.indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" */}`;
const escapedNote = JSON.stringify(body.text).slice(1, -1);
const marker = `\n${plan.indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" note="${escapedNote}" */}`;

const next = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
await fs.writeFile(file, next, 'utf8');
Expand All @@ -1061,7 +1076,7 @@ export function commentsPlugin(opts: CommentsPluginOptions): Plugin {

const lines = source.split('\n');
const idRe = new RegExp(
`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`,
`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"(?:\\s+note="(?:[^"\\\\]|\\\\.)*")?\\s*\\*\\/\\}`,
);
const hit = lines.findIndex((l) => idRe.test(l));
if (hit === -1) return json(res, 404, { error: 'marker not found' });
Expand Down