Skip to content

fix(export): save the note that's on screen as PDF, not always the Standard one - #426

Merged
ruzin merged 1 commit into
stenolabs:mainfrom
Optic00:fix/pdf-export-active-report
Aug 2, 2026
Merged

fix(export): save the note that's on screen as PDF, not always the Standard one#426
ruzin merged 1 commit into
stenolabs:mainfrom
Optic00:fix/pdf-export-active-report

Conversation

@Optic00

@Optic00 Optic00 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Found while testing a build: with a generated template report open, "Save
notes as PDF…" exported the Standard structured note
instead of the report on
screen.

Root cause

saveNotesPdf called buildNotesHtml(noteSections) and never passed
activeReport — the builder's input type had no slot for one. That was a
deliberate scoping decision ("the branded template is structured-note
shaped"
), but it left the two adjacent menu actions disagreeing: #318 taught
Copy notes to follow the screen, and the PDF path was never given the same
treatment.

Fix

A report is free-form markdown, so it is serialised with the same renderer the
detail view uses
(react-markdown via renderToStaticMarkup) and dropped into
the branded shell. Reusing the view's renderer is the point: a second markdown
implementation inside notesPdf.ts would drift from what the user is looking at.

  • buildNotesHtml takes an optional pre-rendered report; when present it
    replaces the Standard sections, mirroring buildNotesCopyText's
    short-circuit.
  • The branded shell moved into renderDocument() so both paths share chrome,
    fonts and layout and cannot drift.
  • CSS for the element types markdown brings that the structured note never
    emits: body headings, ordered lists, emphasis, code, quotes, tables. Without
    it they inherited the structured note's list rules and lost their numbering.
  • Also fixed: canExportNotesPdf now counts an open report on its own. A
    transcript-only note (auto-summarise off) with a generated report had the
    action disabled outright — the report could not be exported at all.

The report HTML is injected unescaped by necessity. notesPdf.ts documents why
that is safe: react-markdown emits no raw HTML from its input unless
rehype-raw is enabled (it is not), and the document's CSP
(default-src 'none') remains the second line of defence.

Testing

  • Four unit cases in notesPdf.test.tsthree failed before the fix, so the
    bug was pinned by tests before any code changed.
  • A notes-pdf-export.t1 case driving the real UI: open the seeded report,
    export, then assert the written file carries the rendered report, the
    template name, no Standard sections, and no leaked <think> reasoning.
  • That e2e case was verified to fail without the fix.

Typecheck clean, lint 0 errors, unit 255 node:test + 124 vitest, T1 suite 50/50.


Summary by cubic

Fixes “Save notes as PDF…” exporting the Standard note when a template report is open. The PDF now matches what’s on screen and can export generated reports even when the structured note is empty.

  • Bug Fixes
    • Report markdown is serialized with the same renderer as the detail view (react-markdown via renderToStaticMarkup) so the PDF matches the UI.
    • buildNotesHtml accepts an optional pre-rendered report and replaces the Standard sections when present.
    • Moved the branded shell to shared renderDocument() and added CSS for markdown elements (headings, ordered lists, code, tables) to keep numbering and layout correct.
    • canExportNotesPdf now treats an open report as exportable on its own, enabling export for transcript-only notes.

Written for commit 6ce64de. Summary will update on new commits.

Review in cubic

…andard one

Reported while testing: with a generated template report open, "Save notes as
PDF…" exported the Standard structured note instead of the report on screen.

Root cause: saveNotesPdf called buildNotesHtml(noteSections) and never passed
activeReport — the builder's input type had no slot for one. It was a deliberate
scoping decision ("the branded template is structured-note shaped"), but it left
the two adjacent menu actions disagreeing: stenolabs#318 taught "Copy notes" to follow
the screen and the PDF path was never given the same treatment.

A report is free-form markdown, so the fix serialises it with the SAME renderer
the detail view uses (react-markdown via renderToStaticMarkup) and drops the
result into the branded shell. Reusing the view's renderer is the point — a
second markdown implementation inside notesPdf.ts would drift from what the
user sees.

- buildNotesHtml takes an optional pre-rendered report; when present it REPLACES
  the Standard sections, mirroring buildNotesCopyText's short-circuit.
- The branded shell moved into renderDocument() so both paths share chrome,
  fonts and layout and can't drift.
- CSS for the element types markdown brings that the structured note never
  emits: body headings, ordered lists, emphasis, code, quotes, tables.
- canExportNotesPdf counts an open report on its own, so a transcript-only note
  with a generated report can be exported at all (it could not before).

The report HTML is injected unescaped by necessity; notesPdf.ts documents why
that is safe (react-markdown emits no raw HTML without rehype-raw, which is not
enabled) and the document CSP stays the second line of defence.

Tests: four unit cases in notesPdf.test.ts (three failed before the fix) and a
notes-pdf-export.t1 case driving the real UI — open the seeded report, export,
assert the file carries the RENDERED report, the template name, no Standard
sections, and no leaked reasoning. Verified the e2e case fails without the fix.
@Optic00
Optic00 requested a review from ruzin as a code owner July 25, 2026 04:41

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

You’re at about 97% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/renderer/src/lib/notesPdf.ts">

