Skip to content

feat(core): add optional PPTX export via slide-to-pptx plugin#156

Open
Luotee wants to merge 2 commits into
1weiho:mainfrom
Luotee:feat/core-pptx-export-plugin
Open

feat(core): add optional PPTX export via slide-to-pptx plugin#156
Luotee wants to merge 2 commits into
1weiho:mainfrom
Luotee:feat/core-pptx-export-plugin

Conversation

@Luotee
Copy link
Copy Markdown

@Luotee Luotee commented May 23, 2026

Summary

Adds an optional Download PPTX entry to the slide download dropdown, alongside the existing HTML and PDF options. PPTX rendering is delegated to the optional npm peer dependency @helping-ai-workflow/slide-to-pptx; @open-slide/core itself never imports it directly, so users who never install the plugin pay zero bundle-size cost.

Two phases:

  • Plugin missing: server returns JSON { status: 'plugin-missing', packageManager, command }. Client surfaces a card-style toast with the detected cd <userCwd> && <pm> add -D @helping-ai-workflow/slide-to-pptx command + an inline copy button.
  • Plugin installed: server opens an SSE stream forwarding the plugin's per-page progress events as progress frames, then a final done frame carrying the base64-encoded pptx. Client reassembles into a Blob and triggers a download. The progress toast mirrors the existing PDF toast UX (per-page count + an interpolated progress bar across the rendering phase).

Scope discipline

Diff is strictly additive — no existing PDF / HTML / route code is refactored. The PPTX path mirrors the PDF path file-for-file (handler ↔ handler, toast ↔ toast, lib ↔ lib) so the two exports are symmetric.

src/app/lib/
  export-html.ts   ← existing
  export-pdf.ts    ← existing
  export-pptx.ts   ← new, sibling pattern

Files

9 added (packages/core/src/):

  • vite/routes/export-pptx-handler.ts — pure handler (plugin-resolution via ESM import() walking node_modules from userCwd, shell-quoted install command, slideId path-traversal guard)
  • vite/routes/export-pptx.ts — registers POST /__os/api/export/pptx
  • vite/routes/pm-detect.ts — pnpm / npm / yarn / bun detection from lockfile + packageManager field
  • app/lib/export-pptx.ts — SSE consumer with try/finally reader cancellation
  • app/components/pptx-progress-toast.tsx — card-shaped progress toast mirroring pdf-progress-toast.tsx
  • app/components/pptx-plugin-missing-toast.tsx — inline copy-button toast
  • vite/routes/export-pptx-handler.test.ts (6 cases incl. 3 path-traversal blocks)
  • vite/routes/pm-detect.test.ts (10 cases)
  • .changeset/pptx-export-optional-plugin.md (minor bump)

8 modified:

  • packages/core/package.json — adds @helping-ai-workflow/slide-to-pptx to peerDependenciesMeta with optional: true
  • packages/core/src/app/routes/slide.tsx — new dropdown item next to PDF
  • packages/core/src/locale/{en,zh-tw,zh-cn,ja,types}.tspptxToast.* keys + exportAsPptx / pptxExportFailed
  • packages/core/src/vite/api-plugin.ts — one-line route registration

Test plan

  • pnpm test — 236 / 236 passing
  • tsc --noEmit clean
  • Dogfooded end-to-end against a 20-deck workspace (vercel-labs-2026, material-design-2014, open-slide-launch, etc.) — both plugin-missing toast (with copy-command UX) and happy-path SSE download verified in the browser

Open for maintainer preference

A few defensive notes I left out to keep the diff additive — happy to address any of these in this PR or a follow-up:

  • SSE write on client disconnect: res.write inside the plugin's onProgress callback can throw EPIPE if the user closes the tab mid-stream. The throw currently lands in the outer try/catch (writes an error event, then res.end() — both no-ops on a closed socket). User-impact is a noisy server log line per cancelled export. Wrappable in a silent try/catch if you'd prefer.

  • PDF dropdown shares the same theoretical toast-id race that the PPTX side closes via a Date.now() session suffix. Did NOT touch the existing PDF dropdown to keep the diff additive, but happy to surface a sibling fix here or in a follow-up if you'd like the convention uniform.

  • shellQuote not applied to the plugin name in pm-detect.installCommand — the package name is a constant today, but if you'd prefer defensive coverage for any future templating, I can add the wrap.

🤖 Drafted with assistance from Claude Code

Summary by CodeRabbit

  • New Features

    • Added "Export as PPTX" to the slide download menu, with streaming download and automatic save.
    • Shows progress toasts during export and a clear message when the export plugin is missing, including a one-click copy of the install command.
    • Localized PPTX status and action strings added for en/ja/zh-CN/zh-TW.
  • Chores

    • Marked the PPTX plugin as an optional peer dependency.
  • Tests

    • Added tests for export handling and package-manager detection.

Review Change Stack

…o-pptx

Adds a third entry to the slide download dropdown ("Download PPTX")
alongside the existing HTML and PDF options. PPTX rendering is
delegated to the optional npm peer dependency
`@helping-ai-workflow/slide-to-pptx`; the core itself never imports
it directly. The dropdown item works in two phases:

1. Plugin not installed: server returns JSON
   `{ status: 'plugin-missing', packageManager, command }` and the
   client surfaces a card-style toast with the detected
   `cd <userCwd> && <pm> add -D @helping-ai-workflow/slide-to-pptx`
   command + an inline copy button (`pptx-plugin-missing-toast`).
2. Plugin installed: server opens a `text/event-stream`, forwards
   the plugin's `onProgress` events as SSE `progress` frames, then
   one `done` frame carrying the base64-encoded pptx. The client
   reassembles into a Blob and triggers a download. A
   `pptx-progress-toast` mirrors the existing PDF toast UX
   (per-page count, progress bar interpolated across the
   `rendering` phase between 10% and 80%).

## Files added

- `packages/core/src/vite/routes/export-pptx-handler.ts`
  Pure handler that detects the plugin via dynamic ESM `import()`
  walking `node_modules` from `userCwd` (works under npm `file:`
  symlinks), shell-quotes the install command, and rejects any
  `slideId` whose resolved path escapes `<userCwd>/slides/`.
- `packages/core/src/vite/routes/export-pptx.ts`
  Registers `POST /__os/api/export/pptx` and wires the handler to
  the project's `userCwd`.
- `packages/core/src/vite/routes/pm-detect.ts`
  Detects pnpm / npm / yarn / bun from lockfile presence and
  `packageManager` field; builds the per-pm install command.
- `packages/core/src/app/lib/export-pptx.ts`
  Browser-side SSE consumer with `try/finally` reader cancellation
  on every error path, switch-default in `computePercent` to keep
  the progress bar advancing on unknown phases. Naming mirrors the
  existing `export-pdf.ts` / `export-html.ts` siblings.
- `packages/core/src/app/components/pptx-progress-toast.tsx`
  Card-shaped toast mirroring `pdf-progress-toast.tsx`: shows
  current/total page count + interpolated percent.
- `packages/core/src/app/components/pptx-plugin-missing-toast.tsx`
  Inline copy-button toast for the install command.
- Tests: `export-pptx-handler.test.ts` (6 cases incl. path-traversal
  blocks), `pm-detect.test.ts` (10 cases).
- `.changeset/pptx-export-optional-plugin.md` (minor bump).

## Files modified

- `packages/core/package.json`
  Adds `@helping-ai-workflow/slide-to-pptx` to
  `peerDependenciesMeta` with `optional: true`. No runtime
  dependency on the plugin; bundle size unaffected for users who
  don't install it.
- `packages/core/src/app/routes/slide.tsx`
  Adds the PPTX dropdown item next to the existing PDF item, with
  a Date.now()-suffixed toast session id so rapid double-clicks
  produce distinct toast ids instead of stomping each other.
- `packages/core/src/locale/{en,zh-tw,zh-cn,ja,types}.ts`
  Adds `pptxToast.*` and `exportAsPptx` / `pptxExportFailed` keys.
- `packages/core/src/vite/api-plugin.ts`
  One-line route registration.

## Scope discipline

The diff is strictly additive — no existing PDF / HTML / route code
is refactored. The PPTX path mirrors the PDF path file-for-file
(handler ↔ handler, toast ↔ toast, lib ↔ lib) so the two exports
are symmetric.

All 236 vitest cases pass; tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

@Luotee is attempting to deploy a commit to the Yiwei Ho Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96477210-5c06-4332-b58b-d326cd2fcd55

