Align copied paths with chat references

This commit is contained in:
Rod Boev 2026-07-04 15:26:25 -04:00
parent 670976a280
commit 71a6259c84
6 changed files with 54 additions and 33 deletions

View file

@ -484,6 +484,7 @@ const LOCALES = {
reveal_in_finder: 'Reveal in File Manager',
reveal_failed: 'Failed to reveal: ',
copy_file_path: 'Copy file path',
copy_relative_path: 'Copy relative path',
open_in_vscode: 'Open in VS Code',
open_in_vscode_failed: 'Failed to open in VS Code: ',
download_folder: 'Download Folder',

View file

@ -1645,7 +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="btnCopyPreviewRelPath" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="copyPreviewRelativePath()" title="Copy relative 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_relative_path">Copy relative 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 #btnCopyPreviewPath,.preview-path #btnDownloadFile,.preview-path #btnEditFile,.preview-path #btnRenderMarkdownAnyway{flex-shrink:0;white-space:nowrap;}
.preview-path #btnCopyPreviewRelPath,.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

@ -17301,6 +17301,22 @@ function _showFileContextMenu(e, item){
}
};
menu.appendChild(copyPathItem);
const copyRelPathItem=document.createElement('div');
copyRelPathItem.textContent=t('copy_relative_path');
copyRelPathItem.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:var(--text);';
copyRelPathItem.onmouseenter=()=>copyRelPathItem.style.background='var(--hover-bg)';
copyRelPathItem.onmouseleave=()=>copyRelPathItem.style.background='';
copyRelPathItem.onclick=async()=>{
menu.remove();
try{
const rel=_normalizeWorkspaceRelPath(item.path)||item.path;
await _copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'));
}catch(err){
showToast(t('path_copy_failed')+(err.message||err));
}
};
menu.appendChild(copyRelPathItem);
}
if(isDirLike){

View file

@ -1216,24 +1216,23 @@ 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;
const btn=$('btnCopyPreviewPath');
async function copyPreviewRelativePath(){
if(!_previewCurrentPath) return;
const btn=$('btnCopyPreviewRelPath');
if(btn&&btn.disabled) return;
if(btn) btn.disabled=true;
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;
const rel=_normalizeWorkspaceRelPath(_previewCurrentPath)||_previewCurrentPath;
if(typeof _copyTextWithFallback==='function'){
await _copyTextWithFallback(abs,t('path_copied'),t('path_copy_failed'));
await _copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'));
return;
}
try{
await navigator.clipboard.writeText(abs);
await navigator.clipboard.writeText(rel);
showToast(t('path_copied'));
}catch(clipErr){
const ta=document.createElement('textarea');
ta.value=abs;
ta.value=rel;
ta.style.cssText='position:fixed;left:-9999px;top:-9999px;';
document.body.appendChild(ta);
ta.select();

View file

@ -4,6 +4,7 @@ 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")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
@ -39,54 +40,58 @@ def _compact(src: str) -> str:
return "".join(src.split())
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_toolbar_has_copy_relative_path_button():
assert 'id="btnCopyPreviewRelPath"' in INDEX
assert 'onclick="copyPreviewRelativePath()"' in INDEX
assert 'data-i18n="copy_relative_path"' in INDEX
assert "Copy relative path" in INDEX
def test_preview_copy_file_path_uses_server_resolved_current_preview_path():
body = _function_body(WORKSPACE_JS, "copyPreviewFilePath")
def test_preview_copy_relative_path_uses_current_preview_path():
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
compact = _compact(body)
assert "_previewCurrentPath" in body
assert "S.session" in body
assert body.index("_previewCurrentPath") < body.index("api('/api/file/path'")
assert "api('/api/file/path'" in body
assert "session_id:S.session.session_id" in body
assert "path:_previewCurrentPath" in body
assert "constabs=(r&&r.path)||_previewCurrentPath" in compact
assert "_normalizeWorkspaceRelPath(_previewCurrentPath)" in body
assert "api('/api/file/path'" not in body
assert "constrel=_normalizeWorkspaceRelPath(_previewCurrentPath)||_previewCurrentPath" in compact
def test_preview_copy_file_path_disables_button_while_request_is_in_flight():
body = _function_body(WORKSPACE_JS, "copyPreviewFilePath")
def test_preview_copy_relative_path_disables_button_while_request_is_in_flight():
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
compact = _compact(body)
guard = "if(btn&&btn.disabled)return;"
disable = "if(btn)btn.disabled=true;"
enable = "finally{if(btn)btn.disabled=false;}"
assert "$('btnCopyPreviewPath')" in body
assert "$('btnCopyPreviewRelPath')" in body
assert guard in compact
assert disable in compact
assert enable in compact
assert compact.index(guard) < compact.index(disable)
assert compact.index(disable) < compact.index("api('/api/file/path'")
assert compact.index(disable) < compact.index("_normalizeWorkspaceRelPath")
def test_preview_copy_file_path_reuses_clipboard_fallback_and_toasts():
body = _function_body(WORKSPACE_JS, "copyPreviewFilePath")
def test_preview_copy_relative_path_reuses_clipboard_fallback_and_toasts():
body = _function_body(WORKSPACE_JS, "copyPreviewRelativePath")
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 "_copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'))" in body
assert "navigator.clipboard.writeText(rel)" in body
assert "document.execCommand('copy')" in body
assert "t('path_copied')" in body
assert "t('path_copy_failed')" in body
def test_tree_context_menu_keeps_absolute_copy_and_adds_relative_copy():
assert "copyPathItem.textContent=t('copy_file_path')" in UI_JS
assert "copyRelPathItem.textContent=t('copy_relative_path')" in UI_JS
assert "const rel=_normalizeWorkspaceRelPath(item.path)||item.path" in UI_JS
assert "_copyTextWithFallback(rel,t('path_copied'),t('path_copy_failed'))" in UI_JS
def test_preview_toolbar_keeps_copy_button_from_shrinking_path_layout():
assert ".preview-path #btnCopyPreviewPath" in STYLE
selector_start = STYLE.index(".preview-path #btnCopyPreviewPath")
assert ".preview-path #btnCopyPreviewRelPath" in STYLE
selector_start = STYLE.index(".preview-path #btnCopyPreviewRelPath")
selector_block = STYLE[selector_start : STYLE.index("}", selector_start) + 1]
assert "flex-shrink:0" in selector_block
assert "white-space:nowrap" in selector_block