Skip to content

/export only exports the loaded portion of a long thread #50014

Closed

Description

@samebutdiff

Description

In a long thread, /export writes only the part of the conversation that's currently
loaded. The file starts partway in and everything before that is missing. There is no
warning: the export succeeds and the file looks complete.

Scrolling to the top first, so the whole thread loads, gives a correct export. I'd
expect /export to write the full thread regardless of scroll position.

Plugins

No response

OpenCode version

v2.0.10

Steps to reproduce

  1. Open a thread with well over 100 messages, so older messages aren't loaded.
  2. Run /export without scrolling, taking the default options.
  3. Check the first message in the file. It begins partway into the session.
  4. Scroll to the top of the same session, then run /export again.
  5. This file has the full thread. Scroll position is the only difference.

Screenshot and/or share link

No response

Operating System

macOS 27

Terminal

Ghostty

Activity

  1. argszero commented on Sep 19, 2026

    @argszero
    Contributor

    I'd like to work on this.

    The report is exact, and the repository already contains the control case for it: the same dialog has a second format that does not have the bug.

    Why the markdown file starts partway in — the markdown export is built in the TUI from the loaded window, while the JSON export is built on the server from storage.

    Step Location (v2)
    /export command, both formats packages/tui/src/routes/session/index.tsx:1113 (id: "session.export", slash: { name: "export" })
    markdown branch — client-side :1132 formatSessionTranscript(sessionData, messages(), options.thinking, options.tools)
    JSON branch — server-side :1134 client.api.session.export({ sessionID, sanitize })
    what messages() is :160 const messages = () => data.session.message.list(route.sessionID) — the same list the timeline pages into
    the formatter itself :3608 formatSessionTranscript(session, messages, thinking, tools) — a pure formatter over the array it is handed; it cannot page
    server side of the JSON path packages/server/src/handlers/session.ts:166 → SessionTransfer.export → packages/core/src/session/transfer.ts reads sessions.messages({ sessionID, order: "asc" })

    So the markdown formatter can only ever see the messages the client has currently loaded, and the loaded window is exactly what scrolling changes — which is why step 5 of your repro produces the full file. The JSON format of the same dialog returns the whole session, because transfer.export reads the messages from storage with no window at all. Same session, same dialog, different data source: a free A/B check for anyone verifying the fix.

    Two surfaces, not one. The sibling command at :1105 (Copy session transcript) calls the same formatSessionTranscript(sessionData, messages(), true), so copying from a long thread silently copies the same partial window.

    Fix options

    1. Point the markdown path at the complete history as well — page the rest in (or use the full-items view) before formatting, keeping the formatter where it is.
    2. Move the markdown rendering server-side and expose a format next to the existing sanitize on session.export, so both formats are built from transfer.export's complete message list. This removes the second, divergent copy of the transcript logic and makes "complete by construction" true for every format.

    My default is (2), because the server-side source is already complete and the endpoint already has the query-parameter shape for it; (1) is the smaller diff if you'd rather not grow the export API. Happy to implement either.

    One related question: should an export that cannot see the whole session fail or warn instead of writing a file that looks complete? That is the part of the report that costs users data, and it is independent of which fix above you prefer — I would make the markdown path refuse to write when the client cannot prove it has the full range, unless you'd rather keep it best-effort.

    Happy to have the issue assigned if either direction looks right.

  2. Tarquinen commented on Oct 20, 2026

    @Tarquinen
    Member

    This was fixed by #50733. That PR merged into the v2 branch, so GitHub didn't close this issue automatically. Closing as completed.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions