fix(slash): wrap sessions/resume handler in try/finally for robustness (#6224)#6278
fix(slash): wrap sessions/resume handler in try/finally for robustness (#6224)#6278webtecnica wants to merge 3 commits into
Conversation
Closes common issue where clicking MEDIA: links for .md files could download a JSON auth error on desktop. Three-layer fix: 1. MIME mapping (api/config.py): add .md/.mkd/.mkdn → text/markdown so the server returns the correct Content-Type instead of application/octet-stream 2. Session allow-list (api/routes.py): add text/markdown to _SESSION_MEDIA_TOKEN_TYPES so .md files are accepted by the session-token path check 3. Frontend preview (static/ui.js + static/style.css): detect .md files in _inlineMediaHtmlForRef, fetch content via api/media, render through the existing renderMd() pipeline, and display inline in a styled container — same pattern as HTML/CSV previews
nesquena#6224) If renderSessionList() throws (network error), the try/finally ensures the composer is still cleared and the dropdown is hidden.
| const MD_MAX_SIZE = 256 * 1024; | ||
|
|
||
| function loadMarkdownInline(container){ | ||
| const root = container || document; | ||
| root.querySelectorAll('.md-inline-load:not([data-loaded])').forEach(el => { | ||
| el.setAttribute('data-loaded', '1'); | ||
| const path = el.dataset.path; | ||
| const fname = path.split('/').pop() || path; | ||
| const mediaSessionId = (typeof S !== 'undefined' && S && S.session && S.session.session_id) ? String(S.session.session_id) : ''; | ||
| const publicMediaUrl = 'api/media?path=' + encodeURIComponent(path); | ||
| const mediaUrl = publicMediaUrl + (mediaSessionId ? '&session_id=' + encodeURIComponent(mediaSessionId) : ''); | ||
| const downloadUrl = mediaUrl + '&download=1'; | ||
| fetch(mediaUrl) | ||
| .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) | ||
| .then(text => { | ||
| if (text.length > MD_MAX_SIZE) { | ||
| el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_too_large') : 'File too large for preview')}</span></div>`; | ||
| return; | ||
| } | ||
| const rendered = renderMd(text); | ||
| el.outerHTML = `<div class="md-inline-wrap"><div class="md-inline-header"><span class="md-preview-title">${esc(fname)}</span><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a></div><div class="md-inline-content">${rendered}</div></div>`; | ||
| }) | ||
| .catch(() => { | ||
| el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_error') : 'Error loading markdown')}</span></div>`; | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
renderMd applies LLM-oriented heuristics to user-authored file content
renderMd runs two rounds of entity-decoding (lines 7090 and 7164–7165) before HTML-tag processing. This means a markdown file that legitimately contains <script> (to display the literal text <script>) will have the entity decoded to <script>, which is then stripped entirely by _tag(). The displayed text becomes empty instead of <script>. For LLM-produced chat messages this is the right trade-off, but for user-authored .md files it silently corrupts intentional escape sequences. A renderer without the double-decode pre-pass (or with a separate sanitise-only path) would be more faithful to the file's intent.
| const MD_MAX_SIZE = 256 * 1024; | ||
|
|
||
| function loadMarkdownInline(container){ | ||
| const root = container || document; | ||
| root.querySelectorAll('.md-inline-load:not([data-loaded])').forEach(el => { | ||
| el.setAttribute('data-loaded', '1'); | ||
| const path = el.dataset.path; | ||
| const fname = path.split('/').pop() || path; | ||
| const mediaSessionId = (typeof S !== 'undefined' && S && S.session && S.session.session_id) ? String(S.session.session_id) : ''; | ||
| const publicMediaUrl = 'api/media?path=' + encodeURIComponent(path); | ||
| const mediaUrl = publicMediaUrl + (mediaSessionId ? '&session_id=' + encodeURIComponent(mediaSessionId) : ''); | ||
| const downloadUrl = mediaUrl + '&download=1'; | ||
| fetch(mediaUrl) | ||
| .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) | ||
| .then(text => { | ||
| if (text.length > MD_MAX_SIZE) { | ||
| el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_too_large') : 'File too large for preview')}</span></div>`; | ||
| return; | ||
| } | ||
| const rendered = renderMd(text); | ||
| el.outerHTML = `<div class="md-inline-wrap"><div class="md-inline-header"><span class="md-preview-title">${esc(fname)}</span><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a></div><div class="md-inline-content">${rendered}</div></div>`; | ||
| }) | ||
| .catch(() => { | ||
| el.outerHTML = `<div class="md-inline-fallback"><a class="msg-media-link" href="${esc(downloadUrl)}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${esc(typeof t === 'function' ? t('md_error') : 'Error loading markdown')}</span></div>`; | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
PR bundles two unrelated logical changes
AGENTS.md states "Keep one logical change per PR; split unrelated refactors or cleanup." This PR's stated fix is wrapping the /sessions+/resume handler in try/finally (the one-line change in messages.js), but it also introduces a complete markdown inline-preview feature spanning api/config.py, api/routes.py, static/style.css, and a new loadMarkdownInline function in static/ui.js. These two changes are independent and would be easier to review, revert, and bisect if split into separate PRs.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
🎬 Cutter preview — PR #6278
|
SummaryThe cleanup goal is correct, but the Code references
In JavaScript, abrupt completion from a try {
// open the session browser and await renderSessionList()
} finally {
document.getElementById('msg').value = '';
autoResize();
hideCmdDropdown();
}
return;That preserves the requested composer/dropdown cleanup on both paths while allowing failures to propagate to whatever invokes Suggested verificationAdd a focused test with |

The /sessions and /resume slash intercept (from #6245) didn't wrap the handler body. If renderSessionList() throws (e.g. network error), the composer stays dirty and the dropdown remains open.
Fix: Wrap in try/finally — cleanup always runs.
Refs #6224