feat(#5533): make preview file paths copyable

This commit is contained in:
Rod Boev 2026-07-04 13:37:39 -04:00
parent 980e98dc7e
commit 7ebd898c5e
4 changed files with 100 additions and 1 deletions

View file

@ -1645,6 +1645,7 @@
<div class="preview-path" id="previewPath">
<span id="previewPathText"></span>
<span class="preview-badge" id="previewBadge"></span>
<button id="btnCopyPreviewPath" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="copyPreviewFilePath()" title="Copy file path"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <span data-i18n="copy_file_path">Copy file path</span></button>
<button id="btnOpenInBrowser" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="openInBrowser()" title="Open in new browser tab"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> <span data-i18n="open_in_browser">Open in browser</span></button>
<button id="btnRenderMarkdownAnyway" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="forceRenderMarkdownPreview()" title="Render this large file as rich Markdown"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M16 3h3a2 2 0 0 1 2 2v3"/><path d="M8 21H5a2 2 0 0 1-2-2v-3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/><path d="M7 8h10"/><path d="M7 12h10"/><path d="M7 16h6"/></svg> Render as markdown anyway</button>
<button id="btnDownloadFile" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="downloadFile(_previewCurrentPath)" title="Download file to your computer"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Download</button>

View file

@ -3377,7 +3377,7 @@ body.tts-enabled .msg-tts-btn{display:inline-flex;align-items:center;}
.preview-path{display:flex;align-items:center;gap:6px;flex-wrap:nowrap;overflow:hidden;min-width:0;}
.preview-path #previewPathText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.preview-path #previewBadge{flex-shrink:0;white-space:nowrap;}
.preview-path #btnDownloadFile,.preview-path #btnEditFile,.preview-path #btnRenderMarkdownAnyway{flex-shrink:0;white-space:nowrap;}
.preview-path #btnCopyPreviewPath,.preview-path #btnDownloadFile,.preview-path #btnEditFile,.preview-path #btnRenderMarkdownAnyway{flex-shrink:0;white-space:nowrap;}
/* Preview badge typography */
.preview-badge{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;}

View file

@ -1216,6 +1216,35 @@ function openInBrowser(){
}
// openInBrowser keeps the helper-based raw path, which expands to an explicit &inline=1 URL.
async function copyPreviewFilePath(){
if(!_previewCurrentPath||!S.session) return;
try{
const r=await api('/api/file/path',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:_previewCurrentPath})});
const abs=(r&&r.path)||_previewCurrentPath;
if(typeof _copyTextWithFallback==='function'){
await _copyTextWithFallback(abs,t('path_copied'),t('path_copy_failed'));
return;
}
try{
await navigator.clipboard.writeText(abs);
showToast(t('path_copied'));
}catch(clipErr){
const ta=document.createElement('textarea');
ta.value=abs;
ta.style.cssText='position:fixed;left:-9999px;top:-9999px;';
document.body.appendChild(ta);
ta.select();
let copied=false;
try{copied=document.execCommand('copy');}catch(_){}
ta.remove();
if(copied) showToast(t('path_copied'));
else showToast(t('path_copy_failed')+(clipErr&&clipErr.message?clipErr.message:String(clipErr)));
}
}catch(err){
showToast(t('path_copy_failed')+(err.message||err));
}
}
// ── Workspace upload ──────────────────────────────────────────────────
function triggerWorkspaceUpload() {
if(_workspacePathIsReadOnly(S.currentDir || '.')){

View file

@ -0,0 +1,69 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
INDEX = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
WORKSPACE_JS = (ROOT / "static" / "workspace.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
def _function_body(src: str, name: str) -> str:
start = src.index(f"function {name}(")
brace = src.index("{", start)
depth = 0
in_string = ""
escape = False
for idx in range(brace, len(src)):
ch = src[idx]
if in_string:
if escape:
escape = False
elif ch == "\\":
escape = True
elif ch == in_string:
in_string = ""
continue
if ch in "'\"`":
in_string = ch
continue
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return src[start : idx + 1]
raise AssertionError(f"{name} body did not close")
def test_preview_toolbar_has_copy_file_path_button():
assert 'id="btnCopyPreviewPath"' in INDEX
assert 'onclick="copyPreviewFilePath()"' in INDEX
assert 'data-i18n="copy_file_path"' in INDEX
assert "Copy file path" in INDEX
def test_preview_copy_file_path_uses_server_resolved_current_preview_path():
body = _function_body(WORKSPACE_JS, "copyPreviewFilePath")
assert "if(!_previewCurrentPath||!S.session) return;" in body
assert "api('/api/file/path'" in body
assert "session_id:S.session.session_id" in body
assert "path:_previewCurrentPath" in body
assert "const abs=(r&&r.path)||_previewCurrentPath" in body
def test_preview_copy_file_path_reuses_clipboard_fallback_and_toasts():
body = _function_body(WORKSPACE_JS, "copyPreviewFilePath")
assert "typeof _copyTextWithFallback==='function'" in body
assert "_copyTextWithFallback(abs,t('path_copied'),t('path_copy_failed'))" in body
assert "navigator.clipboard.writeText(abs)" in body
assert "document.execCommand('copy')" in body
assert "t('path_copied')" in body
assert "t('path_copy_failed')" in body
def test_preview_toolbar_keeps_copy_button_from_shrinking_path_layout():
assert ".preview-path #btnCopyPreviewPath" in STYLE
selector_start = STYLE.index(".preview-path #btnCopyPreviewPath")
selector_block = STYLE[selector_start : STYLE.index("}", selector_start) + 1]
assert "flex-shrink:0" in selector_block
assert "white-space:nowrap" in selector_block