<violation number="1" location="app/renderer/src/lib/notesPdf.ts:284">
P3: Links in generated reports fall back to the browser's default visited/blue link styling in the PDF, unlike the on-screen report and the paper/ink design. Add a scoped anchor rule so exported report links remain readable and branded.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

border: 1px solid var(--rule); padding: 5px 7px; text-align: left; vertical-align: top;
}
.report th { background: var(--paper-1); font-weight: 600; }
.report hr { border: none; border-top: 1px solid var(--rule); margin: 14px 0; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Links in generated reports fall back to the browser's default visited/blue link styling in the PDF, unlike the on-screen report and the paper/ink design. Add a scoped anchor rule so exported report links remain readable and branded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/renderer/src/lib/notesPdf.ts, line 284:

<comment>Links in generated reports fall back to the browser's default visited/blue link styling in the PDF, unlike the on-screen report and the paper/ink design. Add a scoped anchor rule so exported report links remain readable and branded.</comment>

<file context>
@@ -192,6 +238,50 @@ ${listItems(input.actionItems)}
+    border: 1px solid var(--rule); padding: 5px 7px; text-align: left; vertical-align: top;
+  }
+  .report th { background: var(--paper-1); font-weight: 600; }
+  .report hr { border: none; border-top: 1px solid var(--rule); margin: 14px 0; }
   footer {
     margin-top: 30px; padding: 10px var(--inset) 0; border-top: 1px solid var(--rule);
</file context>
Suggested change
.report hr { border: none; border-top: 1px solid var(--rule); margin: 14px 0; }
.report a { color: var(--ink-900); text-decoration: underline; }
.report hr { border: none; border-top: 1px solid var(--rule); margin: 14px 0; }

@Optic00

Optic00 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Merge order note: this overlaps #448

Flagging an overlap rather than proposing a decision - the order is yours.

This PR and #448 (Share menu) touch the same four files, and two of the overlaps are semantic rather than textual:

1. canExportNotesPdf. This PR makes it report-aware:

activeReport ? Boolean(stripReasoning(activeReport.content).trim()) : hasNotesContent(noteSections)

#448 adds two new entries (Save notes as .md…, Share notes as .md…) that hang off that same predicate, and its body flags the resulting gap as a known one: their markdown builder carries an open report, so on a transcript-only note with a report open, Copy notes works while those two entries are disabled. This PR fixes that gap for free. If it lands first, that caveat on #448 simply disappears instead of being patched separately.

2. buildNotesHtml(noteSections, reportForPdf). #448 introduces a second call site for the branded HTML - the Share notes as PDF… entry, which hands the same document to the macOS share sheet. A naive merge would leave that call site passing the Standard note while Save notes as PDF… passes the report, so saving and sharing the same note would produce different PDFs. It is a one-line fix on the #448 side, but only if someone knows to look.

3. e2e/specs/notes-pdf-export.t1.spec.ts. Both PRs rewrite it. #448 moved the PDF action out of the popover into the Share menu, so every selector in that spec changed there.

Suggested order: this one first, then rebase #448. It is the smaller and older change, it resolves #448's open caveat rather than colliding with it, and the reverse order would mean pulling this PR through #448's menu restructuring. Happy to do the #448 rebase as soon as this lands.

@ruzin
ruzin merged commit d612df1 into stenolabs:main Aug 2, 2026
12 checks passed
Optic00 added a commit to Optic00/stenoai that referenced this pull request Aug 2, 2026
One conflict, in app/package.json: main's stenolabs#440 appended update-error-copy.test.js
to test:unit while this branch appended share-temp.test.js. Both kept — the lists
are additive and both files exist.

The overlap flagged in the PR's merge-order note is now resolved by main itself:
stenolabs#426 landed, so "Save notes as PDF" already follows the note on screen. The
semantic half of that integration is the commit that follows this one.
Optic00 added a commit to Optic00/stenoai that referenced this pull request Aug 2, 2026
Integrating main after stenolabs#426 landed. Git merged both sides cleanly, and that is
the problem: stenolabs#426 taught "Save notes as PDF" to export whichever note is on
screen, but "Share notes as PDF" was written on this branch in parallel and
still called buildNotesHtml(noteSections). Merged, the two adjacent entries in
the same menu disagreed about the same document - with a template report open,
sharing sent the Standard structured note. That is the exact bug stenolabs#426 was filed
for, one entry lower.

Both PDF surfaces now go through one buildNotesPdfHtml(), so they cannot drift
apart again. It is a function rather than a memo for the reason stenolabs#426 gives: the
branded shell is ~45KB with its base64 font, so it is built on click.

Also repoints notes-pdf-export.t1's report case at the Share menu. This branch
moves the save entries out of the "..." menu, so the spec's 'More options' click
opened a menu that no longer holds "Save notes as PDF" - it timed out on the
merge, not on a product fault. Its sibling case in the same file already used
the Share fixture.

Tests: a share-menu.t1 case asserting the SHARED payload carries the rendered
report, the template name, no Standard sections and no leaked reasoning. It
needs the whole payload, not the log's 200-char head - a branded PDF's first 200
chars are doctype and font CSS - so the share mock gains
STENOAI_E2E_SHARE_PAYLOAD_PATH, the seam STENOAI_E2E_EXPORT_PATH already gives
the save path. Verified the case fails without the fix (the shared HTML has no
report content) and passes with it; full T1 suite green, 68 passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants