@co0ontty/wand 1.21.11 → 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 +294 -31
- package/dist/structured-session-manager.d.ts +6 -2
- package/dist/structured-session-manager.js +174 -97
- package/dist/types.d.ts +24 -0
- package/dist/web-ui/content/scripts.js +970 -211
- package/dist/web-ui/content/styles.css +812 -51
- 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;
|
|
@@ -2169,10 +2222,13 @@
|
|
|
2169
2222
|
'<label class="settings-toggle-title" for="cfg-inherit-env">继承环境变量</label>' +
|
|
2170
2223
|
'<span class="settings-toggle-desc">启动 PTY / 结构化子进程时,把当前服务进程的环境变量传给 claude / codex。关闭后子进程仅获得最小可用环境(PATH/HOME/SHELL/LANG/TERM 等),可用于隔离 API key 等敏感凭据。</span>' +
|
|
2171
2224
|
'</div>' +
|
|
2172
|
-
'<
|
|
2173
|
-
'<
|
|
2174
|
-
'<
|
|
2175
|
-
|
|
2225
|
+
'<div class="settings-toggle-aside">' +
|
|
2226
|
+
'<button type="button" id="cfg-view-env-btn" class="btn btn-secondary btn-sm" title="查看实际会注入到子进程的环境变量">查看</button>' +
|
|
2227
|
+
'<label class="settings-switch">' +
|
|
2228
|
+
'<input id="cfg-inherit-env" type="checkbox" class="switch-toggle" />' +
|
|
2229
|
+
'<span class="switch-slider"></span>' +
|
|
2230
|
+
'</label>' +
|
|
2231
|
+
'</div>' +
|
|
2176
2232
|
'</div>' +
|
|
2177
2233
|
'<div class="field">' +
|
|
2178
2234
|
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
@@ -2849,118 +2905,205 @@
|
|
|
2849
2905
|
function renderFileExplorer(cwd) {
|
|
2850
2906
|
var root = cwd || getConfigCwd();
|
|
2851
2907
|
if (!root) {
|
|
2852
|
-
return '<div class="file-explorer empty"
|
|
2908
|
+
return '<div class="file-explorer empty">未配置工作目录。</div>';
|
|
2853
2909
|
}
|
|
2854
2910
|
return '<div class="file-tree" id="file-tree" data-cwd="' + escapeHtml(root) + '">' +
|
|
2855
|
-
'<div class="tree-loading"
|
|
2911
|
+
'<div class="tree-loading">加载中…</div>' +
|
|
2856
2912
|
'</div>';
|
|
2857
2913
|
}
|
|
2858
2914
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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;
|
|
2865
2984
|
if (state.selectedId) {
|
|
2866
2985
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
2867
|
-
if (session
|
|
2868
|
-
}
|
|
2869
|
-
if (!cwd) {
|
|
2870
|
-
cwd = getConfigCwd();
|
|
2986
|
+
if (session && session.cwd) return session.cwd;
|
|
2871
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();
|
|
2872
2997
|
if (!cwd) {
|
|
2873
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
2998
|
+
explorer.innerHTML = '<div class="file-explorer empty">没有可显示的工作目录。</div>';
|
|
2874
2999
|
return;
|
|
2875
3000
|
}
|
|
3001
|
+
state.fileExplorerCwd = cwd;
|
|
2876
3002
|
state.fileExplorerLoading = true;
|
|
2877
3003
|
state.allFiles = [];
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
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" })
|
|
2883
3015
|
.then(function(res) {
|
|
2884
|
-
if (!res.ok)
|
|
2885
|
-
throw new Error("Failed to load directory.");
|
|
2886
|
-
}
|
|
3016
|
+
if (!res.ok) throw new Error("Failed to load directory.");
|
|
2887
3017
|
return res.json();
|
|
2888
3018
|
})
|
|
2889
|
-
.then(function(
|
|
3019
|
+
.then(function(payload) {
|
|
2890
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
|
+
}
|
|
2891
3030
|
if (!items || items.length === 0) {
|
|
2892
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3031
|
+
explorer.innerHTML = '<div class="file-explorer empty">空目录或无法访问。</div>';
|
|
2893
3032
|
return;
|
|
2894
3033
|
}
|
|
2895
3034
|
state.allFiles = items;
|
|
3035
|
+
state.fileExplorerTruncated = truncated;
|
|
3036
|
+
state.fileExplorerTotal = total;
|
|
2896
3037
|
filterFileTree();
|
|
2897
3038
|
})
|
|
2898
3039
|
.catch(function() {
|
|
2899
3040
|
state.fileExplorerLoading = false;
|
|
2900
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3041
|
+
explorer.innerHTML = '<div class="file-explorer empty">加载失败,请检查路径或权限。</div>';
|
|
2901
3042
|
});
|
|
2902
3043
|
}
|
|
2903
3044
|
|
|
2904
3045
|
function filterFileTree() {
|
|
2905
3046
|
var explorer = document.getElementById("file-explorer");
|
|
2906
|
-
var cwdEl = document.getElementById("file-explorer-cwd");
|
|
2907
3047
|
if (!explorer) return;
|
|
2908
|
-
var cwd =
|
|
3048
|
+
var cwd = state.fileExplorerCwd || "";
|
|
2909
3049
|
if (!cwd) return;
|
|
2910
3050
|
|
|
2911
3051
|
var query = state.fileSearchQuery;
|
|
2912
3052
|
var items = state.allFiles || [];
|
|
3053
|
+
var filtered = items;
|
|
2913
3054
|
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
}).join("") +
|
|
2920
|
-
'</div>';
|
|
2921
|
-
attachFileTreeListeners();
|
|
2922
|
-
return;
|
|
3055
|
+
if (query) {
|
|
3056
|
+
var lowerQuery = query.toLowerCase();
|
|
3057
|
+
filtered = items.filter(function(item) {
|
|
3058
|
+
return item.name.toLowerCase().indexOf(lowerQuery) !== -1;
|
|
3059
|
+
});
|
|
2923
3060
|
}
|
|
2924
3061
|
|
|
2925
|
-
// 模糊匹配文件名(大小写不敏感)
|
|
2926
|
-
var lowerQuery = query.toLowerCase();
|
|
2927
|
-
var filtered = items.filter(function(item) {
|
|
2928
|
-
return item.name.toLowerCase().indexOf(lowerQuery) !== -1;
|
|
2929
|
-
});
|
|
2930
|
-
|
|
2931
3062
|
if (filtered.length === 0) {
|
|
2932
|
-
explorer.innerHTML = '<div class="file-explorer empty"
|
|
3063
|
+
explorer.innerHTML = '<div class="file-explorer empty">' + (query ? '没有找到匹配的文件' : '空目录') + '</div>';
|
|
2933
3064
|
return;
|
|
2934
3065
|
}
|
|
2935
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
|
+
|
|
2936
3073
|
explorer.innerHTML = '<div class="file-tree" id="file-tree" data-cwd="' + escapeHtml(cwd) + '">' +
|
|
2937
3074
|
filtered.map(function(item) {
|
|
2938
3075
|
return renderFileTreeItem(item);
|
|
2939
3076
|
}).join("") +
|
|
2940
|
-
'</div>';
|
|
3077
|
+
'</div>' + truncatedNotice;
|
|
2941
3078
|
attachFileTreeListeners();
|
|
2942
3079
|
}
|
|
2943
3080
|
|
|
2944
|
-
function renderFileTreeItem(item) {
|
|
3081
|
+
function renderFileTreeItem(item, depth) {
|
|
3082
|
+
depth = depth || 0;
|
|
2945
3083
|
var name = escapeHtml(item.name);
|
|
2946
3084
|
var isDir = item.type === "dir";
|
|
2947
|
-
|
|
2948
|
-
var displayIcon = isDir ? "📁" : "📄";
|
|
3085
|
+
var displayIcon = getFileIcon(item);
|
|
2949
3086
|
var toggleIcon = isDir ? "▸" : "";
|
|
2950
3087
|
var toggleClass = isDir ? "" : " empty";
|
|
2951
3088
|
var gitStatus = item.gitStatus;
|
|
2952
3089
|
var statusBadge = renderGitStatusBadge(gitStatus);
|
|
2953
|
-
|
|
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">' +
|
|
2954
3097
|
'<span class="tree-toggle' + toggleClass + '">' + toggleIcon + '</span>' +
|
|
2955
3098
|
'<span class="tree-icon">' + displayIcon + '</span>' +
|
|
2956
3099
|
'<span class="tree-name">' + name + '</span>' +
|
|
3100
|
+
meta +
|
|
2957
3101
|
(statusBadge ? '<span class="git-status-badge ' + statusBadge.class + '" title="' + statusBadge.title + '">' + statusBadge.text + '</span>' : '') +
|
|
2958
3102
|
'</div>';
|
|
2959
3103
|
}
|
|
2960
3104
|
|
|
2961
3105
|
function renderGitStatusBadge(gitStatus) {
|
|
2962
3106
|
if (!gitStatus) return null;
|
|
2963
|
-
// Priority: staged > unstaged > untracked
|
|
2964
3107
|
if (gitStatus.staged === "added") return { text: "A", class: "git-added", title: "已暂存(新增)" };
|
|
2965
3108
|
if (gitStatus.staged === "modified") return { text: "M", class: "git-modified", title: "已暂存(修改)" };
|
|
2966
3109
|
if (gitStatus.staged === "deleted") return { text: "D", class: "git-deleted", title: "已暂存(删除)" };
|
|
@@ -2975,19 +3118,44 @@
|
|
|
2975
3118
|
var tree = document.getElementById("file-tree");
|
|
2976
3119
|
if (!tree) return;
|
|
2977
3120
|
tree.querySelectorAll(".tree-item[data-type='dir']").forEach(function(item) {
|
|
2978
|
-
item.addEventListener("click", function() {
|
|
3121
|
+
item.addEventListener("click", function(e) {
|
|
3122
|
+
// Don't toggle when click came from the meta/badge area
|
|
2979
3123
|
toggleTreeNode(item);
|
|
2980
3124
|
});
|
|
2981
3125
|
});
|
|
2982
3126
|
tree.querySelectorAll(".tree-item[data-type='file']").forEach(function(item) {
|
|
2983
|
-
|
|
2984
|
-
|
|
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; }
|
|
2985
3153
|
});
|
|
2986
3154
|
});
|
|
2987
3155
|
}
|
|
2988
3156
|
|
|
2989
3157
|
function toggleTreeNode(item) {
|
|
2990
|
-
var
|
|
3158
|
+
var p = item.dataset.path;
|
|
2991
3159
|
var toggle = item.querySelector(".tree-toggle");
|
|
2992
3160
|
var children = item.nextElementSibling;
|
|
2993
3161
|
|
|
@@ -2995,14 +3163,24 @@
|
|
|
2995
3163
|
var isOpen = children.classList.contains("open");
|
|
2996
3164
|
children.classList.toggle("open");
|
|
2997
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 ? "📁" : "📂";
|
|
2998
3169
|
return;
|
|
2999
3170
|
}
|
|
3000
3171
|
|
|
3001
|
-
// Load children with git status
|
|
3002
3172
|
if (toggle) toggle.classList.add("open");
|
|
3003
|
-
|
|
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" })
|
|
3004
3179
|
.then(function(res) { return res.json(); })
|
|
3005
|
-
.then(function(
|
|
3180
|
+
.then(function(payload) {
|
|
3181
|
+
var items;
|
|
3182
|
+
if (Array.isArray(payload)) items = payload;
|
|
3183
|
+
else items = (payload && payload.items) || [];
|
|
3006
3184
|
var childrenDiv = document.createElement("div");
|
|
3007
3185
|
childrenDiv.className = "tree-children open";
|
|
3008
3186
|
if (!items || items.length === 0) {
|
|
@@ -3018,81 +3196,458 @@
|
|
|
3018
3196
|
.catch(function() {});
|
|
3019
3197
|
}
|
|
3020
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
|
+
|
|
3021
3349
|
function openFilePreview(filePath) {
|
|
3022
|
-
|
|
3023
|
-
overlay
|
|
3024
|
-
overlay
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
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>' +
|
|
3030
3365
|
'</div>' +
|
|
3031
|
-
'<div class="file-preview-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
'
|
|
3035
|
-
|
|
3036
|
-
'</div>' +
|
|
3037
|
-
'</div>';
|
|
3038
|
-
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);
|
|
3039
3371
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
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
|
+
}
|
|
3053
3412
|
|
|
3054
3413
|
fetch("/api/file-preview?path=" + encodeURIComponent(filePath), { credentials: "same-origin" })
|
|
3055
|
-
.then(function(res) {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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
|
+
}
|
|
3060
3435
|
return;
|
|
3061
3436
|
}
|
|
3062
|
-
|
|
3437
|
+
_activeFilePreview.data = result.data;
|
|
3438
|
+
renderPreviewContent(overlay, result.data);
|
|
3063
3439
|
})
|
|
3064
|
-
.catch(function(
|
|
3440
|
+
.catch(function() {
|
|
3065
3441
|
var body = overlay.querySelector(".file-preview-body");
|
|
3066
|
-
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>';
|
|
3067
3443
|
});
|
|
3068
3444
|
}
|
|
3069
3445
|
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
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
|
+
}
|
|
3073
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();
|
|
3074
3472
|
var langBadge = document.createElement("span");
|
|
3075
3473
|
langBadge.className = "file-preview-lang";
|
|
3076
|
-
|
|
3077
|
-
|
|
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);
|
|
3078
3481
|
|
|
3079
|
-
|
|
3482
|
+
renderPreviewToolbar(overlay, data);
|
|
3080
3483
|
|
|
3081
|
-
|
|
3082
|
-
|
|
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>';
|
|
3083
3501
|
} else {
|
|
3084
|
-
|
|
3085
|
-
var lines = highlighted.split("\n");
|
|
3086
|
-
var lineNums = lines.map(function(_, i) { return i + 1; });
|
|
3087
|
-
|
|
3088
|
-
body.innerHTML =
|
|
3089
|
-
'<div class="code-preview-wrapper">' +
|
|
3090
|
-
'<div class="code-preview-lines">' + lineNums.join("\n") + '</div>' +
|
|
3091
|
-
'<div class="code-preview-content"><pre>' + lines.join("\n") + '</pre></div>' +
|
|
3092
|
-
'</div>';
|
|
3502
|
+
renderTextPreview(body, data);
|
|
3093
3503
|
}
|
|
3094
3504
|
}
|
|
3095
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); }});
|
|
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";
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3096
3651
|
function highlightCodePreview(code, lang) {
|
|
3097
3652
|
// Escape HTML first
|
|
3098
3653
|
var escaped = code
|
|
@@ -4020,6 +4575,8 @@
|
|
|
4020
4575
|
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
4021
4576
|
var defaultModelRefreshBtn = document.getElementById("cfg-default-model-refresh");
|
|
4022
4577
|
if (defaultModelRefreshBtn) defaultModelRefreshBtn.addEventListener("click", refreshAvailableModels);
|
|
4578
|
+
var viewEnvBtn = document.getElementById("cfg-view-env-btn");
|
|
4579
|
+
if (viewEnvBtn) viewEnvBtn.addEventListener("click", openEnvPreviewModal);
|
|
4023
4580
|
var saveDisplayBtn = document.getElementById("save-display-button");
|
|
4024
4581
|
if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
|
|
4025
4582
|
// App icon picker (APK only)
|
|
@@ -4415,27 +4972,28 @@
|
|
|
4415
4972
|
topbarMoreBtn.classList.remove("active");
|
|
4416
4973
|
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
4417
4974
|
switch (action) {
|
|
4418
|
-
case "
|
|
4419
|
-
|
|
4975
|
+
case "copy-claude-session-id":
|
|
4976
|
+
copySelectedSessionField("claudeSessionId", "Claude 会话 ID 已复制");
|
|
4420
4977
|
break;
|
|
4421
|
-
case "
|
|
4422
|
-
|
|
4978
|
+
case "copy-cwd":
|
|
4979
|
+
copySelectedSessionField("cwd", "工作目录已复制");
|
|
4423
4980
|
break;
|
|
4424
|
-
case "
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
state.showInstallPrompt = false;
|
|
4430
|
-
updateInstallPrompt();
|
|
4431
|
-
});
|
|
4432
|
-
}
|
|
4981
|
+
case "copy-session-id":
|
|
4982
|
+
copySelectedSessionField("id", "会话 ID 已复制");
|
|
4983
|
+
break;
|
|
4984
|
+
case "worktree-merge":
|
|
4985
|
+
if (state.selectedId) openWorktreeMergeModal(state.selectedId);
|
|
4433
4986
|
break;
|
|
4434
|
-
case "
|
|
4435
|
-
|
|
4987
|
+
case "worktree-cleanup":
|
|
4988
|
+
if (state.selectedId) retryWorktreeCleanup(state.selectedId);
|
|
4436
4989
|
break;
|
|
4437
|
-
case "
|
|
4438
|
-
|
|
4990
|
+
case "delete-session":
|
|
4991
|
+
if (state.selectedId) {
|
|
4992
|
+
var pendingId = state.selectedId;
|
|
4993
|
+
if (confirm("确定要删除当前会话吗?")) {
|
|
4994
|
+
deleteSession(pendingId);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4439
4997
|
break;
|
|
4440
4998
|
}
|
|
4441
4999
|
});
|
|
@@ -4496,7 +5054,11 @@
|
|
|
4496
5054
|
setChatAutoFollow(true, { scrollNow: true, smooth: true });
|
|
4497
5055
|
});
|
|
4498
5056
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
4499
|
-
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);
|
|
4500
5062
|
|
|
4501
5063
|
// File search
|
|
4502
5064
|
var fileSearchInput = document.getElementById("file-search-input");
|
|
@@ -5069,6 +5631,20 @@
|
|
|
5069
5631
|
}
|
|
5070
5632
|
}
|
|
5071
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
|
+
|
|
5072
5648
|
/** Copy Claude session ID from badge to clipboard */
|
|
5073
5649
|
function handleClaudeIdCopy(event) {
|
|
5074
5650
|
var badge = event.target.closest("#claude-session-id-badge");
|
|
@@ -6002,31 +6578,14 @@
|
|
|
6002
6578
|
}
|
|
6003
6579
|
|
|
6004
6580
|
function getComposerPlaceholder(session, terminalInteractive) {
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
if (session &&
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
: "向 Claude 发送消息;chat 为结构化对话视图";
|
|
6012
|
-
}
|
|
6013
|
-
if (session && session.provider === "codex") {
|
|
6014
|
-
if (session.status !== "running") {
|
|
6015
|
-
return "Codex 会话已结束,无法继续发送";
|
|
6016
|
-
}
|
|
6017
|
-
return state.currentView === "terminal"
|
|
6018
|
-
? "向 Codex 发送输入;terminal 为原始 TUI 输出"
|
|
6019
|
-
: "向 Codex 发送输入;chat 为解析后的阅读视图";
|
|
6581
|
+
// Keep placeholders short so they don't wrap on portrait mobile screens.
|
|
6582
|
+
// Only show informative state hints; drop the redundant "send to X" labels.
|
|
6583
|
+
if (terminalInteractive) return "终端交互中";
|
|
6584
|
+
if (session && session.status !== "running") {
|
|
6585
|
+
if (canAutoResumeSession(session)) return "";
|
|
6586
|
+
return "会话已结束";
|
|
6020
6587
|
}
|
|
6021
|
-
|
|
6022
|
-
if (canAutoResumeSession(session)) {
|
|
6023
|
-
return "输入消息...";
|
|
6024
|
-
}
|
|
6025
|
-
return "会话已结束,无法继续发送";
|
|
6026
|
-
}
|
|
6027
|
-
return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
|
|
6028
|
-
? "思考中 · 发送新消息将中断当前回复"
|
|
6029
|
-
: "输入消息...";
|
|
6588
|
+
return "";
|
|
6030
6589
|
}
|
|
6031
6590
|
|
|
6032
6591
|
function getToolModeHint(tool, mode) {
|
|
@@ -6207,6 +6766,143 @@
|
|
|
6207
6766
|
});
|
|
6208
6767
|
}
|
|
6209
6768
|
|
|
6769
|
+
// ── Environment-variable preview modal ──
|
|
6770
|
+
// Lazily creates a modal showing the exact env vars wand will inject
|
|
6771
|
+
// into PTY / structured child processes (mirrors buildChildEnv()).
|
|
6772
|
+
function openEnvPreviewModal() {
|
|
6773
|
+
var modal = document.getElementById("env-preview-modal");
|
|
6774
|
+
if (!modal) {
|
|
6775
|
+
modal = document.createElement("section");
|
|
6776
|
+
modal.id = "env-preview-modal";
|
|
6777
|
+
modal.className = "modal-backdrop hidden";
|
|
6778
|
+
modal.innerHTML =
|
|
6779
|
+
'<div class="modal env-preview-modal" role="dialog" aria-labelledby="env-preview-title" aria-modal="true">' +
|
|
6780
|
+
'<div class="modal-header">' +
|
|
6781
|
+
'<div>' +
|
|
6782
|
+
'<h2 class="modal-title" id="env-preview-title">将注入子进程的环境变量</h2>' +
|
|
6783
|
+
'<p class="modal-subtitle" id="env-preview-subtitle">这些变量会被传给 claude / codex(PTY 与结构化运行器一致)。</p>' +
|
|
6784
|
+
'</div>' +
|
|
6785
|
+
'<button id="env-preview-close" class="btn btn-ghost btn-icon modal-close-btn" type="button" aria-label="关闭">' +
|
|
6786
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">' +
|
|
6787
|
+
'<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>' +
|
|
6788
|
+
'</svg>' +
|
|
6789
|
+
'</button>' +
|
|
6790
|
+
'</div>' +
|
|
6791
|
+
'<div class="modal-body env-preview-body">' +
|
|
6792
|
+
'<div class="env-preview-toolbar">' +
|
|
6793
|
+
'<div class="env-preview-meta" id="env-preview-meta">加载中…</div>' +
|
|
6794
|
+
'<div class="env-preview-controls">' +
|
|
6795
|
+
'<input id="env-preview-search" class="env-preview-search" type="search" placeholder="搜索变量名…" />' +
|
|
6796
|
+
'<label class="env-preview-reveal">' +
|
|
6797
|
+
'<input id="env-preview-reveal-toggle" type="checkbox" />' +
|
|
6798
|
+
'<span>显示敏感值</span>' +
|
|
6799
|
+
'</label>' +
|
|
6800
|
+
'</div>' +
|
|
6801
|
+
'</div>' +
|
|
6802
|
+
'<div class="env-preview-list" id="env-preview-list" tabindex="0">' +
|
|
6803
|
+
'<div class="env-preview-loading">加载中…</div>' +
|
|
6804
|
+
'</div>' +
|
|
6805
|
+
'</div>' +
|
|
6806
|
+
'<div class="modal-footer env-preview-footer">' +
|
|
6807
|
+
'<span class="env-preview-hint">敏感字段(含 KEY/TOKEN/SECRET 等)默认掩码,可勾选「显示敏感值」临时还原。</span>' +
|
|
6808
|
+
'<button id="env-preview-close-2" class="btn btn-secondary btn-sm" type="button">关闭</button>' +
|
|
6809
|
+
'</div>' +
|
|
6810
|
+
'</div>';
|
|
6811
|
+
document.body.appendChild(modal);
|
|
6812
|
+
|
|
6813
|
+
// Click outside to close
|
|
6814
|
+
modal.addEventListener("click", function(e) {
|
|
6815
|
+
if (e.target === modal) closeEnvPreviewModal();
|
|
6816
|
+
});
|
|
6817
|
+
var closeBtn = modal.querySelector("#env-preview-close");
|
|
6818
|
+
if (closeBtn) closeBtn.addEventListener("click", closeEnvPreviewModal);
|
|
6819
|
+
var closeBtn2 = modal.querySelector("#env-preview-close-2");
|
|
6820
|
+
if (closeBtn2) closeBtn2.addEventListener("click", closeEnvPreviewModal);
|
|
6821
|
+
var searchEl = modal.querySelector("#env-preview-search");
|
|
6822
|
+
if (searchEl) searchEl.addEventListener("input", function() { renderEnvPreviewList(); });
|
|
6823
|
+
var revealEl = modal.querySelector("#env-preview-reveal-toggle");
|
|
6824
|
+
if (revealEl) revealEl.addEventListener("change", function() { loadEnvPreview(revealEl.checked); });
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6827
|
+
modal.classList.remove("closing");
|
|
6828
|
+
modal.classList.remove("hidden");
|
|
6829
|
+
var revealEl = modal.querySelector("#env-preview-reveal-toggle");
|
|
6830
|
+
if (revealEl) revealEl.checked = false;
|
|
6831
|
+
var searchEl = modal.querySelector("#env-preview-search");
|
|
6832
|
+
if (searchEl) searchEl.value = "";
|
|
6833
|
+
loadEnvPreview(false);
|
|
6834
|
+
}
|
|
6835
|
+
|
|
6836
|
+
function closeEnvPreviewModal() {
|
|
6837
|
+
var modal = document.getElementById("env-preview-modal");
|
|
6838
|
+
if (!modal) return;
|
|
6839
|
+
animateModalClose(modal);
|
|
6840
|
+
}
|
|
6841
|
+
|
|
6842
|
+
function loadEnvPreview(reveal) {
|
|
6843
|
+
var listEl = document.getElementById("env-preview-list");
|
|
6844
|
+
var metaEl = document.getElementById("env-preview-meta");
|
|
6845
|
+
if (listEl) listEl.innerHTML = '<div class="env-preview-loading">加载中…</div>';
|
|
6846
|
+
if (metaEl) metaEl.textContent = "加载中…";
|
|
6847
|
+
var url = "/api/settings/env-preview" + (reveal ? "?reveal=1" : "");
|
|
6848
|
+
fetch(url, { credentials: "same-origin" })
|
|
6849
|
+
.then(function(res) { return res.json(); })
|
|
6850
|
+
.then(function(data) {
|
|
6851
|
+
if (!data || !Array.isArray(data.entries)) {
|
|
6852
|
+
if (listEl) listEl.innerHTML = '<div class="env-preview-empty">读取失败。</div>';
|
|
6853
|
+
if (metaEl) metaEl.textContent = "读取失败";
|
|
6854
|
+
return;
|
|
6855
|
+
}
|
|
6856
|
+
state._envPreview = data;
|
|
6857
|
+
if (metaEl) {
|
|
6858
|
+
var inheritLabel = data.inheritEnv ? "继承父进程" : "最小白名单";
|
|
6859
|
+
metaEl.innerHTML =
|
|
6860
|
+
'<span class="env-preview-pill ' + (data.inheritEnv ? "is-inherit" : "is-minimal") + '">' + inheritLabel + '</span>' +
|
|
6861
|
+
'<span class="env-preview-count">共 ' + data.total + ' 项</span>';
|
|
6862
|
+
}
|
|
6863
|
+
renderEnvPreviewList();
|
|
6864
|
+
})
|
|
6865
|
+
.catch(function() {
|
|
6866
|
+
if (listEl) listEl.innerHTML = '<div class="env-preview-empty">读取失败,请稍后重试。</div>';
|
|
6867
|
+
if (metaEl) metaEl.textContent = "读取失败";
|
|
6868
|
+
});
|
|
6869
|
+
}
|
|
6870
|
+
|
|
6871
|
+
function renderEnvPreviewList() {
|
|
6872
|
+
var listEl = document.getElementById("env-preview-list");
|
|
6873
|
+
if (!listEl) return;
|
|
6874
|
+
var data = state._envPreview;
|
|
6875
|
+
if (!data || !Array.isArray(data.entries)) {
|
|
6876
|
+
listEl.innerHTML = '<div class="env-preview-empty">尚未加载。</div>';
|
|
6877
|
+
return;
|
|
6878
|
+
}
|
|
6879
|
+
var searchEl = document.getElementById("env-preview-search");
|
|
6880
|
+
var query = (searchEl && searchEl.value || "").trim().toLowerCase();
|
|
6881
|
+
var html = "";
|
|
6882
|
+
var shown = 0;
|
|
6883
|
+
for (var i = 0; i < data.entries.length; i++) {
|
|
6884
|
+
var entry = data.entries[i];
|
|
6885
|
+
if (query && entry.name.toLowerCase().indexOf(query) === -1) continue;
|
|
6886
|
+
shown++;
|
|
6887
|
+
var isPlaceholder = typeof entry.value === "string" && entry.value.charAt(0) === "<" && entry.value.charAt(entry.value.length - 1) === ">";
|
|
6888
|
+
html += '<div class="env-preview-row' + (entry.sensitive ? " is-sensitive" : "") + '">' +
|
|
6889
|
+
'<div class="env-preview-name">' +
|
|
6890
|
+
escapeHtml(entry.name) +
|
|
6891
|
+
(entry.sensitive ? '<span class="env-preview-badge" title="被识别为敏感字段">敏感</span>' : '') +
|
|
6892
|
+
(isPlaceholder ? '<span class="env-preview-badge env-preview-badge-runtime" title="按会话动态注入">运行时</span>' : '') +
|
|
6893
|
+
'</div>' +
|
|
6894
|
+
'<div class="env-preview-value' + (isPlaceholder ? " is-runtime" : "") + '" title="' + escapeHtml(String(entry.value)) + '">' +
|
|
6895
|
+
escapeHtml(String(entry.value)) +
|
|
6896
|
+
'</div>' +
|
|
6897
|
+
'<div class="env-preview-len">' + entry.length + ' 字符</div>' +
|
|
6898
|
+
'</div>';
|
|
6899
|
+
}
|
|
6900
|
+
if (shown === 0) {
|
|
6901
|
+
html = '<div class="env-preview-empty">没有匹配的变量。</div>';
|
|
6902
|
+
}
|
|
6903
|
+
listEl.innerHTML = html;
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6210
6906
|
function updateSettingsDefaultModelSelect(data) {
|
|
6211
6907
|
var select = document.getElementById("cfg-default-model");
|
|
6212
6908
|
if (!select) return;
|
|
@@ -9253,7 +9949,7 @@
|
|
|
9253
9949
|
var todoEl = document.getElementById("todo-progress");
|
|
9254
9950
|
if (todoEl) todoEl.classList.add("hidden");
|
|
9255
9951
|
welcomeInput.value = "";
|
|
9256
|
-
welcomeInput.placeholder = "
|
|
9952
|
+
welcomeInput.placeholder = "正在启动…";
|
|
9257
9953
|
welcomeInput.disabled = true;
|
|
9258
9954
|
var mode = state.chatMode || "managed";
|
|
9259
9955
|
var defaultCwd = getEffectiveCwd();
|
|
@@ -9274,7 +9970,7 @@
|
|
|
9274
9970
|
.then(function(data) {
|
|
9275
9971
|
if (data.error) {
|
|
9276
9972
|
showToast(data.error, "error");
|
|
9277
|
-
welcomeInput.placeholder = "
|
|
9973
|
+
welcomeInput.placeholder = "输入消息";
|
|
9278
9974
|
welcomeInput.disabled = false;
|
|
9279
9975
|
return;
|
|
9280
9976
|
}
|
|
@@ -9287,7 +9983,7 @@
|
|
|
9287
9983
|
switchToSessionView(data.id);
|
|
9288
9984
|
subscribeToSession(data.id);
|
|
9289
9985
|
loadOutput(data.id).then(function() {
|
|
9290
|
-
welcomeInput.placeholder = "
|
|
9986
|
+
welcomeInput.placeholder = "输入消息";
|
|
9291
9987
|
welcomeInput.disabled = false;
|
|
9292
9988
|
focusInputBox(true);
|
|
9293
9989
|
});
|
|
@@ -9296,7 +9992,7 @@
|
|
|
9296
9992
|
showToast((error && error.message) || (preferredTool === "codex"
|
|
9297
9993
|
? "无法启动 Codex 会话。"
|
|
9298
9994
|
: "无法启动 Claude 会话。"), "error");
|
|
9299
|
-
welcomeInput.placeholder = "
|
|
9995
|
+
welcomeInput.placeholder = "输入消息";
|
|
9300
9996
|
welcomeInput.disabled = false;
|
|
9301
9997
|
});
|
|
9302
9998
|
}
|
|
@@ -10672,7 +11368,7 @@
|
|
|
10672
11368
|
function createSessionFromWelcomeInput(value) {
|
|
10673
11369
|
var welcomeInput = document.getElementById("welcome-input");
|
|
10674
11370
|
if (!welcomeInput) return;
|
|
10675
|
-
welcomeInput.placeholder = "
|
|
11371
|
+
welcomeInput.placeholder = "正在思考…";
|
|
10676
11372
|
welcomeInput.disabled = true;
|
|
10677
11373
|
var mode = state.chatMode || "managed";
|
|
10678
11374
|
var defaultCwd = getEffectiveCwd();
|
|
@@ -10694,7 +11390,7 @@
|
|
|
10694
11390
|
.then(function(data) {
|
|
10695
11391
|
if (data.error) {
|
|
10696
11392
|
showToast(data.error, "error");
|
|
10697
|
-
welcomeInput.placeholder = "
|
|
11393
|
+
welcomeInput.placeholder = "输入消息";
|
|
10698
11394
|
welcomeInput.disabled = false;
|
|
10699
11395
|
return null;
|
|
10700
11396
|
}
|
|
@@ -10702,11 +11398,11 @@
|
|
|
10702
11398
|
})
|
|
10703
11399
|
.catch(function(error) {
|
|
10704
11400
|
showToast((error && error.message) || "无法启动会话。", "error");
|
|
10705
|
-
welcomeInput.placeholder = "
|
|
11401
|
+
welcomeInput.placeholder = "输入消息";
|
|
10706
11402
|
welcomeInput.disabled = false;
|
|
10707
11403
|
})
|
|
10708
11404
|
.finally(function() {
|
|
10709
|
-
welcomeInput.placeholder = "
|
|
11405
|
+
welcomeInput.placeholder = "输入消息";
|
|
10710
11406
|
welcomeInput.disabled = false;
|
|
10711
11407
|
});
|
|
10712
11408
|
}
|
|
@@ -15972,43 +16668,87 @@
|
|
|
15972
16668
|
playNotificationSound();
|
|
15973
16669
|
|
|
15974
16670
|
var id = ++notificationIdCounter;
|
|
15975
|
-
var
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
'<
|
|
16671
|
+
var card = document.createElement("div");
|
|
16672
|
+
// Reuse the notification stacking system but with a richer card style.
|
|
16673
|
+
card.className = "notification-bubble update-card";
|
|
16674
|
+
card.setAttribute("data-nid", id);
|
|
16675
|
+
|
|
16676
|
+
card.innerHTML =
|
|
16677
|
+
'<div class="update-card-shine" aria-hidden="true"></div>' +
|
|
16678
|
+
'<div class="update-card-header">' +
|
|
16679
|
+
'<div class="update-card-icon" aria-hidden="true">' +
|
|
16680
|
+
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
16681
|
+
'<path d="M12 19V5"/><path d="M5 12l7-7 7 7"/>' +
|
|
16682
|
+
'</svg>' +
|
|
16683
|
+
'</div>' +
|
|
16684
|
+
'<div class="update-card-heading">' +
|
|
16685
|
+
'<div class="update-card-title">\u53d1\u73b0\u65b0\u7248\u672c</div>' +
|
|
16686
|
+
'<div class="update-card-subtitle" id="update-card-subtitle">\u70b9\u51fb\u4e0b\u65b9\u6309\u94ae\u4e00\u952e\u66f4\u65b0</div>' +
|
|
16687
|
+
'</div>' +
|
|
16688
|
+
'<button class="update-card-close" title="\u7a0d\u540e\u63d0\u9192" aria-label="\u5173\u95ed">' +
|
|
16689
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
16690
|
+
'<path d="M18 6L6 18"/><path d="M6 6l12 12"/>' +
|
|
16691
|
+
'</svg>' +
|
|
16692
|
+
'</button>' +
|
|
16693
|
+
'</div>' +
|
|
16694
|
+
'<div class="update-card-version">' +
|
|
16695
|
+
'<span class="update-card-version-chip update-card-version-current">v' + escapeHtml(String(currentVer).replace(/^v/, "")) + '</span>' +
|
|
16696
|
+
'<svg class="update-card-version-arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
16697
|
+
'<path d="M5 12h14"/><path d="M13 5l7 7-7 7"/>' +
|
|
16698
|
+
'</svg>' +
|
|
16699
|
+
'<span class="update-card-version-chip update-card-version-latest">v' + escapeHtml(String(latestVer).replace(/^v/, "")) + '</span>' +
|
|
15984
16700
|
'</div>' +
|
|
15985
|
-
'<div class="
|
|
15986
|
-
|
|
16701
|
+
'<div class="update-card-progress" id="update-card-progress" aria-hidden="true">' +
|
|
16702
|
+
'<div class="update-card-progress-track"><div class="update-card-progress-fill"></div></div>' +
|
|
15987
16703
|
'</div>' +
|
|
15988
|
-
'<div class="
|
|
15989
|
-
|
|
16704
|
+
'<div class="update-card-status hidden" id="update-card-status"></div>' +
|
|
16705
|
+
'<div class="update-card-actions">' +
|
|
16706
|
+
'<button class="update-card-action update-card-action-primary" id="update-bubble-action" type="button">' +
|
|
16707
|
+
'<span class="update-card-action-label">\u7acb\u5373\u66f4\u65b0</span>' +
|
|
16708
|
+
'</button>' +
|
|
15990
16709
|
'</div>';
|
|
15991
16710
|
|
|
15992
|
-
document.body.appendChild(
|
|
16711
|
+
document.body.appendChild(card);
|
|
15993
16712
|
|
|
15994
|
-
var entry = { id: id, el:
|
|
16713
|
+
var entry = { id: id, el: card };
|
|
15995
16714
|
notificationStack.push(entry);
|
|
15996
16715
|
repositionNotifications();
|
|
15997
16716
|
|
|
15998
|
-
var closeBtn =
|
|
16717
|
+
var closeBtn = card.querySelector(".update-card-close");
|
|
15999
16718
|
if (closeBtn) closeBtn.onclick = function() {
|
|
16000
16719
|
dismissNotification(id);
|
|
16001
16720
|
state._updateBubbleShown = false;
|
|
16002
16721
|
};
|
|
16003
16722
|
|
|
16004
|
-
var actionBtn =
|
|
16005
|
-
var
|
|
16723
|
+
var actionBtn = card.querySelector("#update-bubble-action");
|
|
16724
|
+
var actionLabel = card.querySelector(".update-card-action-label");
|
|
16725
|
+
var subtitleEl = card.querySelector("#update-card-subtitle");
|
|
16726
|
+
var statusEl = card.querySelector("#update-card-status");
|
|
16727
|
+
var progressEl = card.querySelector("#update-card-progress");
|
|
16728
|
+
|
|
16729
|
+
function setStatus(text, kind) {
|
|
16730
|
+
if (!statusEl) return;
|
|
16731
|
+
statusEl.textContent = text || "";
|
|
16732
|
+
statusEl.classList.remove("hidden", "error", "success");
|
|
16733
|
+
if (!text) { statusEl.classList.add("hidden"); return; }
|
|
16734
|
+
if (kind) statusEl.classList.add(kind);
|
|
16735
|
+
}
|
|
16736
|
+
function setSubtitle(text) {
|
|
16737
|
+
if (subtitleEl) subtitleEl.textContent = text || "";
|
|
16738
|
+
}
|
|
16739
|
+
function setProgress(active) {
|
|
16740
|
+
if (!progressEl) return;
|
|
16741
|
+
progressEl.classList.toggle("active", !!active);
|
|
16742
|
+
}
|
|
16006
16743
|
|
|
16007
16744
|
if (actionBtn) actionBtn.onclick = function() {
|
|
16008
16745
|
// Phase 1: Performing update
|
|
16009
16746
|
actionBtn.disabled = true;
|
|
16010
|
-
|
|
16011
|
-
if (
|
|
16747
|
+
card.classList.add("is-busy");
|
|
16748
|
+
if (actionLabel) actionLabel.textContent = "\u66f4\u65b0\u4e2d\u2026";
|
|
16749
|
+
setSubtitle("\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026");
|
|
16750
|
+
setProgress(true);
|
|
16751
|
+
setStatus("");
|
|
16012
16752
|
|
|
16013
16753
|
fetch("/api/update", {
|
|
16014
16754
|
method: "POST",
|
|
@@ -16017,39 +16757,58 @@
|
|
|
16017
16757
|
})
|
|
16018
16758
|
.then(function(res) { return res.json(); })
|
|
16019
16759
|
.then(function(data) {
|
|
16760
|
+
setProgress(false);
|
|
16761
|
+
card.classList.remove("is-busy");
|
|
16020
16762
|
if (data.error) {
|
|
16021
16763
|
// Update failed
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
bodyEl.style.color = "var(--error)";
|
|
16025
|
-
}
|
|
16764
|
+
setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
|
|
16765
|
+
setStatus(data.error, "error");
|
|
16026
16766
|
actionBtn.disabled = false;
|
|
16027
|
-
|
|
16767
|
+
if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
|
|
16028
16768
|
return;
|
|
16029
16769
|
}
|
|
16030
16770
|
// Phase 2: Update succeeded, show restart button
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
16035
|
-
actionBtn.textContent = "\u91cd\u542f\u751f\u6548";
|
|
16771
|
+
setSubtitle(data.message || "\u66f4\u65b0\u5b8c\u6210\uff0c\u91cd\u542f\u540e\u751f\u6548");
|
|
16772
|
+
setStatus("");
|
|
16773
|
+
card.classList.add("is-success");
|
|
16774
|
+
if (actionLabel) actionLabel.textContent = "\u91cd\u542f\u751f\u6548";
|
|
16036
16775
|
actionBtn.disabled = false;
|
|
16037
|
-
actionBtn.className = "primary success";
|
|
16038
16776
|
actionBtn.onclick = function() {
|
|
16039
|
-
|
|
16777
|
+
actionBtn.disabled = true;
|
|
16778
|
+
if (actionLabel) actionLabel.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
|
|
16779
|
+
setSubtitle("\u670d\u52a1\u6b63\u5728\u91cd\u542f\u2026");
|
|
16780
|
+
setProgress(true);
|
|
16781
|
+
performRestartCard(actionBtn, actionLabel, subtitleEl, statusEl, progressEl);
|
|
16040
16782
|
};
|
|
16041
16783
|
})
|
|
16042
16784
|
.catch(function() {
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
|
|
16785
|
+
setProgress(false);
|
|
16786
|
+
card.classList.remove("is-busy");
|
|
16787
|
+
setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
|
|
16788
|
+
setStatus("\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u540e\u91cd\u8bd5", "error");
|
|
16047
16789
|
actionBtn.disabled = false;
|
|
16048
|
-
|
|
16790
|
+
if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
|
|
16049
16791
|
});
|
|
16050
16792
|
};
|
|
16051
16793
|
}
|
|
16052
16794
|
|
|
16795
|
+
// Restart driver used by the new update card.
|
|
16796
|
+
function performRestartCard(btn, labelEl, subtitleEl, statusEl, progressEl) {
|
|
16797
|
+
fetch("/api/restart", {
|
|
16798
|
+
method: "POST",
|
|
16799
|
+
headers: { "Content-Type": "application/json" },
|
|
16800
|
+
credentials: "same-origin"
|
|
16801
|
+
})
|
|
16802
|
+
.then(function(res) { return res.json(); })
|
|
16803
|
+
.then(function() {
|
|
16804
|
+
showRestartOverlay();
|
|
16805
|
+
})
|
|
16806
|
+
.catch(function() {
|
|
16807
|
+
// Network error likely means the server already shut down \u2014 show overlay anyway
|
|
16808
|
+
showRestartOverlay();
|
|
16809
|
+
});
|
|
16810
|
+
}
|
|
16811
|
+
|
|
16053
16812
|
/**
|
|
16054
16813
|
* Call POST /api/restart and show the restart overlay.
|
|
16055
16814
|
*/
|