Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,9 @@ def verify_hermes_imports() -> tuple:
".json": "application/json",
".html": "text/html",
".htm": "text/html",
".md": "text/markdown",
".mkd": "text/markdown",
".mkdn": "text/markdown",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".doc": "application/msword",
Expand Down
2 changes: 1 addition & 1 deletion api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18767,7 +18767,7 @@ def _handle_media(handler, parsed):
"video/mp4", "video/quicktime", "video/webm", "video/ogg",
"application/pdf",
}
_SESSION_MEDIA_TOKEN_TYPES = _INLINE_IMAGE_TYPES | _AUDIO_VIDEO_PDF_TYPES | {"text/html"}
_SESSION_MEDIA_TOKEN_TYPES = _INLINE_IMAGE_TYPES | _AUDIO_VIDEO_PDF_TYPES | {"text/html", "text/markdown"}
session_media_allowed = _session_media_token_allows_path(
qs.get("session_id", [""])[0],
target,
Expand Down
11 changes: 7 additions & 4 deletions static/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1469,10 +1469,13 @@ async function send(){
// Open the native WebUI session browser rather than sending as chat text (#6224).
// Use the mobile-aware opener so phone-width layouts (where expandSidebar is a
// no-op) actually reveal the session drawer.
if(typeof _openProfileSwitchSessionBrowser==='function') _openProfileSwitchSessionBrowser();
else if(typeof expandSidebar==='function') expandSidebar();
if(typeof renderSessionList==='function') await renderSessionList();
$('msg').value='';autoResize();hideCmdDropdown();return;
try {
if(typeof _openProfileSwitchSessionBrowser==='function') _openProfileSwitchSessionBrowser();
else if(typeof expandSidebar==='function') expandSidebar();
if(typeof renderSessionList==='function') await renderSessionList();
} finally {
$('msg').value='';autoResize();hideCmdDropdown();return;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
const _agentCmd=typeof getAgentCommandMetadata==='function'
? await getAgentCommandMetadata(_parsedCmd.name)
Expand Down
11 changes: 11 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -6582,6 +6582,17 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.html-preview-fallback{padding:8px;font-size:13px;}
.html-preview-spinner{animation:pulse 1.5s ease-in-out infinite;}

/* ── Markdown inline preview ──────────────────────────────────────────────── */
.md-inline-load{color:var(--muted);font-size:13px;padding:8px 12px;border:1px dashed var(--border);border-radius:8px;margin:6px 0;}
.md-preview-spinner{animation:pulse 1.5s ease-in-out infinite;}
.md-inline-wrap{border:1px solid var(--border);border-radius:6px;overflow:hidden;margin:4px 0;background:var(--surface);}
.md-inline-header{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--surface-2);font-size:12px;font-weight:600;border-bottom:1px solid var(--border);}
.md-preview-title{color:var(--text);}
.md-inline-fallback{padding:8px;font-size:13px;}
.md-inline-content{padding:12px 16px;overflow-x:auto;}
.md-inline-content > :first-child{margin-top:0;}
.md-inline-content > :last-child{margin-bottom:0;}

/* ── Insights panel (#464) ────────────────────────────────────────────────── */
main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.insights-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:16px;}
Expand Down
33 changes: 33 additions & 0 deletions static/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2499,6 +2499,7 @@ const _SVG_EXTS=/\.svg$/i;
const _AUDIO_EXTS=/\.(mp3|ogg|wav|m4a|aac|flac|wma|opus|webm|oga)$/i;
const _VIDEO_EXTS=/\.(mp4|webm|mkv|mov|avi|ogv|m4v)$/i;
const _CSV_EXTS=/\.csv$/i;
const _MD_EXTS=/\.(md|mkd|mkdn)$/i;
const _EXCALIDRAW_EXTS=/\.excalidraw$/i;
// ── Media playback speed controls ─────────────────────────────────────────
const MEDIA_PLAYBACK_RATES=[0.5,0.75,1,1.25,1.5,2];
Expand Down Expand Up @@ -2661,6 +2662,9 @@ function _inlineMediaHtmlForRef(ref, sessionId, altText){
if(_HTML_EXTS.test(ref)){
return `<div class="html-preview-load" data-path="${esc(ref)}"><span class="html-preview-spinner">⏳</span> ${esc(typeof t==='function'?t('html_loading'):'Loading')}...</div>`;
}
if(_MD_EXTS.test(ref)){
return `<div class="md-inline-load" data-path="${esc(ref)}"><span class="md-preview-spinner">⏳</span> ${esc(typeof t==='function'?t('md_loading'):'Loading')}...</div>`;
}
const fname=esc(ref.split('/').pop()||ref);
if(/\.(patch|diff)$/i.test(ref)) return `<div class="diff-inline-load" data-path="${esc(ref)}">${esc(typeof t==='function'?t('diff_loading'):'Loading diff')} ${fname}...</div>`;
if(_CSV_EXTS.test(ref)) return `<div class="csv-inline-load" data-path="${esc(ref)}">${esc(typeof t==='function'?t('csv_loading'):'Loading')} ${fname}...</div>`;
Expand Down Expand Up @@ -18087,6 +18091,7 @@ function postProcessRenderedMessages(container) {
loadExcalidrawInline(container);
loadPdfInline(container);
loadHtmlInline(container);
loadMarkdownInline(container);
renderMermaidBlocks(container);
renderKatexBlocks(container);
initTreeViews(container);
Expand Down Expand Up @@ -18654,6 +18659,34 @@ function loadHtmlInline(container){
});
}

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>`;
});
});
}
Comment on lines +18662 to +18688

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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 &lt;script&gt; (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.

Comment on lines +18662 to +18688

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!


function renderMermaidBlocks(container){
const root=container||document;
const blocks=root.querySelectorAll('.mermaid-block:not([data-rendered])');
Expand Down
Loading