📥 Commits

Reviewing files that changed from the base of the PR and between af70b00 and ae172bd.

📒 Files selected for processing (1)
  • .changeset/pptx-export-optional-plugin.md

Walkthrough

Adds optional "Export as PPTX" to slide downloads: client export API and toasts, locale strings, server SSE export route with plugin dynamic import and package-manager detection, tests, and package.json marking the plugin as an optional peer dependency.

Changes

PPTX Export Feature

Layer / File(s) Summary
Feature declaration and package metadata
.changeset/pptx-export-optional-plugin.md, packages/core/package.json
Changesets entry documents the optional PPTX export and package.json marks @helping-ai-workflow/slide-to-pptx as an optional peer dependency.
Locale type schema and translations
packages/core/src/locale/types.ts, packages/core/src/locale/en.ts, packages/core/src/locale/ja.ts, packages/core/src/locale/zh-cn.ts, packages/core/src/locale/zh-tw.ts
Locale contract adds copyCommand, PPTX export labels, error/missing-plugin messages, and pptxToast lifecycle strings; translations updated across locales.
Client export API and progress computation
packages/core/src/app/lib/export-pptx.ts
Client posts slideId to /__os/api/export/pptx, handles JSON plugin-missing responses and SSE progress stream, computes phase-based percent, decodes base64 PPTX, and triggers a browser download.
Client UI components
packages/core/src/app/components/pptx-progress-toast.tsx, packages/core/src/app/components/pptx-plugin-missing-toast.tsx
PptxProgressToast shows phase-specific messages and progress bar; PptxPluginMissingToast displays install command with copy-to-clipboard and success toast.
Slide dropdown UI integration
packages/core/src/app/routes/slide.tsx
Adds PPTX export dropdown item that starts export, manages persistent progress toast, handles plugin-missing callback, and surfaces error/completion toasts.
Package manager detection
packages/core/src/vite/routes/pm-detect.ts, packages/core/src/vite/routes/pm-detect.test.ts
Detects project package manager from package.json or lockfiles (pnpm/npm/yarn/bun) and maps to install commands; tests validate detection and command strings.
Server export handler
packages/core/src/vite/routes/export-pptx-handler.ts, packages/core/src/vite/routes/export-pptx-handler.test.ts
Validates slideId (prevents traversal), attempts plugin load, returns JSON on plugin-missing, or streams SSE progress and final done event with base64 PPTX; includes tests for plugin-missing, happy path, and input validation.
Server route registration and plugin loading
packages/core/src/vite/routes/export-pptx.ts
Registers POST /__os/api/export/pptx, parses JSON body, and dynamically imports the plugin from the consumer's node_modules, throwing ERR_MODULE_NOT_FOUND when absent.
API plugin integration
packages/core/src/vite/api-plugin.ts
API plugin imports and registers the export-PPTX routes during dev-server configuration.

Sequence Diagram

sequenceDiagram
  participant Client
  participant DevServer
  participant PluginModule
  participant Renderer
  Client->>DevServer: POST /__os/api/export/pptx { slideId }
  DevServer->>DevServer: validate slideId, resolve slideDir
  DevServer->>PluginModule: dynamic import register/render entry
  alt module not found
    DevServer-->>Client: 200 JSON { status: "plugin-missing", packageManager, command }
  else plugin loaded
    DevServer->>Renderer: renderSlideToPptx(slideDir, onProgress)
    Renderer-->>DevServer: progress events (phase,current,total)
    DevServer-->>Client: SSE events (progress ...)
    Renderer-->>DevServer: done (Buffer)
    DevServer-->>Client: SSE done (base64 payload)
    Client->>Client: decode base64 -> download blob
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • 1weiho

Poem

🐰 I hopped through code to add a key,
PPTX waits if plugins flee,
A toast to guide, a copy near,
Install and export — cheer, cheer, cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature addition: optional PPTX export functionality integrated via an optional plugin dependency.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 @.changeset/pptx-export-optional-plugin.md:
- Line 5: Replace the current line 5 in
.changeset/pptx-export-optional-plugin.md with a single short present-tense
user-facing change sentence (e.g., "Add optional 'Export as PPTX' item to the
slide download dropdown."), removing the install instructions and rationale so
the changeset contains only a one-line description of what changed from the
user's perspective.

In `@packages/core/package.json`:
- Around line 66-70: Add the missing peer dependency entry for
"`@helping-ai-workflow/slide-to-pptx`" in package.json so the existing
"peerDependenciesMeta" optional flag applies; specifically, add a
"peerDependencies" object (or update it) to include
"`@helping-ai-workflow/slide-to-pptx`": "*" (or an appropriate semver) alongside
the existing "peerDependenciesMeta" entry so npm treats it as an optional peer
dependency. Ensure the symbol names to edit are the JSON keys "peerDependencies"
and "peerDependenciesMeta" and the package name
"`@helping-ai-workflow/slide-to-pptx`".

In `@packages/core/src/app/components/pptx-plugin-missing-toast.tsx`:
- Around line 23-27: The onClick handler currently calls
navigator.clipboard?.writeText(command) and immediately shows a success toast
and dismisses toastId even if the copy failed or the Clipboard API is missing;
change the onClick logic in the component to first check for navigator.clipboard
and call navigator.clipboard.writeText(command) (which returns a Promise) and
only call toast.success(...) and toast.dismiss(toastId) in the Promise .then()
(or await the promise), and handle failures in .catch() by either attempting a
fallback copy (e.g., temporary textarea) or showing an error toast; ensure you
reference the existing onClick handler, navigator.clipboard.writeText, command,
toast.success and toast.dismiss(toastId) when applying the fix.

In `@packages/core/src/app/components/pptx-progress-toast.tsx`:
- Around line 16-38: The switch over progress.phase (in the pptx progress toast
component) lacks a default, so if progress.phase contains an unexpected value
`text` may remain unset; add a default branch in the switch that assigns a
sensible runtime fallback to `text` (e.g., t.pptxToast.unknown or a generic
message) and optionally log or warn including the unexpected `progress.phase`
value to aid debugging; update the case switch that references progress.phase
and the variable text accordingly.

In `@packages/core/src/app/routes/slide.tsx`:
- Around line 507-513: Replace the millisecond-precision session id using
Date.now() with a collision-safe unique id to avoid toast key clashes: update
the code that constructs session (currently `const session =
`${slideId}-${Date.now()}``) so it uses a UUID (e.g., `crypto.randomUUID()`) or
combines `performance.now()` entropy, then use that new session to build
`progressToastId` and `errorToastId` (the variables `session`,
`progressToastId`, and `errorToastId` are the identifiers to change); ensure the
fallback is available for environments without crypto.randomUUID().

In `@packages/core/src/vite/routes/pm-detect.ts`:
- Around line 17-20: The bun detection only checks for 'bun.lockb' so Bun
projects with 'bun.lock' get misidentified; update the PM detection logic in the
function that uses fs.existsSync(path.join(cwd, ...)) to check for both
'bun.lock' and 'bun.lockb' (or check 'bun.lock' first) and return { name: 'bun'
} when either exists, ensuring the branch that returns { name: 'bun' } is
reached when a Bun lockfile is present.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56b34478-7224-41d4-9fcd-b32363fcea43

📥 Commits

Reviewing files that changed from the base of the PR and between 51108f1 and af70b00.

📒 Files selected for processing (17)
  • .changeset/pptx-export-optional-plugin.md
  • packages/core/package.json
  • packages/core/src/app/components/pptx-plugin-missing-toast.tsx
  • packages/core/src/app/components/pptx-progress-toast.tsx
  • packages/core/src/app/lib/export-pptx.ts
  • packages/core/src/app/routes/slide.tsx
  • packages/core/src/locale/en.ts
  • packages/core/src/locale/ja.ts
  • packages/core/src/locale/types.ts
  • packages/core/src/locale/zh-cn.ts
  • packages/core/src/locale/zh-tw.ts
  • packages/core/src/vite/api-plugin.ts
  • packages/core/src/vite/routes/export-pptx-handler.test.ts
  • packages/core/src/vite/routes/export-pptx-handler.ts
  • packages/core/src/vite/routes/export-pptx.ts
  • packages/core/src/vite/routes/pm-detect.test.ts
  • packages/core/src/vite/routes/pm-detect.ts

Comment thread .changeset/pptx-export-optional-plugin.md Outdated
Comment on lines +66 to +70
"peerDependenciesMeta": {
"@helping-ai-workflow/slide-to-pptx": {
"optional": true
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In npm package.json, does "peerDependenciesMeta" have any effect if the same package is not listed in "peerDependencies"?

💡 Result:

Yes and no—depends on what you mean by “effect,” and which package manager. For npm itself: peerDependenciesMeta only applies to entries under peerDependencies. npm’s documentation describes peerDependenciesMeta as providing metadata (currently: marking peer dependencies as optional) for “peer dependencies” declared in peerDependencies, e.g., “Marking a peer dependency as optional…” [1]. The npm docs and examples don’t define any behavior for keys in peerDependenciesMeta that are not present in peerDependencies, so npm does not have an advertised/useful effect for that mismatched case. For real-world behavior in the npm ecosystem: some tools treat peerDependenciesMeta entries as if they were peers even when they are missing from peerDependencies, which can result in installing/handling those packages anyway. For example, pnpm issue reports describe a case where a package present only in peerDependenciesMeta (and not peerDependencies) still affected peer handling/installation behavior [2]. That indicates that non-npm implementations may interpret peerDependenciesMeta more broadly than npm’s documented contract. So, if you’re asking specifically about npm: peerDependenciesMeta is meant to annotate peerDependencies, and keys not listed in peerDependencies aren’t part of the documented mechanism—so you should not rely on any effect there [1]. If you’re using pnpm/yarn or a custom installer, confirm with a test because behavior can differ [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/core/package.json"

echo "== package.json lines 50-90 =="
nl -ba "$FILE" | sed -n '50,90p'

echo
echo "== peerDependencies occurrences =="
rg -n "peerDependencies" "$FILE" || true

echo
echo "== slide-to-pptx occurrences =="
rg -n "slide-to-pptx" "$FILE" || true

Repository: 1weiho/open-slide

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/core/package.json"

echo "== package.json lines 50-90 =="
awk 'NR>=50 && NR<=90 {printf "%6d  %s\n", NR, $0}' "$FILE" || true

echo
echo "== peerDependencies occurrences =="
rg -n "peerDependencies" "$FILE" || true

echo
echo "== slide-to-pptx occurrences =="
rg -n "slide-to-pptx" "$FILE" || true

Repository: 1weiho/open-slide

Length of output: 1665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/core/package.json"

echo "== exact \"peerDependencies\": key matches =="
rg -n '"peerDependencies"\s*:' "$FILE" || true

echo
echo "== keys containing \"peerDependencies\" (for context) =="
rg -n '"peerDependencies[^"]*"\s*:' "$FILE" || true

Repository: 1weiho/open-slide

Length of output: 192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la .changeset || true
find .changeset -maxdepth 1 -type f -name '*.md' | sort | sed -n '1,200p'
find .changeset -maxdepth 1 -type f -name 'pptx-export-optional-plugin.md' -print || true

Repository: 1weiho/open-slide

Length of output: 473


Declare the peer dependency explicitly—peerDependenciesMeta alone isn’t sufficient.

In packages/core/package.json (lines 66-70), peerDependenciesMeta marks @helping-ai-workflow/slide-to-pptx as optional, but the package isn’t declared in peerDependencies, so the “optional peer” metadata won’t be applied as intended by npm.

Suggested fix
+  "peerDependencies": {
+    "`@helping-ai-workflow/slide-to-pptx`": "*"
+  },
   "peerDependenciesMeta": {
     "`@helping-ai-workflow/slide-to-pptx`": {
       "optional": true
     }
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"peerDependenciesMeta": {
"@helping-ai-workflow/slide-to-pptx": {
"optional": true
}
},
"peerDependencies": {
"`@helping-ai-workflow/slide-to-pptx`": "*"
},
"peerDependenciesMeta": {
"`@helping-ai-workflow/slide-to-pptx`": {
"optional": true
}
},
🤖 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 `@packages/core/package.json` around lines 66 - 70, Add the missing peer
dependency entry for "`@helping-ai-workflow/slide-to-pptx`" in package.json so the
existing "peerDependenciesMeta" optional flag applies; specifically, add a
"peerDependencies" object (or update it) to include
"`@helping-ai-workflow/slide-to-pptx`": "*" (or an appropriate semver) alongside
the existing "peerDependenciesMeta" entry so npm treats it as an optional peer
dependency. Ensure the symbol names to edit are the JSON keys "peerDependencies"
and "peerDependenciesMeta" and the package name
"`@helping-ai-workflow/slide-to-pptx`".

Comment on lines +23 to +27
onClick={() => {
navigator.clipboard?.writeText(command).catch(() => {});
toast.success(t.common.copyCommand, { duration: 1500 });
toast.dismiss(toastId);
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact snippet around the referenced lines
FILE="packages/core/src/app/components/pptx-plugin-missing-toast.tsx"
nl -ba "$FILE" | sed -n '1,120p'

# Extract just the onClick handler for quick review
nl -ba "$FILE" | sed -n '15,60p'

# Search for other clipboard usage patterns in packages/core
rg -n "navigator\.clipboard|clipboard\?\.writeText|writeText\(" packages/core/src/app -S || true
rg -n "toast\.success|toast\.dismiss" packages/core/src/app/components/pptx-plugin-missing-toast.tsx -S

# Search for "copyCommand" usage to see expected semantics elsewhere
rg -n "copyCommand" packages/core/src -S || true

Repository: 1weiho/open-slide

Length of output: 104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/core/src/app/components/pptx-plugin-missing-toast.tsx"

# Print file with line numbers via awk (no nl dependency)
awk '{print NR "\t" $0}' "$FILE" | sed -n '1,120p'

# Show the exact region around the prior referenced lines (23-27) with context
awk '{print NR "\t" $0}' "$FILE" | sed -n '15,45p'

# Search for clipboard usage patterns in core
rg -n "navigator\.clipboard|clipboard\?\.writeText|writeText\(" packages/core/src -S || true

# Search for toast usage patterns in the file
rg -n "toast\.success|toast\.dismiss" "$FILE" -S || true

# Check how copyCommand is used elsewhere (semantics)
rg -n "copyCommand" packages/core/src -S || true

Repository: 1weiho/open-slide

Length of output: 3874


Guard Clipboard API usage and only show success after an actual copy.

  • navigator.clipboard?.writeText(command) can evaluate to undefined; calling .catch(...) on that result can throw at runtime.
  • The success toast + toast.dismiss(toastId) currently run even if the copy didn’t happen.
🐛 Proposed fix
           <button
             type="button"
             onClick={() => {
-              navigator.clipboard?.writeText(command).catch(() => {});
-              toast.success(t.common.copyCommand, { duration: 1500 });
-              toast.dismiss(toastId);
+              const copyPromise = navigator.clipboard?.writeText(command);
+              if (!copyPromise) return;
+              copyPromise
+                .then(() => {
+                  toast.success(t.common.copyCommand, { duration: 1500 });
+                  toast.dismiss(toastId);
+                })
+                .catch(() => {});
             }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={() => {
navigator.clipboard?.writeText(command).catch(() => {});
toast.success(t.common.copyCommand, { duration: 1500 });
toast.dismiss(toastId);
}}
<button
type="button"
onClick={() => {
const copyPromise = navigator.clipboard?.writeText(command);
if (!copyPromise) return;
copyPromise
.then(() => {
toast.success(t.common.copyCommand, { duration: 1500 });
toast.dismiss(toastId);
})
.catch(() => {});
}}
🤖 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 `@packages/core/src/app/components/pptx-plugin-missing-toast.tsx` around lines
23 - 27, The onClick handler currently calls
navigator.clipboard?.writeText(command) and immediately shows a success toast
and dismisses toastId even if the copy failed or the Clipboard API is missing;
change the onClick logic in the component to first check for navigator.clipboard
and call navigator.clipboard.writeText(command) (which returns a Promise) and
only call toast.success(...) and toast.dismiss(toastId) in the Promise .then()
(or await the promise), and handle failures in .catch() by either attempting a
fallback copy (e.g., temporary textarea) or showing an error toast; ensure you
reference the existing onClick handler, navigator.clipboard.writeText, command,
toast.success and toast.dismiss(toastId) when applying the fix.

Comment on lines +16 to +38
switch (progress.phase) {
case 'loading':
text = t.pptxToast.loading;
break;
case 'measuring':
text = t.pptxToast.loading;
break;
case 'rendering':
text = format(t.pptxToast.rendering, {
current: progress.current.toString().padStart(2, '0'),
total: progress.total.toString().padStart(2, '0'),
});
break;
case 'building':
text = t.pptxToast.building;
break;
case 'postprocessing':
text = t.pptxToast.postprocessing;
break;
case 'done':
text = t.pptxToast.done;
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a runtime fallback for unexpected progress phases.

Line 16 to Line 38 has no default branch. Since phase is parsed from JSON at runtime, an unknown phase can leave text unset.

♻️ Proposed fix
   switch (progress.phase) {
@@
     case 'done':
       text = t.pptxToast.done;
       break;
+    default:
+      text = t.pptxToast.loading;
+      break;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch (progress.phase) {
case 'loading':
text = t.pptxToast.loading;
break;
case 'measuring':
text = t.pptxToast.loading;
break;
case 'rendering':
text = format(t.pptxToast.rendering, {
current: progress.current.toString().padStart(2, '0'),
total: progress.total.toString().padStart(2, '0'),
});
break;
case 'building':
text = t.pptxToast.building;
break;
case 'postprocessing':
text = t.pptxToast.postprocessing;
break;
case 'done':
text = t.pptxToast.done;
break;
}
switch (progress.phase) {
case 'loading':
text = t.pptxToast.loading;
break;
case 'measuring':
text = t.pptxToast.loading;
break;
case 'rendering':
text = format(t.pptxToast.rendering, {
current: progress.current.toString().padStart(2, '0'),
total: progress.total.toString().padStart(2, '0'),
});
break;
case 'building':
text = t.pptxToast.building;
break;
case 'postprocessing':
text = t.pptxToast.postprocessing;
break;
case 'done':
text = t.pptxToast.done;
break;
default:
text = t.pptxToast.loading;
break;
}
🤖 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 `@packages/core/src/app/components/pptx-progress-toast.tsx` around lines 16 -
38, The switch over progress.phase (in the pptx progress toast component) lacks
a default, so if progress.phase contains an unexpected value `text` may remain
unset; add a default branch in the switch that assigns a sensible runtime
fallback to `text` (e.g., t.pptxToast.unknown or a generic message) and
optionally log or warn including the unexpected `progress.phase` value to aid
debugging; update the case switch that references progress.phase and the
variable text accordingly.

Comment on lines +507 to +513
// Suffix with a high-resolution timestamp so two
// rapid clicks (before React re-renders to disable
// the dropdown item) produce distinct toast ids
// instead of stomping each other's state.
const session = `${slideId}-${Date.now()}`;
const progressToastId = `pptx-export-${session}`;
const errorToastId = `pptx-export-${session}-error`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a collision-safe session id for toast keys.

Date.now() is millisecond precision, so two very fast selects can still generate the same session and overwrite toast state. Prefer crypto.randomUUID() (or append performance.now() entropy) for guaranteed uniqueness.

Suggested fix
-                        const session = `${slideId}-${Date.now()}`;
+                        const session = `${slideId}-${crypto.randomUUID()}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Suffix with a high-resolution timestamp so two
// rapid clicks (before React re-renders to disable
// the dropdown item) produce distinct toast ids
// instead of stomping each other's state.
const session = `${slideId}-${Date.now()}`;
const progressToastId = `pptx-export-${session}`;
const errorToastId = `pptx-export-${session}-error`;
// Suffix with a high-resolution timestamp so two
// rapid clicks (before React re-renders to disable
// the dropdown item) produce distinct toast ids
// instead of stomping each other's state.
const session = `${slideId}-${crypto.randomUUID()}`;
const progressToastId = `pptx-export-${session}`;
const errorToastId = `pptx-export-${session}-error`;
🤖 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 `@packages/core/src/app/routes/slide.tsx` around lines 507 - 513, Replace the
millisecond-precision session id using Date.now() with a collision-safe unique
id to avoid toast key clashes: update the code that constructs session
(currently `const session = `${slideId}-${Date.now()}``) so it uses a UUID
(e.g., `crypto.randomUUID()`) or combines `performance.now()` entropy, then use
that new session to build `progressToastId` and `errorToastId` (the variables
`session`, `progressToastId`, and `errorToastId` are the identifiers to change);
ensure the fallback is available for environments without crypto.randomUUID().

Comment on lines +17 to +20
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { name: 'pnpm' };
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return { name: 'bun' };
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { name: 'yarn' };
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return { name: 'npm' };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What lockfile filename does Bun currently generate by default (bun.lockorbun.lockb), and what compatibility expectations exist for tooling that detects Bun projects?

💡 Result:

By default, Bun currently generates a lockfile named bun.lock (not bun.lockb) when you run bun install. [1] Compatibility expectations for tooling that detects Bun projects: - Many tools historically looked specifically for bun.lockb; Bun changed the default lockfile format to the text-based bun.lock, and it kept support for existing bun.lockb lockfiles (with an automatic migration path when running bun install). [1] - Tooling should therefore detect both bun.lock and bun.lockb during a transition period, because Bun supports both and will use bun.lock when present (as reflected by community/tooling guidance). [1][2] - If a tool relies on the lockfile’s contents/format, it should expect bun.lock to be the canonical “text-based lockfile” going forward, since Bun notes that features will target bun.lock after the migration. [1][3] Net: prefer bun.lock as the current default, but detection logic should treat bun.lockb as a fallback for older repos and/or during migration, and avoid assuming only one filename. [1][3][2]

Citations:


Add bun.lock fallback detection to avoid misidentifying Bun projects
Bun generates bun.lock by default, so checking only bun.lockb can cause Bun repos to fall through and be detected as another package manager (leading to the wrong install command).

Suggested patch
   if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { name: 'pnpm' };
+  if (fs.existsSync(path.join(cwd, 'bun.lock'))) return { name: 'bun' };
   if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return { name: 'bun' };
   if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { name: 'yarn' };
   if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return { name: 'npm' };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { name: 'pnpm' };
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return { name: 'bun' };
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { name: 'yarn' };
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return { name: 'npm' };
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { name: 'pnpm' };
if (fs.existsSync(path.join(cwd, 'bun.lock'))) return { name: 'bun' };
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return { name: 'bun' };
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { name: 'yarn' };
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return { name: 'npm' };
🤖 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 `@packages/core/src/vite/routes/pm-detect.ts` around lines 17 - 20, The bun
detection only checks for 'bun.lockb' so Bun projects with 'bun.lock' get
misidentified; update the PM detection logic in the function that uses
fs.existsSync(path.join(cwd, ...)) to check for both 'bun.lock' and 'bun.lockb'
(or check 'bun.lock' first) and return { name: 'bun' } when either exists,
ensuring the branch that returns { name: 'bun' } is reached when a Bun lockfile
is present.

Applied with a small tweak — kept "(requires `@helping-ai-workflow/slide-to-pptx` plugin)" so the "optional" part is clear to users reading the changelog. Install command and toast behavior moved out as suggested.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@1weiho
Copy link
Copy Markdown
Owner

1weiho commented May 24, 2026

Hey @Luotee, thanks for this PR! The implementation is really well done, and I appreciate the effort you put into it.

I do want to raise one architectural concern before we move forward. I'd prefer to have the PPTX rendering logic built directly into open-slide rather than depending on an external package (@helping-ai-workflow/slide-to-pptx), for two reasons:

  1. Security: As a framework used by people, asking users to install a third-party package for a core export feature adds a dependency we can't fully audit or control.
  2. Maintainability: When open-slide makes breaking changes to slide primitives, we need to update all export paths together. An external plugin creates a sync gap we can't fix on our end.

Would you be open to co-working on this as a built-in feature? I'd love to collaborate on landing it together. Let me know what you think!

@Luotee
Copy link
Copy Markdown
Author

Luotee commented May 24, 2026

Hi @1weiho , thanks for the kind words and the thoughtful concerns. Both points totally make sense.

One thing worth flagging though: even if we merge this directly into open-slide, users will still need to run npx playwright install chromium to use the export. The PPTX rendering precision currently relies on a real browser, so the extra download is hard to avoid with the current approach.

That said, I think it's worth exploring whether we can hit the same precision without the browser dependency. Would make the whole thing cleaner. I'm traveling this week, so let me poke at this when I'm back and we can sync on a plan then. Excited to collaborate on landing this properly! 🙌

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