diff --git a/static/i18n.js b/static/i18n.js
index 4055d47a..5ef083ff 100644
--- a/static/i18n.js
+++ b/static/i18n.js
@@ -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',
diff --git a/static/index.html b/static/index.html
index 7e24f0fc..e439b89a 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1645,7 +1645,7 @@
-
+
diff --git a/static/style.css b/static/style.css
index d7943d8c..3bdc84ed 100644
--- a/static/style.css
+++ b/static/style.css
@@ -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;}
diff --git a/static/ui.js b/static/ui.js
index 170eb14e..78b34ea3 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -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){
diff --git a/static/workspace.js b/static/workspace.js
index 35b0c4bf..97dbc4af 100644
--- a/static/workspace.js
+++ b/static/workspace.js
@@ -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();
diff --git a/tests/test_issue5533_preview_copy_file_path.py b/tests/test_issue5533_preview_copy_file_path.py
index b611234e..9d4ad821 100644
--- a/tests/test_issue5533_preview_copy_file_path.py
+++ b/tests/test_issue5533_preview_copy_file_path.py
@@ -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