fix(export): serve transcript Markdown from server (Chromium blob: download hang)#4
Merged
zhonghuaY merged 1 commit intofix/sidebar-rename-models-fullwidthfrom Apr 29, 2026
Conversation
Prior fix (#3) made the blob-URL download more robust (DOM-attached anchor, deferred URL revoke, try/catch) but did NOT solve the actual hang user reported: the file would write completely to disk, yet the browser download UI never reached the 'complete' state. Root cause is a Chromium quirk with blob: URL downloads where the download progress UI cannot reliably tell when the stream ends — the file is on disk but the download bar shows it as still in-progress indefinitely. Solution: bypass blob URLs entirely. Serve the markdown from the server with proper Content-Disposition + Content-Length, mirroring the JSON export path which works flawlessly. Server side (api/routes.py) - new _build_session_markdown(session) function mirrors the JS transcript() in messages.js: skip tool messages, join array text blocks, append attachments marker - new _handle_session_export_markdown(handler, parsed) mirrors the JSON export structure: redact_session_data() before rendering, set Content-Type: text/markdown; charset=utf-8, Content-Disposition: attachment with filename, Content-Length, Cache-Control: no-store - /api/session/export dispatcher now branches on ?format=md vs JSON default Client side (static/boot.js) - #btnDownload click handler now navigates to /api/session/export?format=md&... - still uses the working pattern from downloadFile() in workspace.js: a.style.display='none' + appendChild + click() + setTimeout removeChild - removed all blob URL machinery (no createObjectURL, no revokeObjectURL) - still wrapped in try/catch with showToast on success and failure Tests (tests/test_session_export_markdown.py, 9 tests) - 400 on missing session_id, 404 on unknown session - correct Content-Type / Content-Disposition / Content-Length headers - markdown body renders user/assistant messages with correct heading levels - tool messages are skipped (not exported) - array content text blocks are joined, non-text blocks ignored - attachments emit the _Files: a, b_ marker - default JSON export still works (no regression) - boot.js click handler is pinned to the server endpoint (no createObjectURL) Replaces tests/test_transcript_download_robust.py from PR #3 — that file asserted the blob-URL pattern which we're now removing entirely. Co-authored-by: Copilot <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
PR #3 fixed the most common hang causes for the Transcript download (sync
URL.revokeObjectURLrace +<a>not attached to DOM). However, users still reported the download UI never reaching 'complete' even though the file was fully written to disk.The remaining issue is a Chromium quirk with
blob:URL downloads: the browser's progress UI cannot reliably tell when a blob stream ends, so the download bar stays in an indeterminate 'in progress' state forever, even though the file is on disk.What
Bypass blob URLs entirely. Serve the markdown from the server with proper
Content-Disposition+Content-Length— exactly like the existing/api/session/exportJSON path, which works flawlessly.Server (
api/routes.py)_build_session_markdown(session)mirrors the JStranscript()inmessages.js(skip tool messages, join array text blocks, append attachments marker)_handle_session_export_markdown(handler, parsed)runsredact_session_data()then writestext/markdown; charset=utf-8withContent-Disposition: attachment; filename="<title>.md",Content-Length,Cache-Control: no-store/api/session/export?format=mdto the new handler; default JSON path unchangedClient (
static/boot.js)#btnDownloadclick handler now navigates to/api/session/export?session_id=...&format=md<a>pattern (display:none + appendChild + click() + setTimeout removeChild)showToaston success and failureTests
tests/test_session_export_markdown.py(9 tests, all green):_Files: a, b_markercreateObjectURLno longer referenced for transcript)tests/test_transcript_download_robust.py(from PR #3) is removed — it asserted the blob-URL pattern that we're deleting.Manual verification
Co-authored-by: Copilot [email protected]