Skip to content

Export chat session: Markdown, plain text, and clipboard#189

Merged
quiet-node merged 11 commits into
mainfrom
feat/export-session
May 25, 2026
Merged

Export chat session: Markdown, plain text, and clipboard#189
quiet-node merged 11 commits into
mainfrom
feat/export-session

Conversation

@quiet-node
Copy link
Copy Markdown
Owner

@quiet-node quiet-node commented May 25, 2026

Overview

Adds a chat-session export feature reachable from a new icon in the chat header. Users can save the active conversation as a self-contained Markdown file or copy a clipboard-friendly Markdown body to the system clipboard. The flow is built around the existing NSPanel overlay so the native save dialog feels at home with the rest of the spotlight surface.

What changed

  • New chat-header export button with a small popover offering two rows: Save as Markdown… and Copy to clipboard. The popover is a proper menu/menuitem widget with first-item autofocus and Escape-to-dismiss.
  • Two serializers live in src/lib/exportSerializer.ts:
    • serializeForFile emits a self-contained Markdown artefact (YAML frontmatter, role-labelled blocks, inline base64 screenshots, search source links).
    • serializeForClipboard emits a body-only Markdown block with image markers (no frontmatter, no base64) so paste targets like Slack and Discord stay readable.
  • The Rust side owns the save dialog AND the write in one atomic command (export::prompt_and_save_chat_export). The frontend hands over the serialised content and a suggested filename; the destination path is consumed entirely inside Rust and never crosses the IPC boundary.
  • A small NSPanel alpha helper hides Thuki while the save dialog is on screen and fades it back when the dialog dismisses, so the dialog's drop-shadow and vibrancy backdrop have nothing to bleed onto.
  • The Save dialog defaults to the compact macOS layout on first launch (one-off NSNavPanelExpandedStateForSaveMode write), matching the spotlight feel.

How it works

  1. User clicks the export icon → popover opens; the first menuitem is focused.
  2. User picks Save as Markdown…. The handler serialises the conversation while the overlay is still visible (so the prep step is the perceived activity), then fires set_overlay_alpha(0) and invokes prompt_and_save_chat_export.
  3. Rust opens the native NSSavePanel with a Markdown filter.
  4. On a chosen path Rust writes the file directly. On cancellation it returns Ok(false). On a write failure it returns a sanitised, kind-specific message (no absolute path).
  5. The overlay alpha is restored to 1 in finally, regardless of which branch executed.

Security and correctness notes

  • The dialog and write are fused into a single Rust command so a compromised renderer cannot drive fs::write at an arbitrary path.
  • dialog:allow-save capability is no longer required and was removed from capabilities/default.json; @tauri-apps/plugin-dialog is dropped from frontend dependencies because the renderer no longer calls the dialog plugin directly.
  • Write errors return short, fixed user-facing strings keyed off std::io::ErrorKind, so the destination path never appears in the error banner / screenshots / screen recordings.
  • YAML frontmatter values are double-quoted with control-character escaping so a model name containing \n, \", \\, or --- cannot escape the block and inject keys downstream parsers would honour.
  • Search source links are emitted with backslash-escaped titles and angle-bracketed URLs; javascript:, data:, and other non-http(s) URLs degrade to a non-clickable line that preserves the raw URL for context. Streamdown + rehype-sanitize already protect Thuki's own renderer; this is the matching protection for the exported file when it is opened in a third-party Markdown viewer.
  • The export popover is mutually exclusive with the history dropdown and the model picker; opening any one closes the other two. The popover is also dismissed on minimize, on "new conversation", and on loading a different session.
  • A re-entrancy guard in runFileExport ensures the alpha:0/alpha:1 brackets cannot interleave if the user double-clicks the menuitem while a save dialog is still on screen.

Testing

  • bun run test:all:coverage — Vitest (1480 tests, 100% line/branch/statement/function coverage across src/) plus cargo +nightly-2026-03-30 llvm-cov (882 unit tests, lib.rs/main.rs-excluded line gate passes).
  • bun run validate-build — lint (ESLint + cargo clippy -D warnings), Prettier + rustfmt format check, tsc --noEmit, frontend Vite build, tauri build release bundle.

Note

The Markdown path runs image loads in parallel via Promise.all so an image-heavy session does not stall the overlay-hidden window any longer than the actual file write.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…adow

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ping, popover a11y

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node force-pushed the feat/export-session branch from 3a71129 to 187f25b Compare May 25, 2026 17:00
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node merged commit ea33d8c into main May 25, 2026
3 checks passed
@quiet-node quiet-node deleted the feat/export-session branch May 25, 2026 17:44
quiet-node added a commit that referenced this pull request May 25, 2026
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request May 25, 2026
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
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.

1 participant