@co0ontty/wand 1.21.12 → 1.21.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +254 -31
- package/dist/structured-session-manager.d.ts +6 -2
- package/dist/structured-session-manager.js +170 -97
- package/dist/types.d.ts +24 -0
- package/dist/web-ui/content/scripts.js +709 -138
- package/dist/web-ui/content/styles.css +313 -0
- package/package.json +1 -1
|
@@ -215,6 +215,12 @@
|
|
|
215
215
|
fileSearchQuery: "",
|
|
216
216
|
fileExplorerLoading: false,
|
|
217
217
|
allFiles: [],
|
|
218
|
+
fileExplorerCwd: "",
|
|
219
|
+
fileExplorerTruncated: false,
|
|
220
|
+
fileExplorerTotal: 0,
|
|
221
|
+
fileExplorerShowHidden: (function() {
|
|
222
|
+
try { return localStorage.getItem("wand-file-show-hidden") === "1"; } catch (e) { return false; }
|
|
223
|
+
})(),
|
|
218
224
|
claudeHistory: [],
|
|
219
225
|
claudeHistoryLoaded: false,
|
|
220
226
|
claudeHistoryExpanded: true,
|
|
@@ -1346,16 +1352,7 @@
|
|
|
1346
1352
|
'<div class="topbar-right">' +
|
|
1347
1353
|
(selectedSession && selectedSession.cwd ? '<button id="topbar-file-button" class="topbar-btn square' + (state.filePanelOpen ? ' active' : '') + '" type="button" aria-label="文件" title="文件"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>' : '') +
|
|
1348
1354
|
'<span id="topbar-git-slot" class="topbar-git-slot">' + renderTopbarGitBadgeHtml() + '</span>' +
|
|
1349
|
-
|
|
1350
|
-
'<button id="topbar-more-button" class="topbar-btn square' + (state.topbarMoreOpen ? ' active' : '') + '" type="button" aria-label="更多" aria-haspopup="menu" aria-expanded="' + (state.topbarMoreOpen ? 'true' : 'false') + '" title="更多"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>' +
|
|
1351
|
-
'<div id="topbar-more-menu" class="topbar-more-menu' + (state.topbarMoreOpen ? '' : ' hidden') + '" role="menu">' +
|
|
1352
|
-
'<button class="topbar-more-item" data-action="settings" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span>设置</span></button>' +
|
|
1353
|
-
'<button class="topbar-more-item" data-action="refresh" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg><span>刷新</span></button>' +
|
|
1354
|
-
'<button class="topbar-more-item' + (state.showInstallPrompt && state.deferredPrompt ? '' : ' hidden') + '" id="topbar-install-item" data-action="install" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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><span>安装应用</span></button>' +
|
|
1355
|
-
(hasNativeSwitchServer() ? '<button class="topbar-more-item" data-action="switch-server" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg><span>切换服务器</span></button>' : '') +
|
|
1356
|
-
'<button class="topbar-more-item topbar-more-item-danger" data-action="logout" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg><span>退出</span></button>' +
|
|
1357
|
-
'</div>' +
|
|
1358
|
-
'</div>' +
|
|
1355
|
+
(selectedSession ? renderTopbarMoreMenuHtml(selectedSession) : '') +
|
|
1359
1356
|
'</div>' +
|
|
1360
1357
|
'</div>' +
|
|
1361
1358
|
// File panel backdrop (mobile)
|
|
@@ -1368,7 +1365,9 @@
|
|
|
1368
1365
|
'</div>' +
|
|
1369
1366
|
'<div class="file-side-panel-body">' +
|
|
1370
1367
|
'<div class="file-explorer-header">' +
|
|
1371
|
-
'<
|
|
1368
|
+
'<button class="file-explorer-up" id="file-explorer-up" type="button" title="返回上级目录" aria-label="返回上级目录">⬆</button>' +
|
|
1369
|
+
'<span class="file-explorer-path" id="file-explorer-cwd" title="' + escapeHtml(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '">' + escapeHtml(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</span>' +
|
|
1370
|
+
'<button class="file-explorer-toggle-hidden' + (state.fileExplorerShowHidden ? ' active' : '') + '" id="file-explorer-toggle-hidden" type="button" title="' + (state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件") + '" aria-pressed="' + (state.fileExplorerShowHidden ? "true" : "false") + '">' + (state.fileExplorerShowHidden ? "👁" : "👁🗨") + '</button>' +
|
|
1372
1371
|
'<button class="file-explorer-refresh" id="file-explorer-refresh" title="刷新" aria-label="刷新文件列表">↻</button>' +
|
|
1373
1372
|
'</div>' +
|
|
1374
1373
|
'<div class="file-search-box">' +
|
|
@@ -1549,6 +1548,60 @@
|
|
|
1549
1548
|
}
|
|
1550
1549
|
}
|
|
1551
1550
|
|
|
1551
|
+
/**
|
|
1552
|
+
* Render the topbar three-dot menu. Items are scoped to the currently
|
|
1553
|
+
* selected session — global actions (settings/install/switch-server/
|
|
1554
|
+
* logout) live in the sidebar footer, so we don't duplicate them here.
|
|
1555
|
+
*/
|
|
1556
|
+
function renderTopbarMoreMenuHtml(session) {
|
|
1557
|
+
if (!session) return "";
|
|
1558
|
+
var open = state.topbarMoreOpen;
|
|
1559
|
+
var hasClaudeId = !!session.claudeSessionId;
|
|
1560
|
+
var hasCwd = !!session.cwd;
|
|
1561
|
+
var canOpenMerge = session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
1562
|
+
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
1563
|
+
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
1564
|
+
var showMerge = canOpenMerge && session.worktreeMergeStatus !== "merged";
|
|
1565
|
+
var showCleanup = needsCleanup;
|
|
1566
|
+
var hasInfoGroup = hasClaudeId || hasCwd || true; // session-id button always renders
|
|
1567
|
+
var hasActionGroup = showMerge || showCleanup || true; // delete button always renders
|
|
1568
|
+
|
|
1569
|
+
var copyIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>';
|
|
1570
|
+
var cloudIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19a4.5 4.5 0 1 0-1.5-8.74A6 6 0 1 0 6 14h11.5z"/></svg>';
|
|
1571
|
+
var folderIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
1572
|
+
var hashIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg>';
|
|
1573
|
+
var mergeIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/><path d="M5 7l-2 2 2 2"/><path d="M19 15l2 2-2 2"/></svg>';
|
|
1574
|
+
var trashIconSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>';
|
|
1575
|
+
|
|
1576
|
+
var infoItems = "";
|
|
1577
|
+
if (hasClaudeId) {
|
|
1578
|
+
infoItems += '<button class="topbar-more-item" data-action="copy-claude-session-id" type="button" role="menuitem">' + cloudIconSvg + '<span>复制 Claude 会话 ID</span></button>';
|
|
1579
|
+
}
|
|
1580
|
+
if (hasCwd) {
|
|
1581
|
+
infoItems += '<button class="topbar-more-item" data-action="copy-cwd" type="button" role="menuitem">' + folderIconSvg + '<span>复制工作目录</span></button>';
|
|
1582
|
+
}
|
|
1583
|
+
infoItems += '<button class="topbar-more-item" data-action="copy-session-id" type="button" role="menuitem">' + hashIconSvg + '<span>复制会话 ID</span></button>';
|
|
1584
|
+
|
|
1585
|
+
var actionItems = "";
|
|
1586
|
+
if (showMerge) {
|
|
1587
|
+
actionItems += '<button class="topbar-more-item" data-action="worktree-merge" type="button" role="menuitem"' + (mergeDisabled ? ' disabled' : '') + '>' + mergeIconSvg + '<span>合并到主分支…</span></button>';
|
|
1588
|
+
} else if (showCleanup) {
|
|
1589
|
+
actionItems += '<button class="topbar-more-item" data-action="worktree-cleanup" type="button" role="menuitem">' + mergeIconSvg + '<span>重试 worktree 清理</span></button>';
|
|
1590
|
+
}
|
|
1591
|
+
actionItems += '<button class="topbar-more-item topbar-more-item-danger" data-action="delete-session" type="button" role="menuitem">' + trashIconSvg + '<span>删除当前会话</span></button>';
|
|
1592
|
+
|
|
1593
|
+
var divider = (hasInfoGroup && hasActionGroup) ? '<div class="topbar-more-divider" role="separator"></div>' : '';
|
|
1594
|
+
|
|
1595
|
+
return '<div class="topbar-more-wrap">' +
|
|
1596
|
+
'<button id="topbar-more-button" class="topbar-btn square' + (open ? ' active' : '') + '" type="button" aria-label="当前会话操作" aria-haspopup="menu" aria-expanded="' + (open ? 'true' : 'false') + '" title="当前会话操作"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>' +
|
|
1597
|
+
'<div id="topbar-more-menu" class="topbar-more-menu' + (open ? '' : ' hidden') + '" role="menu" aria-label="当前会话">' +
|
|
1598
|
+
infoItems +
|
|
1599
|
+
divider +
|
|
1600
|
+
actionItems +
|
|
1601
|
+
'</div>' +
|
|
1602
|
+
'</div>';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1552
1605
|
function loadGitStatus(sessionId, options) {
|
|
1553
1606
|
if (!sessionId) return Promise.resolve(null);
|
|
1554
1607
|
var force = options && options.force;
|
|
@@ -2852,118 +2905,205 @@
|
|
|
2852
2905
|
function renderFileExplorer(cwd) {
|
|
2853
2906
|
var root = cwd || getConfigCwd();
|
|
2854
2907
|
if (!root) {
|
|
2855
|
-
return '<div class="file-explorer empty"
|
|
2908
|
+
return '<div class="file-explorer empty">未配置工作目录。</div>';
|
|
2856
2909
|
}
|
|
2857
2910
|
return '<div class="file-tree" id="file-tree" data-cwd="' + escapeHtml(root) + '">' +
|
|
2858
|
-
'<div class="tree-loading"
|
|
2911
|
+
'<div class="tree-loading">加载中…</div>' +
|
|
2859
2912
|
'</div>';
|
|
2860
2913
|
}
|
|
2861
2914
|
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2915
|
+
// ── File tree helpers ──
|
|
2916
|
+
|
|
2917
|
+
var FILE_ICON_MAP = {
|
|
2918
|
+
// images
|
|
2919
|
+
png: "🖼️", jpg: "🖼️", jpeg: "🖼️", gif: "🖼️", webp: "🖼️", svg: "🖼️",
|
|
2920
|
+
avif: "🖼️", bmp: "🖼️", ico: "🖼️", heic: "🖼️", heif: "🖼️",
|
|
2921
|
+
// pdf / doc
|
|
2922
|
+
pdf: "📕", doc: "📘", docx: "📘", odt: "📘",
|
|
2923
|
+
xls: "📊", xlsx: "📊", csv: "📊", tsv: "📊",
|
|
2924
|
+
ppt: "📙", pptx: "📙",
|
|
2925
|
+
// video / audio
|
|
2926
|
+
mp4: "🎬", webm: "🎬", mov: "🎬", mkv: "🎬", m4v: "🎬", ogv: "🎬",
|
|
2927
|
+
mp3: "🎵", wav: "🎵", ogg: "🎵", m4a: "🎵", flac: "🎵", aac: "🎵", opus: "🎵",
|
|
2928
|
+
// archives
|
|
2929
|
+
zip: "📦", tar: "📦", gz: "📦", tgz: "📦", bz2: "📦", "7z": "📦", rar: "📦", xz: "📦",
|
|
2930
|
+
// markup / docs
|
|
2931
|
+
md: "📝", markdown: "📝", mdx: "📝", rst: "📝", txt: "📝", log: "📝",
|
|
2932
|
+
// web / styles
|
|
2933
|
+
html: "🌐", htm: "🌐", xml: "🌐",
|
|
2934
|
+
css: "🎨", scss: "🎨", less: "🎨",
|
|
2935
|
+
// configs
|
|
2936
|
+
json: "⚙️", jsonc: "⚙️", yaml: "⚙️", yml: "⚙️", toml: "⚙️",
|
|
2937
|
+
ini: "⚙️", cfg: "⚙️", conf: "⚙️", env: "⚙️", editorconfig: "⚙️",
|
|
2938
|
+
// code (default 📜)
|
|
2939
|
+
ts: "📜", tsx: "📜", js: "📜", jsx: "📜", mjs: "📜", cjs: "📜",
|
|
2940
|
+
py: "📜", rb: "📜", go: "📜", rs: "📜", java: "📜", c: "📜", cpp: "📜",
|
|
2941
|
+
h: "📜", hpp: "📜", cs: "📜", swift: "📜", kt: "📜", scala: "📜",
|
|
2942
|
+
php: "📜", sh: "📜", bash: "📜", zsh: "📜", fish: "📜", lua: "📜",
|
|
2943
|
+
sql: "📜", graphql: "📜", proto: "📜", vue: "📜", svelte: "📜",
|
|
2944
|
+
diff: "📜", patch: "📜",
|
|
2945
|
+
// fonts / binary
|
|
2946
|
+
ttf: "🔤", otf: "🔤", woff: "🔤", woff2: "🔤", eot: "🔤",
|
|
2947
|
+
};
|
|
2948
|
+
|
|
2949
|
+
function getFileIcon(item) {
|
|
2950
|
+
if (!item) return "📄";
|
|
2951
|
+
if (item.type === "dir") return "📁";
|
|
2952
|
+
var name = (item.name || "").toLowerCase();
|
|
2953
|
+
// basename-only matches first
|
|
2954
|
+
if (name === "dockerfile") return "🐳";
|
|
2955
|
+
if (name === "makefile") return "🛠️";
|
|
2956
|
+
if (name === "license") return "📜";
|
|
2957
|
+
if (name === "readme") return "📝";
|
|
2958
|
+
var dot = name.lastIndexOf(".");
|
|
2959
|
+
if (dot < 0 || dot === name.length - 1) return "📄";
|
|
2960
|
+
var ext = name.slice(dot + 1);
|
|
2961
|
+
return FILE_ICON_MAP[ext] || "📄";
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
function formatFileSize(bytes) {
|
|
2965
|
+
if (typeof bytes !== "number" || !isFinite(bytes) || bytes < 0) return "";
|
|
2966
|
+
if (bytes < 1024) return bytes + " B";
|
|
2967
|
+
var kb = bytes / 1024;
|
|
2968
|
+
if (kb < 1024) return (kb >= 10 ? Math.round(kb) : kb.toFixed(1)) + " KB";
|
|
2969
|
+
var mb = kb / 1024;
|
|
2970
|
+
if (mb < 1024) return (mb >= 10 ? Math.round(mb) : mb.toFixed(1)) + " MB";
|
|
2971
|
+
var gb = mb / 1024;
|
|
2972
|
+
return (gb >= 10 ? Math.round(gb) : gb.toFixed(1)) + " GB";
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function formatRelativeTime(iso) {
|
|
2976
|
+
if (!iso) return "";
|
|
2977
|
+
var t = Date.parse(iso);
|
|
2978
|
+
if (isNaN(t)) return "";
|
|
2979
|
+
return new Date(t).toLocaleString();
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function getEffectiveExplorerCwd() {
|
|
2983
|
+
if (state.fileExplorerCwd) return state.fileExplorerCwd;
|
|
2868
2984
|
if (state.selectedId) {
|
|
2869
2985
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
2870
|
-
if (session
|
|
2871
|
-
}
|
|
2872
|
-
if (!cwd) {
|
|
2873
|
-
cwd = getConfigCwd();
|
|
2986
|
+
if (session && session.cwd) return session.cwd;
|
|
2874
2987
|
}
|
|
2988
|
+
return getConfigCwd();
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function refreshFileExplorer(opts) {
|
|
2992
|
+
opts = opts || {};
|
|
2993
|
+
var explorer = document.getElementById("file-explorer");
|
|
2994
|
+
var cwdEl = document.getElementById("file-explorer-cwd");
|
|
2995
|
+
if (!explorer) return;
|
|
2996
|
+
var cwd = opts.cwd || getEffectiveExplorerCwd();
|
|
2875
2997
|
if (!cwd) {
|
|
2876
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
2998
|
+
explorer.innerHTML = '<div class="file-explorer empty">没有可显示的工作目录。</div>';
|
|
2877
2999
|
return;
|
|
2878
3000
|
}
|
|
3001
|
+
state.fileExplorerCwd = cwd;
|
|
2879
3002
|
state.fileExplorerLoading = true;
|
|
2880
3003
|
state.allFiles = [];
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3004
|
+
state.fileExplorerTruncated = false;
|
|
3005
|
+
state.fileExplorerTotal = 0;
|
|
3006
|
+
explorer.innerHTML = '<div class="file-explorer"><div class="tree-loading" style="padding:12px;color:var(--text-muted);font-size:0.8125rem;">加载中…</div></div>';
|
|
3007
|
+
if (cwdEl) {
|
|
3008
|
+
cwdEl.textContent = cwd;
|
|
3009
|
+
cwdEl.title = cwd;
|
|
3010
|
+
}
|
|
3011
|
+
var url = "/api/directory?q=" + encodeURIComponent(cwd) +
|
|
3012
|
+
"&gitStatus=true" +
|
|
3013
|
+
(state.fileExplorerShowHidden ? "&showHidden=true" : "");
|
|
3014
|
+
fetch(url, { credentials: "same-origin" })
|
|
2886
3015
|
.then(function(res) {
|
|
2887
|
-
if (!res.ok)
|
|
2888
|
-
throw new Error("Failed to load directory.");
|
|
2889
|
-
}
|
|
3016
|
+
if (!res.ok) throw new Error("Failed to load directory.");
|
|
2890
3017
|
return res.json();
|
|
2891
3018
|
})
|
|
2892
|
-
.then(function(
|
|
3019
|
+
.then(function(payload) {
|
|
2893
3020
|
state.fileExplorerLoading = false;
|
|
3021
|
+
// Backend returns { items, truncated, total }; tolerate the legacy array shape too.
|
|
3022
|
+
var items, truncated, total;
|
|
3023
|
+
if (Array.isArray(payload)) {
|
|
3024
|
+
items = payload; truncated = false; total = payload.length;
|
|
3025
|
+
} else {
|
|
3026
|
+
items = (payload && payload.items) || [];
|
|
3027
|
+
truncated = !!(payload && payload.truncated);
|
|
3028
|
+
total = (payload && payload.total) || items.length;
|
|
3029
|
+
}
|
|
2894
3030
|
if (!items || items.length === 0) {
|
|
2895
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3031
|
+
explorer.innerHTML = '<div class="file-explorer empty">空目录或无法访问。</div>';
|
|
2896
3032
|
return;
|
|
2897
3033
|
}
|
|
2898
3034
|
state.allFiles = items;
|
|
3035
|
+
state.fileExplorerTruncated = truncated;
|
|
3036
|
+
state.fileExplorerTotal = total;
|
|
2899
3037
|
filterFileTree();
|
|
2900
3038
|
})
|
|
2901
3039
|
.catch(function() {
|
|
2902
3040
|
state.fileExplorerLoading = false;
|
|
2903
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3041
|
+
explorer.innerHTML = '<div class="file-explorer empty">加载失败,请检查路径或权限。</div>';
|
|
2904
3042
|
});
|
|
2905
3043
|
}
|
|
2906
3044
|
|
|
2907
3045
|
function filterFileTree() {
|
|
2908
3046
|
var explorer = document.getElementById("file-explorer");
|
|
2909
|
-
var cwdEl = document.getElementById("file-explorer-cwd");
|
|
2910
3047
|
if (!explorer) return;
|
|
2911
|
-
var cwd =
|
|
3048
|
+
var cwd = state.fileExplorerCwd || "";
|
|
2912
3049
|
if (!cwd) return;
|
|
2913
3050
|
|
|
2914
3051
|
var query = state.fileSearchQuery;
|
|
2915
3052
|
var items = state.allFiles || [];
|
|
3053
|
+
var filtered = items;
|
|
2916
3054
|
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
}).join("") +
|
|
2923
|
-
'</div>';
|
|
2924
|
-
attachFileTreeListeners();
|
|
2925
|
-
return;
|
|
3055
|
+
if (query) {
|
|
3056
|
+
var lowerQuery = query.toLowerCase();
|
|
3057
|
+
filtered = items.filter(function(item) {
|
|
3058
|
+
return item.name.toLowerCase().indexOf(lowerQuery) !== -1;
|
|
3059
|
+
});
|
|
2926
3060
|
}
|
|
2927
3061
|
|
|
2928
|
-
// 模糊匹配文件名(大小写不敏感)
|
|
2929
|
-
var lowerQuery = query.toLowerCase();
|
|
2930
|
-
var filtered = items.filter(function(item) {
|
|
2931
|
-
return item.name.toLowerCase().indexOf(lowerQuery) !== -1;
|
|
2932
|
-
});
|
|
2933
|
-
|
|
2934
3062
|
if (filtered.length === 0) {
|
|
2935
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3063
|
+
explorer.innerHTML = '<div class="file-explorer empty">' + (query ? '没有找到匹配的文件' : '空目录') + '</div>';
|
|
2936
3064
|
return;
|
|
2937
3065
|
}
|
|
2938
3066
|
|
|
3067
|
+
var truncatedNotice = "";
|
|
3068
|
+
if (!query && state.fileExplorerTruncated) {
|
|
3069
|
+
var shown = items.length;
|
|
3070
|
+
truncatedNotice = '<div class="tree-truncated" title="后端按字母序最多返回 ' + shown + ' 项">显示前 ' + shown + ' 项 / 共 ' + state.fileExplorerTotal + ' 项</div>';
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2939
3073
|
explorer.innerHTML = '<div class="file-tree" id="file-tree" data-cwd="' + escapeHtml(cwd) + '">' +
|
|
2940
3074
|
filtered.map(function(item) {
|
|
2941
3075
|
return renderFileTreeItem(item);
|
|
2942
3076
|
}).join("") +
|
|
2943
|
-
'</div>';
|
|
3077
|
+
'</div>' + truncatedNotice;
|
|
2944
3078
|
attachFileTreeListeners();
|
|
2945
3079
|
}
|
|
2946
3080
|
|
|
2947
|
-
function renderFileTreeItem(item) {
|
|
3081
|
+
function renderFileTreeItem(item, depth) {
|
|
3082
|
+
depth = depth || 0;
|
|
2948
3083
|
var name = escapeHtml(item.name);
|
|
2949
3084
|
var isDir = item.type === "dir";
|
|
2950
|
-
|
|
2951
|
-
var displayIcon = isDir ? "📁" : "📄";
|
|
3085
|
+
var displayIcon = getFileIcon(item);
|
|
2952
3086
|
var toggleIcon = isDir ? "▸" : "";
|
|
2953
3087
|
var toggleClass = isDir ? "" : " empty";
|
|
2954
3088
|
var gitStatus = item.gitStatus;
|
|
2955
3089
|
var statusBadge = renderGitStatusBadge(gitStatus);
|
|
2956
|
-
|
|
3090
|
+
var meta = "";
|
|
3091
|
+
if (!isDir && typeof item.size === "number") {
|
|
3092
|
+
meta = '<span class="tree-meta" title="大小:' + escapeHtml(formatFileSize(item.size)) +
|
|
3093
|
+
(item.mtime ? '\n修改时间:' + escapeHtml(formatRelativeTime(item.mtime)) : '') +
|
|
3094
|
+
'">' + escapeHtml(formatFileSize(item.size)) + '</span>';
|
|
3095
|
+
}
|
|
3096
|
+
return '<div class="tree-item" data-path="' + escapeHtml(item.path) + '" data-type="' + escapeHtml(item.type) + '" data-name="' + escapeHtml(item.name) + '" tabindex="0">' +
|
|
2957
3097
|
'<span class="tree-toggle' + toggleClass + '">' + toggleIcon + '</span>' +
|
|
2958
3098
|
'<span class="tree-icon">' + displayIcon + '</span>' +
|
|
2959
3099
|
'<span class="tree-name">' + name + '</span>' +
|
|
3100
|
+
meta +
|
|
2960
3101
|
(statusBadge ? '<span class="git-status-badge ' + statusBadge.class + '" title="' + statusBadge.title + '">' + statusBadge.text + '</span>' : '') +
|
|
2961
3102
|
'</div>';
|
|
2962
3103
|
}
|
|
2963
3104
|
|
|
2964
3105
|
function renderGitStatusBadge(gitStatus) {
|
|
2965
3106
|
if (!gitStatus) return null;
|
|
2966
|
-
// Priority: staged > unstaged > untracked
|
|
2967
3107
|
if (gitStatus.staged === "added") return { text: "A", class: "git-added", title: "已暂存(新增)" };
|
|
2968
3108
|
if (gitStatus.staged === "modified") return { text: "M", class: "git-modified", title: "已暂存(修改)" };
|
|
2969
3109
|
if (gitStatus.staged === "deleted") return { text: "D", class: "git-deleted", title: "已暂存(删除)" };
|
|
@@ -2978,19 +3118,44 @@
|
|
|
2978
3118
|
var tree = document.getElementById("file-tree");
|
|
2979
3119
|
if (!tree) return;
|
|
2980
3120
|
tree.querySelectorAll(".tree-item[data-type='dir']").forEach(function(item) {
|
|
2981
|
-
item.addEventListener("click", function() {
|
|
3121
|
+
item.addEventListener("click", function(e) {
|
|
3122
|
+
// Don't toggle when click came from the meta/badge area
|
|
2982
3123
|
toggleTreeNode(item);
|
|
2983
3124
|
});
|
|
2984
3125
|
});
|
|
2985
3126
|
tree.querySelectorAll(".tree-item[data-type='file']").forEach(function(item) {
|
|
2986
|
-
|
|
2987
|
-
|
|
3127
|
+
var openHandler = function() { openFilePreview(item.dataset.path); };
|
|
3128
|
+
item.addEventListener("click", openHandler);
|
|
3129
|
+
// Keep dblclick for old muscle memory; both work.
|
|
3130
|
+
item.addEventListener("dblclick", openHandler);
|
|
3131
|
+
});
|
|
3132
|
+
// Long-press / right-click context menu (path actions)
|
|
3133
|
+
var pressTimer = null;
|
|
3134
|
+
var pressFired = false;
|
|
3135
|
+
tree.querySelectorAll(".tree-item").forEach(function(item) {
|
|
3136
|
+
item.addEventListener("contextmenu", function(e) {
|
|
3137
|
+
e.preventDefault();
|
|
3138
|
+
showFileContextMenu(e.clientX, e.clientY, item);
|
|
3139
|
+
});
|
|
3140
|
+
item.addEventListener("touchstart", function(e) {
|
|
3141
|
+
pressFired = false;
|
|
3142
|
+
pressTimer = setTimeout(function() {
|
|
3143
|
+
pressFired = true;
|
|
3144
|
+
var t = e.touches && e.touches[0];
|
|
3145
|
+
showFileContextMenu(t ? t.clientX : 0, t ? t.clientY : 0, item);
|
|
3146
|
+
}, 500);
|
|
3147
|
+
}, { passive: true });
|
|
3148
|
+
item.addEventListener("touchend", function() {
|
|
3149
|
+
if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; }
|
|
3150
|
+
});
|
|
3151
|
+
item.addEventListener("touchmove", function() {
|
|
3152
|
+
if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; }
|
|
2988
3153
|
});
|
|
2989
3154
|
});
|
|
2990
3155
|
}
|
|
2991
3156
|
|
|
2992
3157
|
function toggleTreeNode(item) {
|
|
2993
|
-
var
|
|
3158
|
+
var p = item.dataset.path;
|
|
2994
3159
|
var toggle = item.querySelector(".tree-toggle");
|
|
2995
3160
|
var children = item.nextElementSibling;
|
|
2996
3161
|
|
|
@@ -2998,14 +3163,24 @@
|
|
|
2998
3163
|
var isOpen = children.classList.contains("open");
|
|
2999
3164
|
children.classList.toggle("open");
|
|
3000
3165
|
if (toggle) toggle.classList.toggle("open", !isOpen);
|
|
3166
|
+
// swap folder icon between 📁 and 📂
|
|
3167
|
+
var iconEl = item.querySelector(".tree-icon");
|
|
3168
|
+
if (iconEl) iconEl.textContent = isOpen ? "📁" : "📂";
|
|
3001
3169
|
return;
|
|
3002
3170
|
}
|
|
3003
3171
|
|
|
3004
|
-
// Load children with git status
|
|
3005
3172
|
if (toggle) toggle.classList.add("open");
|
|
3006
|
-
|
|
3173
|
+
var iconEl2 = item.querySelector(".tree-icon");
|
|
3174
|
+
if (iconEl2) iconEl2.textContent = "📂";
|
|
3175
|
+
var url = "/api/directory?q=" + encodeURIComponent(p) +
|
|
3176
|
+
"&gitStatus=true" +
|
|
3177
|
+
(state.fileExplorerShowHidden ? "&showHidden=true" : "");
|
|
3178
|
+
fetch(url, { credentials: "same-origin" })
|
|
3007
3179
|
.then(function(res) { return res.json(); })
|
|
3008
|
-
.then(function(
|
|
3180
|
+
.then(function(payload) {
|
|
3181
|
+
var items;
|
|
3182
|
+
if (Array.isArray(payload)) items = payload;
|
|
3183
|
+
else items = (payload && payload.items) || [];
|
|
3009
3184
|
var childrenDiv = document.createElement("div");
|
|
3010
3185
|
childrenDiv.className = "tree-children open";
|
|
3011
3186
|
if (!items || items.length === 0) {
|
|
@@ -3021,79 +3196,456 @@
|
|
|
3021
3196
|
.catch(function() {});
|
|
3022
3197
|
}
|
|
3023
3198
|
|
|
3199
|
+
// Walk up to the parent directory and re-render the tree.
|
|
3200
|
+
function navigateExplorerUp() {
|
|
3201
|
+
var cwd = getEffectiveExplorerCwd();
|
|
3202
|
+
if (!cwd) return;
|
|
3203
|
+
var parent = cwd.replace(/\/+$/, "").replace(/\/[^\/]+$/, "");
|
|
3204
|
+
if (!parent) parent = "/";
|
|
3205
|
+
if (parent === cwd) return;
|
|
3206
|
+
refreshFileExplorer({ cwd: parent });
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// Toggle the show-hidden flag and persist it.
|
|
3210
|
+
function toggleExplorerHidden() {
|
|
3211
|
+
state.fileExplorerShowHidden = !state.fileExplorerShowHidden;
|
|
3212
|
+
try { localStorage.setItem("wand-file-show-hidden", state.fileExplorerShowHidden ? "1" : "0"); } catch (e) {}
|
|
3213
|
+
var btn = document.getElementById("file-explorer-toggle-hidden");
|
|
3214
|
+
if (btn) {
|
|
3215
|
+
btn.classList.toggle("active", state.fileExplorerShowHidden);
|
|
3216
|
+
btn.setAttribute("aria-pressed", state.fileExplorerShowHidden ? "true" : "false");
|
|
3217
|
+
btn.textContent = state.fileExplorerShowHidden ? "👁" : "👁🗨";
|
|
3218
|
+
btn.title = state.fileExplorerShowHidden ? "隐藏点开头文件" : "显示隐藏文件";
|
|
3219
|
+
}
|
|
3220
|
+
refreshFileExplorer();
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function appendToComposer(text) {
|
|
3224
|
+
var inputBox = document.getElementById("input-box");
|
|
3225
|
+
if (!inputBox) return false;
|
|
3226
|
+
var current = inputBox.value || "";
|
|
3227
|
+
var sep = current && !current.endsWith(" ") && !current.endsWith("\n") ? " " : "";
|
|
3228
|
+
inputBox.value = current + sep + text;
|
|
3229
|
+
inputBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3230
|
+
try { inputBox.focus(); inputBox.setSelectionRange(inputBox.value.length, inputBox.value.length); } catch (e) {}
|
|
3231
|
+
return true;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function copyTextSafely(text) {
|
|
3235
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3236
|
+
return navigator.clipboard.writeText(text).then(function() { return true; }).catch(function() { return fallback(); });
|
|
3237
|
+
}
|
|
3238
|
+
return Promise.resolve(fallback());
|
|
3239
|
+
function fallback() {
|
|
3240
|
+
try {
|
|
3241
|
+
var ta = document.createElement("textarea");
|
|
3242
|
+
ta.value = text;
|
|
3243
|
+
ta.style.position = "fixed";
|
|
3244
|
+
ta.style.left = "-9999px";
|
|
3245
|
+
document.body.appendChild(ta);
|
|
3246
|
+
ta.select();
|
|
3247
|
+
var ok = document.execCommand("copy");
|
|
3248
|
+
document.body.removeChild(ta);
|
|
3249
|
+
return ok;
|
|
3250
|
+
} catch (e) { return false; }
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function showToastIfPossible(msg) {
|
|
3255
|
+
if (typeof window.showToast === "function") { window.showToast(msg); return; }
|
|
3256
|
+
// Lightweight transient toast.
|
|
3257
|
+
var t = document.createElement("div");
|
|
3258
|
+
t.className = "wand-mini-toast";
|
|
3259
|
+
t.textContent = msg;
|
|
3260
|
+
document.body.appendChild(t);
|
|
3261
|
+
setTimeout(function() { t.classList.add("show"); }, 10);
|
|
3262
|
+
setTimeout(function() {
|
|
3263
|
+
t.classList.remove("show");
|
|
3264
|
+
setTimeout(function() { t.remove(); }, 220);
|
|
3265
|
+
}, 1600);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
function dismissFileContextMenu() {
|
|
3269
|
+
var menu = document.getElementById("file-context-menu");
|
|
3270
|
+
if (menu) menu.remove();
|
|
3271
|
+
document.removeEventListener("click", dismissFileContextMenu, true);
|
|
3272
|
+
document.removeEventListener("scroll", dismissFileContextMenu, true);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
function showFileContextMenu(x, y, item) {
|
|
3276
|
+
dismissFileContextMenu();
|
|
3277
|
+
var fullPath = item.dataset.path || "";
|
|
3278
|
+
var type = item.dataset.type || "file";
|
|
3279
|
+
var cwd = state.fileExplorerCwd || "";
|
|
3280
|
+
var relativePath = fullPath;
|
|
3281
|
+
if (cwd && fullPath.indexOf(cwd) === 0) {
|
|
3282
|
+
relativePath = fullPath.slice(cwd.length).replace(/^\/+/, "") || ".";
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
var menu = document.createElement("div");
|
|
3286
|
+
menu.id = "file-context-menu";
|
|
3287
|
+
menu.className = "file-context-menu";
|
|
3288
|
+
var actions = [];
|
|
3289
|
+
if (type === "file") {
|
|
3290
|
+
actions.push({ label: "打开预览", icon: "👁", run: function() { openFilePreview(fullPath); } });
|
|
3291
|
+
} else {
|
|
3292
|
+
actions.push({ label: "进入此目录", icon: "📂", run: function() { refreshFileExplorer({ cwd: fullPath }); } });
|
|
3293
|
+
}
|
|
3294
|
+
actions.push({ label: "复制完整路径", icon: "📋", run: function() {
|
|
3295
|
+
copyTextSafely(fullPath).then(function() { showToastIfPossible("已复制路径"); });
|
|
3296
|
+
}});
|
|
3297
|
+
if (relativePath && relativePath !== fullPath) {
|
|
3298
|
+
actions.push({ label: "复制相对路径", icon: "📋", run: function() {
|
|
3299
|
+
copyTextSafely(relativePath).then(function() { showToastIfPossible("已复制相对路径"); });
|
|
3300
|
+
}});
|
|
3301
|
+
}
|
|
3302
|
+
actions.push({ label: "粘贴路径到输入框", icon: "✏️", run: function() {
|
|
3303
|
+
if (appendToComposer(fullPath)) showToastIfPossible("已粘贴到输入框");
|
|
3304
|
+
}});
|
|
3305
|
+
if (type === "file") {
|
|
3306
|
+
actions.push({ label: "下载文件", icon: "⬇", run: function() {
|
|
3307
|
+
var a = document.createElement("a");
|
|
3308
|
+
a.href = "/api/file-raw?download=1&path=" + encodeURIComponent(fullPath);
|
|
3309
|
+
a.rel = "noopener";
|
|
3310
|
+
document.body.appendChild(a);
|
|
3311
|
+
a.click();
|
|
3312
|
+
a.remove();
|
|
3313
|
+
}});
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
menu.innerHTML = actions.map(function(act, i) {
|
|
3317
|
+
return '<button type="button" class="file-context-menu-item" data-idx="' + i + '"><span class="ctx-icon">' + act.icon + '</span><span class="ctx-label">' + escapeHtml(act.label) + '</span></button>';
|
|
3318
|
+
}).join("");
|
|
3319
|
+
document.body.appendChild(menu);
|
|
3320
|
+
|
|
3321
|
+
// Position with viewport-bound clamp
|
|
3322
|
+
var vw = window.innerWidth;
|
|
3323
|
+
var vh = window.innerHeight;
|
|
3324
|
+
var rect = menu.getBoundingClientRect();
|
|
3325
|
+
var left = Math.min(x, vw - rect.width - 8);
|
|
3326
|
+
var top = Math.min(y, vh - rect.height - 8);
|
|
3327
|
+
menu.style.left = Math.max(8, left) + "px";
|
|
3328
|
+
menu.style.top = Math.max(8, top) + "px";
|
|
3329
|
+
|
|
3330
|
+
menu.querySelectorAll(".file-context-menu-item").forEach(function(btn) {
|
|
3331
|
+
btn.addEventListener("click", function(ev) {
|
|
3332
|
+
ev.stopPropagation();
|
|
3333
|
+
var idx = parseInt(btn.dataset.idx, 10);
|
|
3334
|
+
dismissFileContextMenu();
|
|
3335
|
+
if (actions[idx]) actions[idx].run();
|
|
3336
|
+
});
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
// Close on outside click / scroll
|
|
3340
|
+
setTimeout(function() {
|
|
3341
|
+
document.addEventListener("click", dismissFileContextMenu, true);
|
|
3342
|
+
document.addEventListener("scroll", dismissFileContextMenu, true);
|
|
3343
|
+
}, 0);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// Module-level handle for keyboard nav between siblings.
|
|
3347
|
+
var _activeFilePreview = null;
|
|
3348
|
+
|
|
3024
3349
|
function openFilePreview(filePath) {
|
|
3025
|
-
|
|
3026
|
-
overlay
|
|
3027
|
-
overlay
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3350
|
+
// If a modal is already open, just swap content (used by ←/→ navigation).
|
|
3351
|
+
var overlay = _activeFilePreview && _activeFilePreview.overlay;
|
|
3352
|
+
if (!overlay) {
|
|
3353
|
+
overlay = document.createElement("div");
|
|
3354
|
+
overlay.className = "file-preview-overlay";
|
|
3355
|
+
overlay.innerHTML =
|
|
3356
|
+
'<div class="file-preview-modal" tabindex="-1">' +
|
|
3357
|
+
'<div class="file-preview-header">' +
|
|
3358
|
+
'<div class="file-preview-title">' +
|
|
3359
|
+
'<span class="file-preview-icon">📄</span>' +
|
|
3360
|
+
'<span class="file-preview-filename">加载中…</span>' +
|
|
3361
|
+
'</div>' +
|
|
3362
|
+
'<div class="file-preview-path" title=""></div>' +
|
|
3363
|
+
'<div class="file-preview-toolbar"></div>' +
|
|
3364
|
+
'<button class="file-preview-close" title="关闭 (Esc)" aria-label="关闭">✕</button>' +
|
|
3033
3365
|
'</div>' +
|
|
3034
|
-
'<div class="file-preview-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
'
|
|
3038
|
-
|
|
3039
|
-
'</div>' +
|
|
3040
|
-
'</div>';
|
|
3041
|
-
document.body.appendChild(overlay);
|
|
3366
|
+
'<div class="file-preview-body">' +
|
|
3367
|
+
'<div class="file-preview-loading">加载预览…</div>' +
|
|
3368
|
+
'</div>' +
|
|
3369
|
+
'</div>';
|
|
3370
|
+
document.body.appendChild(overlay);
|
|
3042
3371
|
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3372
|
+
var closeBtn = overlay.querySelector(".file-preview-close");
|
|
3373
|
+
var closeModal = function() {
|
|
3374
|
+
overlay.remove();
|
|
3375
|
+
document.removeEventListener("keydown", keyHandler);
|
|
3376
|
+
_activeFilePreview = null;
|
|
3377
|
+
};
|
|
3378
|
+
closeBtn.addEventListener("click", closeModal);
|
|
3379
|
+
overlay.addEventListener("click", function(e) {
|
|
3380
|
+
if (e.target === overlay) closeModal();
|
|
3381
|
+
});
|
|
3382
|
+
var keyHandler = function(e) {
|
|
3383
|
+
if (e.key === "Escape") { closeModal(); return; }
|
|
3384
|
+
if (!_activeFilePreview) return;
|
|
3385
|
+
if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
|
|
3386
|
+
if (e.key === "ArrowLeft") { e.preventDefault(); navigatePreviewSibling(-1); }
|
|
3387
|
+
else if (e.key === "ArrowRight") { e.preventDefault(); navigatePreviewSibling(1); }
|
|
3388
|
+
};
|
|
3389
|
+
document.addEventListener("keydown", keyHandler);
|
|
3390
|
+
|
|
3391
|
+
_activeFilePreview = { overlay: overlay, close: closeModal, path: filePath, data: null };
|
|
3392
|
+
} else {
|
|
3393
|
+
_activeFilePreview.path = filePath;
|
|
3394
|
+
// Reset header / body for the new file.
|
|
3395
|
+
var titleEl = overlay.querySelector(".file-preview-title");
|
|
3396
|
+
if (titleEl) {
|
|
3397
|
+
titleEl.innerHTML = '<span class="file-preview-icon">📄</span><span class="file-preview-filename">加载中…</span>';
|
|
3398
|
+
}
|
|
3399
|
+
var toolbarEl = overlay.querySelector(".file-preview-toolbar");
|
|
3400
|
+
if (toolbarEl) toolbarEl.innerHTML = "";
|
|
3401
|
+
var pathEl = overlay.querySelector(".file-preview-path");
|
|
3402
|
+
if (pathEl) { pathEl.textContent = ""; pathEl.title = ""; }
|
|
3403
|
+
var bodyReset = overlay.querySelector(".file-preview-body");
|
|
3404
|
+
if (bodyReset) bodyReset.innerHTML = '<div class="file-preview-loading">加载预览…</div>';
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
var pathDisplayEl = overlay.querySelector(".file-preview-path");
|
|
3408
|
+
if (pathDisplayEl) {
|
|
3409
|
+
pathDisplayEl.textContent = filePath;
|
|
3410
|
+
pathDisplayEl.title = filePath;
|
|
3411
|
+
}
|
|
3056
3412
|
|
|
3057
3413
|
fetch("/api/file-preview?path=" + encodeURIComponent(filePath), { credentials: "same-origin" })
|
|
3058
|
-
.then(function(res) {
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3414
|
+
.then(function(res) {
|
|
3415
|
+
return res.json().then(function(data) { return { ok: res.ok, status: res.status, data: data }; });
|
|
3416
|
+
})
|
|
3417
|
+
.then(function(result) {
|
|
3418
|
+
var body = overlay.querySelector(".file-preview-body");
|
|
3419
|
+
if (!result.ok || (result.data && result.data.error)) {
|
|
3420
|
+
var msg = (result.data && result.data.error) || "加载失败";
|
|
3421
|
+
if (result.status === 413 && result.data && result.data.size) {
|
|
3422
|
+
msg += "(文件大小:" + formatFileSize(result.data.size) + ")";
|
|
3423
|
+
}
|
|
3424
|
+
body.innerHTML = '<div class="file-preview-error"><span class="preview-error-icon">⚠</span><span>' + escapeHtml(msg) + '</span></div>';
|
|
3425
|
+
// Even when text preview is rejected for size, still allow download.
|
|
3426
|
+
if (result.status === 413) {
|
|
3427
|
+
renderPreviewToolbar(overlay, {
|
|
3428
|
+
kind: "binary",
|
|
3429
|
+
path: filePath,
|
|
3430
|
+
name: filePath.split("/").pop() || filePath,
|
|
3431
|
+
ext: "",
|
|
3432
|
+
size: (result.data && result.data.size) || 0,
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3063
3435
|
return;
|
|
3064
3436
|
}
|
|
3065
|
-
|
|
3437
|
+
_activeFilePreview.data = result.data;
|
|
3438
|
+
renderPreviewContent(overlay, result.data);
|
|
3066
3439
|
})
|
|
3067
|
-
.catch(function(
|
|
3440
|
+
.catch(function() {
|
|
3068
3441
|
var body = overlay.querySelector(".file-preview-body");
|
|
3069
|
-
body.innerHTML = '<div class="file-preview-error"><span class="preview-error-icon">⚠</span><span
|
|
3442
|
+
body.innerHTML = '<div class="file-preview-error"><span class="preview-error-icon">⚠</span><span>加载预览失败</span></div>';
|
|
3070
3443
|
});
|
|
3071
3444
|
}
|
|
3072
3445
|
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3446
|
+
// Move to the previous/next file sibling in the current explorer view.
|
|
3447
|
+
function navigatePreviewSibling(direction) {
|
|
3448
|
+
if (!_activeFilePreview) return;
|
|
3449
|
+
var siblings = (state.allFiles || []).filter(function(item) { return item.type === "file"; });
|
|
3450
|
+
if (!siblings.length) return;
|
|
3451
|
+
var currentPath = _activeFilePreview.path;
|
|
3452
|
+
var idx = -1;
|
|
3453
|
+
for (var i = 0; i < siblings.length; i++) {
|
|
3454
|
+
if (siblings[i].path === currentPath) { idx = i; break; }
|
|
3455
|
+
}
|
|
3456
|
+
if (idx < 0) return;
|
|
3457
|
+
var nextIdx = (idx + direction + siblings.length) % siblings.length;
|
|
3458
|
+
var nextPath = siblings[nextIdx].path;
|
|
3459
|
+
if (nextPath && nextPath !== currentPath) openFilePreview(nextPath);
|
|
3460
|
+
}
|
|
3076
3461
|
|
|
3462
|
+
function renderPreviewContent(overlay, data) {
|
|
3463
|
+
var filenameEl = overlay.querySelector(".file-preview-filename");
|
|
3464
|
+
if (filenameEl) filenameEl.textContent = data.name;
|
|
3465
|
+
var iconEl = overlay.querySelector(".file-preview-icon");
|
|
3466
|
+
if (iconEl) iconEl.textContent = getFileIcon({ name: data.name, type: "file" });
|
|
3467
|
+
|
|
3468
|
+
// Title-bar badge: language for text, kind label for media/binary
|
|
3469
|
+
var titleEl = overlay.querySelector(".file-preview-title");
|
|
3470
|
+
var existingBadge = overlay.querySelector(".file-preview-lang");
|
|
3471
|
+
if (existingBadge) existingBadge.remove();
|
|
3077
3472
|
var langBadge = document.createElement("span");
|
|
3078
3473
|
langBadge.className = "file-preview-lang";
|
|
3079
|
-
|
|
3080
|
-
|
|
3474
|
+
var labelMap = { image: "图片", pdf: "PDF", video: "视频", audio: "音频", binary: "二进制" };
|
|
3475
|
+
if (data.kind === "text") {
|
|
3476
|
+
langBadge.textContent = data.lang || (data.ext || "").replace(".", "") || "text";
|
|
3477
|
+
} else {
|
|
3478
|
+
langBadge.textContent = labelMap[data.kind] || (data.ext || "").replace(".", "") || data.kind;
|
|
3479
|
+
}
|
|
3480
|
+
if (titleEl) titleEl.appendChild(langBadge);
|
|
3081
3481
|
|
|
3082
|
-
|
|
3482
|
+
renderPreviewToolbar(overlay, data);
|
|
3083
3483
|
|
|
3084
|
-
|
|
3085
|
-
|
|
3484
|
+
var body = overlay.querySelector(".file-preview-body");
|
|
3485
|
+
body.innerHTML = "";
|
|
3486
|
+
body.classList.remove("kind-text", "kind-image", "kind-pdf", "kind-video", "kind-audio", "kind-binary");
|
|
3487
|
+
body.classList.add("kind-" + (data.kind || "text"));
|
|
3488
|
+
|
|
3489
|
+
if (data.kind === "image") {
|
|
3490
|
+
renderImagePreview(body, data);
|
|
3491
|
+
} else if (data.kind === "pdf") {
|
|
3492
|
+
renderPdfPreview(body, data);
|
|
3493
|
+
} else if (data.kind === "video") {
|
|
3494
|
+
renderVideoPreview(body, data);
|
|
3495
|
+
} else if (data.kind === "audio") {
|
|
3496
|
+
renderAudioPreview(body, data);
|
|
3497
|
+
} else if (data.kind === "binary") {
|
|
3498
|
+
renderBinaryPreview(body, data);
|
|
3499
|
+
} else if ((data.lang === "markdown") || /\.(md|markdown|mdx)$/i.test(data.name || "")) {
|
|
3500
|
+
body.innerHTML = '<div class="markdown-preview">' + renderMarkdownPreview(data.content || "") + '</div>';
|
|
3086
3501
|
} else {
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3502
|
+
renderTextPreview(body, data);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function renderTextPreview(body, data) {
|
|
3507
|
+
var highlighted = highlightCodePreview(data.content || "", data.lang);
|
|
3508
|
+
var lines = highlighted.split("\n");
|
|
3509
|
+
var lineNums = lines.map(function(_, i) { return i + 1; });
|
|
3510
|
+
body.innerHTML =
|
|
3511
|
+
'<div class="code-preview-wrapper">' +
|
|
3512
|
+
'<div class="code-preview-lines">' + lineNums.join("\n") + '</div>' +
|
|
3513
|
+
'<div class="code-preview-content"><pre>' + lines.join("\n") + '</pre></div>' +
|
|
3514
|
+
'</div>';
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function renderImagePreview(body, data) {
|
|
3518
|
+
var src = "/api/file-raw?path=" + encodeURIComponent(data.path);
|
|
3519
|
+
body.innerHTML =
|
|
3520
|
+
'<div class="image-preview-wrapper">' +
|
|
3521
|
+
'<img class="image-preview-img" src="' + src + '" alt="' + escapeHtml(data.name) + '" />' +
|
|
3522
|
+
'</div>';
|
|
3523
|
+
var img = body.querySelector(".image-preview-img");
|
|
3524
|
+
if (!img) return;
|
|
3525
|
+
var zoomed = false;
|
|
3526
|
+
img.addEventListener("click", function() {
|
|
3527
|
+
zoomed = !zoomed;
|
|
3528
|
+
img.classList.toggle("zoomed", zoomed);
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
function renderPdfPreview(body, data) {
|
|
3533
|
+
var src = "/api/file-raw?path=" + encodeURIComponent(data.path);
|
|
3534
|
+
body.innerHTML =
|
|
3535
|
+
'<iframe class="pdf-preview-frame" src="' + src + '" title="' + escapeHtml(data.name) + '"></iframe>';
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
function renderVideoPreview(body, data) {
|
|
3539
|
+
var src = "/api/file-raw?path=" + encodeURIComponent(data.path);
|
|
3540
|
+
body.innerHTML =
|
|
3541
|
+
'<div class="media-preview-wrapper">' +
|
|
3542
|
+
'<video class="media-preview-video" controls preload="metadata" src="' + src + '">您的浏览器不支持 video 标签。</video>' +
|
|
3543
|
+
'<div class="media-preview-meta">' + escapeHtml(formatFileSize(data.size)) + '</div>' +
|
|
3544
|
+
'</div>';
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
function renderAudioPreview(body, data) {
|
|
3548
|
+
var src = "/api/file-raw?path=" + encodeURIComponent(data.path);
|
|
3549
|
+
body.innerHTML =
|
|
3550
|
+
'<div class="media-preview-wrapper audio">' +
|
|
3551
|
+
'<div class="media-preview-icon">🎵</div>' +
|
|
3552
|
+
'<div class="media-preview-name">' + escapeHtml(data.name) + '</div>' +
|
|
3553
|
+
'<audio class="media-preview-audio" controls preload="metadata" src="' + src + '">您的浏览器不支持 audio 标签。</audio>' +
|
|
3554
|
+
'<div class="media-preview-meta">' + escapeHtml(formatFileSize(data.size)) + '</div>' +
|
|
3555
|
+
'</div>';
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
function renderBinaryPreview(body, data) {
|
|
3559
|
+
var rawUrl = "/api/file-raw?download=1&path=" + encodeURIComponent(data.path);
|
|
3560
|
+
body.innerHTML =
|
|
3561
|
+
'<div class="binary-preview-card">' +
|
|
3562
|
+
'<div class="binary-preview-icon">📦</div>' +
|
|
3563
|
+
'<div class="binary-preview-name">' + escapeHtml(data.name) + '</div>' +
|
|
3564
|
+
'<div class="binary-preview-meta">' +
|
|
3565
|
+
'<span>' + escapeHtml((data.ext || "").replace(/^\./, "") || "未知格式") + '</span>' +
|
|
3566
|
+
'<span>·</span>' +
|
|
3567
|
+
'<span>' + escapeHtml(formatFileSize(data.size)) + '</span>' +
|
|
3568
|
+
'</div>' +
|
|
3569
|
+
'<div class="binary-preview-path" title="' + escapeHtml(data.path) + '">' + escapeHtml(data.path) + '</div>' +
|
|
3570
|
+
'<div class="binary-preview-actions">' +
|
|
3571
|
+
'<a class="binary-preview-btn" href="' + rawUrl + '" download="' + escapeHtml(data.name) + '">下载文件</a>' +
|
|
3572
|
+
'<button class="binary-preview-btn" type="button" data-action="view-cat">在终端中查看</button>' +
|
|
3573
|
+
'</div>' +
|
|
3574
|
+
'</div>';
|
|
3575
|
+
var catBtn = body.querySelector('[data-action="view-cat"]');
|
|
3576
|
+
if (catBtn) catBtn.addEventListener("click", function() {
|
|
3577
|
+
if (appendToComposer('cat -- "' + data.path + '"')) {
|
|
3578
|
+
showToastIfPossible("命令已粘贴到输入框");
|
|
3579
|
+
}
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
function renderPreviewToolbar(overlay, data) {
|
|
3584
|
+
var bar = overlay.querySelector(".file-preview-toolbar");
|
|
3585
|
+
if (!bar) return;
|
|
3586
|
+
bar.innerHTML = "";
|
|
3587
|
+
var buttons = [];
|
|
3588
|
+
|
|
3589
|
+
// Common actions across all kinds
|
|
3590
|
+
buttons.push({ label: "复制路径", icon: "📋", action: function() {
|
|
3591
|
+
copyTextSafely(data.path).then(function() { showToastIfPossible("已复制路径"); });
|
|
3592
|
+
}});
|
|
3593
|
+
buttons.push({ label: "粘贴到输入框", icon: "✏️", action: function() {
|
|
3594
|
+
if (appendToComposer(data.path)) showToastIfPossible("已粘贴到输入框");
|
|
3595
|
+
}});
|
|
3596
|
+
buttons.push({ label: "下载", icon: "⬇", action: function() {
|
|
3597
|
+
var a = document.createElement("a");
|
|
3598
|
+
a.href = "/api/file-raw?download=1&path=" + encodeURIComponent(data.path);
|
|
3599
|
+
a.download = data.name || "";
|
|
3600
|
+
document.body.appendChild(a);
|
|
3601
|
+
a.click();
|
|
3602
|
+
a.remove();
|
|
3603
|
+
}});
|
|
3604
|
+
|
|
3605
|
+
if (data.kind === "text") {
|
|
3606
|
+
buttons.push({ label: "复制全部", icon: "📄", action: function() {
|
|
3607
|
+
copyTextSafely(data.content || "").then(function() { showToastIfPossible("已复制内容"); });
|
|
3608
|
+
}});
|
|
3609
|
+
buttons.push({ label: "自动换行", icon: "↩", toggleClass: "toolbar-active",
|
|
3610
|
+
getInitial: function() {
|
|
3611
|
+
var pre = overlay.querySelector(".code-preview-content pre");
|
|
3612
|
+
return pre && pre.classList.contains("wrap");
|
|
3613
|
+
},
|
|
3614
|
+
action: function(btn) {
|
|
3615
|
+
var pre = overlay.querySelector(".code-preview-content pre");
|
|
3616
|
+
if (!pre) return;
|
|
3617
|
+
pre.classList.toggle("wrap");
|
|
3618
|
+
btn.classList.toggle("toolbar-active", pre.classList.contains("wrap"));
|
|
3619
|
+
}
|
|
3620
|
+
});
|
|
3621
|
+
buttons.push({ label: "字号 −", icon: "A−", action: function() { adjustPreviewFontSize(overlay, -1); }});
|
|
3622
|
+
buttons.push({ label: "字号 +", icon: "A+", action: function() { adjustPreviewFontSize(overlay, +1); }});
|
|
3096
3623
|
}
|
|
3624
|
+
|
|
3625
|
+
buttons.forEach(function(b) {
|
|
3626
|
+
var btn = document.createElement("button");
|
|
3627
|
+
btn.type = "button";
|
|
3628
|
+
btn.className = "file-preview-toolbar-btn";
|
|
3629
|
+
btn.title = b.label;
|
|
3630
|
+
btn.setAttribute("aria-label", b.label);
|
|
3631
|
+
btn.innerHTML = '<span class="toolbar-icon">' + b.icon + '</span>';
|
|
3632
|
+
if (b.getInitial && b.getInitial()) btn.classList.add("toolbar-active");
|
|
3633
|
+
btn.addEventListener("click", function(ev) {
|
|
3634
|
+
ev.stopPropagation();
|
|
3635
|
+
b.action(btn);
|
|
3636
|
+
});
|
|
3637
|
+
bar.appendChild(btn);
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function adjustPreviewFontSize(overlay, delta) {
|
|
3642
|
+
var pre = overlay.querySelector(".code-preview-content pre");
|
|
3643
|
+
var nums = overlay.querySelector(".code-preview-lines");
|
|
3644
|
+
if (!pre) return;
|
|
3645
|
+
var current = parseFloat(getComputedStyle(pre).fontSize) || 13;
|
|
3646
|
+
var next = Math.max(10, Math.min(22, current + delta));
|
|
3647
|
+
pre.style.fontSize = next + "px";
|
|
3648
|
+
if (nums) nums.style.fontSize = next + "px";
|
|
3097
3649
|
}
|
|
3098
3650
|
|
|
3099
3651
|
function highlightCodePreview(code, lang) {
|
|
@@ -4420,27 +4972,28 @@
|
|
|
4420
4972
|
topbarMoreBtn.classList.remove("active");
|
|
4421
4973
|
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
4422
4974
|
switch (action) {
|
|
4423
|
-
case "
|
|
4424
|
-
|
|
4975
|
+
case "copy-claude-session-id":
|
|
4976
|
+
copySelectedSessionField("claudeSessionId", "Claude 会话 ID 已复制");
|
|
4425
4977
|
break;
|
|
4426
|
-
case "
|
|
4427
|
-
|
|
4978
|
+
case "copy-cwd":
|
|
4979
|
+
copySelectedSessionField("cwd", "工作目录已复制");
|
|
4428
4980
|
break;
|
|
4429
|
-
case "
|
|
4430
|
-
|
|
4431
|
-
state.deferredPrompt.prompt();
|
|
4432
|
-
state.deferredPrompt.userChoice.then(function() {
|
|
4433
|
-
state.deferredPrompt = null;
|
|
4434
|
-
state.showInstallPrompt = false;
|
|
4435
|
-
updateInstallPrompt();
|
|
4436
|
-
});
|
|
4437
|
-
}
|
|
4981
|
+
case "copy-session-id":
|
|
4982
|
+
copySelectedSessionField("id", "会话 ID 已复制");
|
|
4438
4983
|
break;
|
|
4439
|
-
case "
|
|
4440
|
-
|
|
4984
|
+
case "worktree-merge":
|
|
4985
|
+
if (state.selectedId) openWorktreeMergeModal(state.selectedId);
|
|
4441
4986
|
break;
|
|
4442
|
-
case "
|
|
4443
|
-
|
|
4987
|
+
case "worktree-cleanup":
|
|
4988
|
+
if (state.selectedId) retryWorktreeCleanup(state.selectedId);
|
|
4989
|
+
break;
|
|
4990
|
+
case "delete-session":
|
|
4991
|
+
if (state.selectedId) {
|
|
4992
|
+
var pendingId = state.selectedId;
|
|
4993
|
+
if (confirm("确定要删除当前会话吗?")) {
|
|
4994
|
+
deleteSession(pendingId);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4444
4997
|
break;
|
|
4445
4998
|
}
|
|
4446
4999
|
});
|
|
@@ -4501,7 +5054,11 @@
|
|
|
4501
5054
|
setChatAutoFollow(true, { scrollNow: true, smooth: true });
|
|
4502
5055
|
});
|
|
4503
5056
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
4504
|
-
if (fileRefresh) fileRefresh.addEventListener("click", refreshFileExplorer);
|
|
5057
|
+
if (fileRefresh) fileRefresh.addEventListener("click", function() { refreshFileExplorer(); });
|
|
5058
|
+
var fileUp = document.getElementById("file-explorer-up");
|
|
5059
|
+
if (fileUp) fileUp.addEventListener("click", navigateExplorerUp);
|
|
5060
|
+
var fileToggleHidden = document.getElementById("file-explorer-toggle-hidden");
|
|
5061
|
+
if (fileToggleHidden) fileToggleHidden.addEventListener("click", toggleExplorerHidden);
|
|
4505
5062
|
|
|
4506
5063
|
// File search
|
|
4507
5064
|
var fileSearchInput = document.getElementById("file-search-input");
|
|
@@ -5074,6 +5631,20 @@
|
|
|
5074
5631
|
}
|
|
5075
5632
|
}
|
|
5076
5633
|
|
|
5634
|
+
/** Copy a string field of the currently selected session to clipboard. */
|
|
5635
|
+
function copySelectedSessionField(field, successMsg) {
|
|
5636
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
5637
|
+
if (!session) return;
|
|
5638
|
+
var value = session[field];
|
|
5639
|
+
if (!value) {
|
|
5640
|
+
showToast("当前会话没有可复制的内容。", "error");
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
copyToClipboard(String(value), null, function() {
|
|
5644
|
+
showToast(successMsg || "已复制", "info");
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5077
5648
|
/** Copy Claude session ID from badge to clipboard */
|
|
5078
5649
|
function handleClaudeIdCopy(event) {
|
|
5079
5650
|
var badge = event.target.closest("#claude-session-id-badge");
|