Skip to content

Let a person export a conversation as a Markdown file - #1021

Open
Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:export-conversation-markdown
Open

Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:export-conversation-markdown

Conversation

@Ayush7614

Copy link
Copy Markdown
Contributor

What this changes

A person can export a conversation as a Markdown file, from a new download button in the conversation's header.

A conversation's record lives in the history store, and until now the only way out of it was the screen. Keeping a transcript, sharing it with someone who was not in it, or filing it beside the work it produced all needed something the app had no button for.

How it works:

  • The export reads the conversation's durable history fresh, through the same readThreadMessages the transcript restores with, on a longer deadline (10s vs the mount's 1.5s): a mount must not stall behind a slow store, but an export is asked for deliberately, by a person waiting on a button. A turn still streaming when Export is pressed can be missing from the file; pressing it again once the Bot has finished takes the record whole. The transcript on screen is untouched.
  • The file is built from the transcript's own projection (toVisibleChatItems), so an export shows exactly what the conversation shows: tool calls paired with the results that answered them, every file a person attached, malformed rows skipped rather than losing the file. An export that re-read the messages would be a second opinion about all of that, and the two would drift.
  • Unreadable turns are counted at the head of the file (> 2 turns in this conversation could not be read…) rather than quietly absent — the same rule the transcript's footer follows.
  • Tool results are fenced with a fence one backtick longer than any run the content holds, so a page of Markdown a tool read cannot close its own fence and pour itself into the document. Long results are cut at 4,000 characters with a line saying they were cut.
  • The file name is the conversation's own name, made safe for a download dialog: path separators and control characters become spaces, whitespace is collapsed, and the name is capped at 80 characters rather than left to the platform.
  • The button sits in the header beside the Bot's settings with the same waiting state as every other control (spinner while reading), disabled while the channel loads rather than hidden, and a refusal is one sentence under the header where the click happened, cleared by the next attempt.

Where it runs

  • New state that outlives a request? None. The export is a read of the history store plus a file built in the browser; nothing is written anywhere.
  • What happens on the second replica? Nothing to answer: no server change, no shared state, nothing serialised.
  • Anything serialised? No. Two people exporting the same conversation at once produce two files.
  • Anything fanned out to a browser? No. The download happens in the tab that pressed the button.
  • New listener, port, or schedule? No.

Boundary and audit

  • No acting call: the export only reads a thread's messages through the same session-scoped history endpoint the transcript already restores through, so nothing new is acted on and nothing new needs a boundary decision or an audit row.
  • Nothing new is trusted from the client: the file is built from what the server already returns, checked by the projection's existing guards (isReadableMessage, isReadablePart, isReadableToolCall) rather than by any new trust.

Changelog

A line added under Unreleased.

Proof

All run on this branch, on macOS:

bun test app/src/components/channels/transcript-export.test.ts
  → 10 pass, 0 fail (29 expect calls)

bun test app/
  → 2694 tests across 333 files, 0 fail (9268 expect calls)

cd app && bun run typecheck   (tsc --noEmit)
  → clean

bunx biome lint --error-on-warnings <the three changed files>
  → clean

bunx biome format --write <the three changed files>
  → applied, no remaining diffs

The ten new tests cover: question and answer order, a tool call with its paired result, a call still in flight (no result section), attached files with names and image marking, the unreadable-turns note (singular and plural), an empty conversation, a result containing its own ``` fence (the fence around it grows), a result over budget saying it was cut, a malformed row skipped without losing the conversation, and file-name safety (path separators, control characters, whitespace collapse, empty fallback, length cap).

A conversation's record lives in the history store, and until now the only
way out of it was the screen. Keeping a transcript, sharing it with someone
who was not in it, or filing it beside the work it produced all needed
something this app had no button for.

The export reads the conversation's durable history fresh, through the same
readThreadMessages the transcript restores with, on a longer deadline: a
mount must not stall behind a slow store, but an export is asked for
deliberately, by a person waiting on a button. The transcript on screen is
untouched; a turn still streaming when Export is pressed can be missing from
the file, and pressing it again once the Bot has finished takes the record
whole.

The file is built from the transcript's own projection
(toVisibleChatItems), so an export shows exactly what the conversation
shows: tool calls paired with the results that answered them, every file a
person attached, and malformed rows skipped rather than losing the file.
Unreadable turns are counted at the head of the file rather than quietly
absent. Results are fenced with a fence one backtick longer than any run
the content holds, so a page of Markdown a tool read cannot close its own
fence and pour itself into the document; long results are cut at a budget
that keeps the file readable, with a line saying they were cut.

The button sits in the conversation header beside the Bot's settings, with
the same waiting state as every other control, and a refusal is one
sentence under the header where the click happened, cleared by the next
attempt. The file name is the conversation's name made safe for a download
dialog: separators and control characters become spaces, and the name is
capped rather than left to the platform.

This branch has not been deployed

No deployments
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