feat(plugins): add ChatGPT conversation export - #921
Conversation
Add opt-in Markdown, JSON, PDF, selection, and temporary-chat export flows. Co-authored-by: Codex <codex@users.noreply.github.com>
📝 WalkthroughWalkthroughThe PR adds a ChatGPT export plugin for Markdown, JSON, and PDF. It collects virtualized conversations, supports message selection and temporary-chat handoff, adds localized UI, and introduces ChatGPT-specific PDF rendering. ChangesChatGPT export
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ChatGPT
participant ChatGptExportPlugin
participant conversation.ts
participant exporter.ts
participant PDFPrintService
ChatGPT->>ChatGptExportPlugin: Open export menu
ChatGptExportPlugin->>conversation.ts: Collect conversation messages
conversation.ts-->>ChatGptExportPlugin: Return snapshots and metadata
ChatGptExportPlugin->>exporter.ts: Submit export format
exporter.ts->>PDFPrintService: Export PDF with ChatGPT appearance
PDFPrintService-->>ChatGptExportPlugin: Return generated filename
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (1)
src/features/export/services/__tests__/PDFPrintService.test.ts (1)
95-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the title assertion discriminating, and reduce CSS string coupling.
Two points:
- Line 96 and Line 105 use the same value,
Export test. The ChatGPT branch atgetPrintDialogTitleprefersmetadata.titleover the page title. With identical values the assertion at Line 116 passes for either precedence order. Use distinct values to lock the intended behavior.- Lines 117-123 assert exact CSS declaration text, including spacing. A reformat of
buildChatGptPrintStylesbreaks these tests without any behavior change. Assert on the scoped selector plus a small number of load-bearing declarations, or assert on computed markup instead.💚 Proposed change for point 1
- document.title = 'Export test'; + document.title = 'Stale page title'; window.print = vi.fn();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/export/services/__tests__/PDFPrintService.test.ts` around lines 95 - 124, Update the ChatGPT layout test around PDFPrintService.export to use distinct page and metadata titles, then assert the resulting document.title reflects the metadata-title precedence. Replace exact CSS string/spacing assertions with less brittle checks that verify the scoped selector and only essential load-bearing declarations or computed markup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/export/services/PDFPrintService.ts`:
- Around line 534-543: Update isMeaningfulConversationTitle to reject the
literal title “ChatGPT” alongside the existing Gemini exclusions, so
getConversationTitle cannot use document.title as a meaningful fallback and
empty metadata titles resolve to the intended neutral fallback.
- Around line 1262-1283: Update the generated code-block styles in the PDF print
template around the `.gv-print-turn-text pre` rule to preserve readable contrast
when print background graphics are disabled. Keep the existing dark styling by
default, and add a `forced-colors` or no-background safeguard that switches the
code surface and text to a light, bordered presentation without relying on the
dark background.
In `@src/features/plugins/builtin/chatgptExport/conversation.ts`:
- Around line 256-275: Update the collection loop around the conversation
collector to merge mounted messages after the final scroll, including when
maxSteps is reached. If collection remains above the bottom once the step limit
is exhausted, return an explicit incomplete-collection error instead of
exporting collected messages; preserve normal completion behavior at the bottom.
Add a regression test using a small maxSteps value that verifies the error and
final-scroll merge.
- Around line 118-132: Update readStableId so fallback identities never use the
scan-local order, which is unstable across virtualized windows. Prefer a stable
ChatGPT identifier for every supported schema; when none exists, return a
non-deduplicating identity or otherwise ensure the message is retained rather
than merged in collected. Add coverage for window swaps containing repeated
role/text messages.
In `@src/features/plugins/builtin/chatgptExport/exporter.test.ts`:
- Around line 18-89: Add a JSON export test to the ChatGPT export formats suite
using exportChatGptConversation with format 'json'; stub the Blob/download path,
parse the generated payload, and assert the schema preserves selected state,
message IDs, roles, and content. Reuse collectMountedChatGptMessages and the
existing metadata setup, while keeping the current Markdown and PDF coverage
unchanged.
In `@src/features/plugins/builtin/chatgptExport/i18n.ts`:
- Line 93: Update the tempRegret and tempTitle translation strings in the chat
export i18n definitions to describe saving and transferring the temporary chat
to a normal chat, replacing the regret wording. Keep both labels aligned and
readable across themes.
In `@src/features/plugins/builtin/chatgptExport/index.ts`:
- Around line 67-70: The whole-conversation icon path in the paths mapping
should use coordinates that keep the downward arrow within the SVG viewport.
Update the whole entry’s arrow segment while preserving its existing vertical
stem and bottom bar; leave the selected and temporary paths unchanged.
In `@src/features/plugins/builtin/chatgptExport/styles.ts`:
- Around line 284-315: Complete the dark-theme selector coverage in the styles
for .gv-chatgpt-export-menu-item:hover,
.gv-chatgpt-export-menu-item:focus-visible, .gv-chatgpt-export-btn:hover,
.gv-chatgpt-export-format-option:has(input:checked), and
.gv-chatgpt-export-button--floating by adding the missing body.dark variants.
Preserve the existing light-theme rules and dark colors for html.dark,
body.dark, and [data-theme='dark'].
In `@src/features/plugins/builtin/chatgptExport/tempHandoff.test.ts`:
- Around line 19-53: Extend the temporary chat handoff tests to cover
handoffTemporaryChat and resumePendingHandoff lifecycle behavior, pending-entry
expiry, and attachment-delivery failure. Verify failed attachment delivery
preserves the existing composer draft instead of clearing it through
input.replaceChildren(), while retaining the current route, transcript, and
attachment assertions. Run bun run test, bun run lint, and bun run typecheck.
In `@src/features/plugins/builtin/chatgptExport/tempHandoff.ts`:
- Around line 182-187: Update the attachment fallback in tempHandoff around
dispatchPaste so a false result never calls input.replaceChildren or overwrites
the existing composer draft. Instead, return a delivery failure and display the
appropriate error while preserving the user's current content; keep the
successful paste path unchanged.
- Around line 106-110: Update writePending and resumePendingHandoff to persist a
stable active ChatGPT account identity in PendingHandoff and compare it before
delivery; clear the pending handoff and skip delivery when identities differ.
Also adjust the attachment-paste fallback in deliver so it preserves existing
composer content or requires confirmation before replacing it, rather than
clearing the draft automatically.
In `@src/features/plugins/builtin/chatgptExport/ui.ts`:
- Around line 45-83: Update the dialog creation and lifecycle around the
existing overlay/dialog helpers to add an accessible name via the title element,
then reuse the established gv-pm-* modal precedent for Tab focus trapping,
Escape/outside dismissal, focus restoration to the trigger, teardown, and theme
behavior. Add keyboard coverage verifying Tab cycling within the modal and
restoring focus after close.
---
Nitpick comments:
In `@src/features/export/services/__tests__/PDFPrintService.test.ts`:
- Around line 95-124: Update the ChatGPT layout test around
PDFPrintService.export to use distinct page and metadata titles, then assert the
resulting document.title reflects the metadata-title precedence. Replace exact
CSS string/spacing assertions with less brittle checks that verify the scoped
selector and only essential load-bearing declarations or computed markup
behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b7469713-a40e-4214-a43b-f3ca24e62781
📒 Files selected for processing (19)
src/features/export/services/PDFPrintService.tssrc/features/export/services/__tests__/PDFPrintService.test.tssrc/features/plugins/builtin/builtin.test.tssrc/features/plugins/builtin/chatgptExport/conversation.test.tssrc/features/plugins/builtin/chatgptExport/conversation.tssrc/features/plugins/builtin/chatgptExport/exporter.test.tssrc/features/plugins/builtin/chatgptExport/exporter.tssrc/features/plugins/builtin/chatgptExport/i18n.tssrc/features/plugins/builtin/chatgptExport/index.test.tssrc/features/plugins/builtin/chatgptExport/index.tssrc/features/plugins/builtin/chatgptExport/selectionMode.test.tssrc/features/plugins/builtin/chatgptExport/selectionMode.tssrc/features/plugins/builtin/chatgptExport/styles.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.test.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.tssrc/features/plugins/builtin/chatgptExport/ui.test.tssrc/features/plugins/builtin/chatgptExport/ui.tssrc/features/plugins/builtin/index.tssrc/pages/content/pluginNativeRegistration.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2bb4b8e4ef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function getChatGptExportCopy(): ChatGptExportCopy { | ||
| return /^zh(?:-|_|$)/i.test(navigator.language) ? ZH : EN; |
There was a problem hiding this comment.
Localize the export UI for all supported locales
This chooses copy from navigator.language rather than Voyager's stored language and only provides English and Chinese dictionaries; the manifest similarly supplies only zh and zh_TW. Users selecting ar, es, fr, ja, ko, pt, or ru therefore see English plugin metadata and controls, and even a Chinese Voyager setting is ignored when the browser language differs. Load the configured language and provide copy for the complete supported locale set.
AGENTS.md reference: AGENTS.md:L82-L82
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The preference bug is fixed in 1f0e159: runtime copy follows the stored Voyager language. This plugin intentionally ships authored English and Chinese copy with English fallback; I am not adding unreviewed machine translations for the other locales in this fix.
Co-authored-by: Codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 655ea91a99
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: Codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/plugins/builtin/chatgptExport/tempHandoff.test.ts`:
- Around line 122-139: Add a test for the immediate account-change path in
handoffTemporaryChat, covering navigation from /u/0/ to /u/1/. Assert it returns
account-mismatch, preserves the existing composer draft, and removes the pending
state; then run bun run test, bun run lint, and bun run typecheck.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b363876c-3abf-4109-ab80-962d65fdc8de
📒 Files selected for processing (14)
src/features/export/services/PDFPrintService.tssrc/features/export/services/__tests__/PDFPrintService.test.tssrc/features/plugins/builtin/chatgptExport/conversation.test.tssrc/features/plugins/builtin/chatgptExport/conversation.tssrc/features/plugins/builtin/chatgptExport/exporter.test.tssrc/features/plugins/builtin/chatgptExport/exporter.tssrc/features/plugins/builtin/chatgptExport/i18n.tssrc/features/plugins/builtin/chatgptExport/index.test.tssrc/features/plugins/builtin/chatgptExport/index.tssrc/features/plugins/builtin/chatgptExport/styles.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.test.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.tssrc/features/plugins/builtin/chatgptExport/ui.test.tssrc/features/plugins/builtin/chatgptExport/ui.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- src/features/plugins/builtin/chatgptExport/i18n.ts
- src/features/plugins/builtin/chatgptExport/index.test.ts
- src/features/export/services/tests/PDFPrintService.test.ts
- src/features/plugins/builtin/chatgptExport/ui.test.ts
- src/features/plugins/builtin/chatgptExport/ui.ts
- src/features/plugins/builtin/chatgptExport/conversation.ts
- src/features/export/services/PDFPrintService.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f0e159199
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: Codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 096a5f8c69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/export/services/DOMContentExtractor.ts`:
- Around line 82-87: Update extractAssistantContent and its processNodes flow to
include direct text nodes alongside child elements, preserving their DOM order
when combining ordinary user-content such as text surrounding links. Ensure
mixed content retains both surrounding text and extracted element output, and
add a regression case covering text before and after an anchor.
- Around line 82-87: Update the empty-content fallback in DOMContentExtractor so
standard ChatGPT file-pill metadata is extracted and merged into
ExtractedContent.attachments instead of relying only on
extractAssistantContent(element). Preserve the existing text and HTML fallback
behavior, and add a regression test covering standard file pills in the JSON
export.
- Around line 610-624: Update both anchor-handling branches, including the
standalone-link block and the corresponding branch around the other reported
location, to serialize anchor children through the existing inline extractor
instead of using link.textContent. Wrap the extracted HTML and Markdown in the
link destination while preserving rich child elements such as images, bold text,
and emphasis; retain the current URL fallback only when the extracted content is
empty.
- Around line 610-624: Validate link URLs in both anchor-export paths, including
the standalone-link branch around the visible `tagName === 'a'` logic and the
other anchor branch near the corresponding export code, before adding them to
`htmlParts` or `textParts`. Allow only intended schemes such as `http` and
`https`; skip or safely handle disallowed schemes like `javascript:` while
preserving existing label and escaping behavior for valid links.
- Around line 610-624: Update the standalone-link handling in the extraction
method to resolve relative href values against link.ownerDocument.baseURI before
generating either HTML or Markdown output, using the resolved destination
consistently for both paths. Apply the same normalization in the corresponding
link serialization logic around the later link-handling block.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7255be61-b37c-4558-87ea-2d2521087360
📒 Files selected for processing (5)
src/features/export/services/DOMContentExtractor.tssrc/features/export/services/__tests__/DOMContentExtractor.test.tssrc/features/plugins/builtin/chatgptExport/exporter.test.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.test.tssrc/features/plugins/builtin/chatgptExport/tempHandoff.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/features/plugins/builtin/chatgptExport/exporter.test.ts
- src/features/plugins/builtin/chatgptExport/tempHandoff.ts
Co-authored-by: Codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37bb145ada
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: Codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: daab5703cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function extractSnapshotMarkdown(message: ChatGptMessageSnapshot): string { | ||
| const extracted = | ||
| message.role === 'user' | ||
| ? DOMContentExtractor.extractUserContent(message.element) | ||
| : DOMContentExtractor.extractAssistantContent(message.element); |
There was a problem hiding this comment.
Preserve ChatGPT formulas during rich extraction
When a ChatGPT message contains current KaTeX markup, this still routes it through DOMContentExtractor, whose math branches recognize Gemini's data-math/.math-inline shapes but not ChatGPT's [data-math-source] and .katex-display shapes. Fresh evidence after the earlier rich-extractor review is FormulaCopyService.extractLatexSource, which explicitly handles current ChatGPT markup, together with its fixtures at lines 562-619; without equivalent handling here, Markdown/JSON flatten formulas to rendered glyph text and PDF loses the semantic KaTeX structure. Reuse that established extraction path for both inline and display math.
AGENTS.md reference: AGENTS.md:L73-L73
Useful? React with 👍 / 👎.
| const maximum = Math.max(0, target.scrollHeight - target.clientHeight); | ||
| if (target.scrollTop >= maximum - 2) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
Wait for an active response to finish before exporting
When the user starts a whole-conversation export while ChatGPT is still generating the bottom response, reaching the current scroll maximum breaks this loop immediately; the only subsequent delay is one settleMs sample, with no generation-state or content-stability check. Any response that continues beyond that roughly 120 ms window is silently exported as a partial answer, so wait for generation completion or explicitly reject an in-progress export before treating the bottom as complete.
Useful? React with 👍 / 👎.
| const file = new File([delivery.attachment], delivery.filename, { type: 'text/markdown' }); | ||
| if (!(await dispatchAttachmentAndVerify(input, file))) return false; | ||
| return insertComposerText(input, delivery.directive); |
There was a problem hiding this comment.
Track partial attachment delivery before retrying
If the attachment paste succeeds and creates a preview but inserting the directive fails, this returns false after modifying the composer and leaves the pending payload for retry. The next resumePendingHandoff attempts the attachment again, but dispatchAttachmentAndVerify can never report success once alreadyVisible is true because every success check is guarded by !alreadyVisible; retries can therefore add duplicate files while the handoff remains stuck until its TTL expires. Persist the attachment-complete stage or make retries deliver only the missing directive.
Useful? React with 👍 / 👎.
Co-authored-by: Codex <codex@users.noreply.github.com>
|
The 80% docstring warning is not adopted. That threshold is not part of this repository's checks, and boilerplate docstrings on local plugin internals would add noise without improving a public API. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5c6960b1a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const extracted = this.extractAssistantContent(element); | ||
| extracted.attachments = attachments; | ||
| return extracted; |
There was a problem hiding this comment.
Include mixed ChatGPT attachments in delegated exports
When a ChatGPT user turn contains both prompt text in a nested .markdown container and a sibling [data-testid="file-attachment"] pill, extractAssistantContent() narrows extraction to the Markdown container and excludes the pill. This branch then records the attachment only in extracted.attachments, but the Markdown/JSON exporters consume text and the PDF exporter consumes html, so the uploaded filename disappears from every format. Fresh evidence after the earlier attachment review is that the new ChatGPT selector populates this otherwise-unserialized array; append the detected attachments to the delegated text and HTML as the Gemini path does.
Useful? React with 👍 / 👎.
| const host = turn || element; | ||
| const virtualPosition = readVirtualPosition(host); | ||
| if (virtualPosition) return `fallback-${role}-${virtualPosition.key}`; |
There was a problem hiding this comment.
Stabilize fallback IDs across history prepends
For the supported fallback turn structures without native IDs, the ID is derived from the element's current global scroll offset. When reaching the top asynchronously prepends older history, every previously mounted turn shifts to a new offset; snapshots captured before the prepend therefore retain the old IDs while the same turns are collected again under new IDs. Whole-conversation exports can consequently contain duplicates and place the formerly oldest turn before newly loaded older messages, so identity must remain stable across prepend-induced layout shifts or collection must avoid retaining pre-stabilization fallback snapshots.
Useful? React with 👍 / 👎.
Description
PluginScopelifecycle cleanup.Related Issue
Refs #841
PR #920 opened while this branch was being prepared. It was not open at the preceding check, so the two implementations arrived independently within minutes of each other.
community-only.Visual Proof
Format selection dialog:
PDF output:
Browser Testing
Tested commit:
a5c6960Missing checks and owner, or N/A reason:
Commands not run and reason:
Verification notes:
bun run format: passed.bun run lint: passed with 0 errors; existing repository warnings remain.bun run verify:pr: ran through the standard checks. 2667/2668 tests passed; the only failure is an unrelated Windows path-separator assertion for allowed embedded provisioning profiles.zipcompatibility shim; no shim or build output is included in this PR.bun run docs:build: passed.Checklist
bun run format,bun run lint, then the standard localbun run verify:pr, and documented the local verification exceptions above.