dsh-flyout-sidebar 0.1.0 → 0.1.2
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/client.js +118 -24
- package/dist/index.js +20 -3
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
}
|
|
33
33
|
/** host RPC 桥:与 host 侧 /flyout-sidebar/* 路由一一对应 */
|
|
34
34
|
const host = {
|
|
35
|
-
gitStatus(sessionId) {
|
|
36
|
-
return getJson("/flyout-sidebar/gitstatus" + qs(["sessionId", sessionId]));
|
|
35
|
+
gitStatus(sessionId, force) {
|
|
36
|
+
return getJson("/flyout-sidebar/gitstatus" + qs(["sessionId", sessionId], ["force", force ? "1" : ""]));
|
|
37
37
|
},
|
|
38
38
|
gitDiff(path, sessionId) {
|
|
39
39
|
return getJson("/flyout-sidebar/gitdiff" + qs(["path", path], ["sessionId", sessionId]));
|
|
@@ -68,6 +68,7 @@ header:has([data-slot="conversation.session.header.utilities"]) {
|
|
|
68
68
|
@media (prefers-reduced-motion: reduce) {
|
|
69
69
|
html #root { transition: none; }
|
|
70
70
|
header:has([data-slot="conversation.session.header.utilities"]) { transition: none; }
|
|
71
|
+
.artifacts-preview-overlay, .artifacts-panel, .artifacts-corner-btn { transition: none; }
|
|
71
72
|
}
|
|
72
73
|
.artifacts-preview-overlay {
|
|
73
74
|
position: fixed; top: 0; bottom: 0; left: 0;
|
|
@@ -81,7 +82,13 @@ header:has([data-slot="conversation.session.header.utilities"]) {
|
|
|
81
82
|
pointer-events: auto;
|
|
82
83
|
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif);
|
|
83
84
|
font-size: 13px; line-height: 1.5;
|
|
84
|
-
transition: right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease)
|
|
85
|
+
transition: right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease),
|
|
86
|
+
transform var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease);
|
|
87
|
+
}
|
|
88
|
+
.artifacts-preview-overlay.artifacts-slid-out,
|
|
89
|
+
.artifacts-panel.artifacts-slid-out {
|
|
90
|
+
transform: translateX(105%);
|
|
91
|
+
pointer-events: none;
|
|
85
92
|
}
|
|
86
93
|
body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
|
|
87
94
|
.artifacts-preview-overlay-tabs {
|
|
@@ -132,7 +139,8 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
|
|
|
132
139
|
font-size: 13px; line-height: 1.5;
|
|
133
140
|
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
134
141
|
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
135
|
-
transition: right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease)
|
|
142
|
+
transition: right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease),
|
|
143
|
+
transform var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease);
|
|
136
144
|
}
|
|
137
145
|
.artifacts-panel.artifacts-resizing { transition: none; user-select: none; }
|
|
138
146
|
.artifacts-head {
|
|
@@ -157,7 +165,16 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
|
|
|
157
165
|
}
|
|
158
166
|
.artifacts-iconbtn:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
159
167
|
.artifacts-main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
|
160
|
-
.artifacts-body { flex: 0 0 auto; min-height: 0; overflow-y: auto; }
|
|
168
|
+
.artifacts-body { flex: 0 0 auto; min-height: 0; overflow-y: auto; transition: opacity .15s; }
|
|
169
|
+
.artifacts-body.artifacts-refreshing { opacity: .45; }
|
|
170
|
+
/* 刷新后逐行浮现:延迟由行内 style 按行号注入 */
|
|
171
|
+
.artifacts-item.artifacts-flash-in,
|
|
172
|
+
.artifacts-tree-node.artifacts-flash-in,
|
|
173
|
+
.artifacts-tree-row.artifacts-flash-in { animation: artifacts-flash-in .3s var(--ds-ease-in-out, ease) both; }
|
|
174
|
+
@keyframes artifacts-flash-in {
|
|
175
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
176
|
+
to { opacity: 1; transform: none; }
|
|
177
|
+
}
|
|
161
178
|
.artifacts-empty { padding: 28px 16px; color: var(--dsw-alias-label-tertiary); text-align: center; }
|
|
162
179
|
.artifacts-item {
|
|
163
180
|
display: flex; align-items: stretch; width: 100%; padding: 0;
|
|
@@ -176,11 +193,17 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
|
|
|
176
193
|
.artifacts-hint { padding: 24px 16px; color: var(--dsw-alias-label-tertiary); text-align: center; }
|
|
177
194
|
.artifacts-error { padding: 16px; color: var(--dsw-alias-state-error-primary); font-family: var(--dsh-font-mono, monospace); word-break: break-all; }
|
|
178
195
|
.artifacts-corner-btn {
|
|
179
|
-
position: fixed; top:
|
|
180
|
-
z-index: 10000; width: 36px; height:
|
|
196
|
+
position: fixed; top: 0; right: calc(var(--dsh-sidebar-width, 0px) + 12px);
|
|
197
|
+
z-index: 10000; width: 36px; height: 28px; padding: 0;
|
|
181
198
|
border: none; background: transparent; color: var(--dsw-alias-label-secondary);
|
|
182
199
|
cursor: pointer; align-items: center; justify-content: center; display: inline-flex;
|
|
183
|
-
transition:
|
|
200
|
+
transition: transform var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease),
|
|
201
|
+
right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease), color .15s;
|
|
202
|
+
}
|
|
203
|
+
/* 面板打开时随面板一起滑出屏右缘(推拉动画的另一半) */
|
|
204
|
+
.artifacts-corner-btn.artifacts-slid-out {
|
|
205
|
+
transform: translateX(calc(100% + 24px));
|
|
206
|
+
pointer-events: none;
|
|
184
207
|
}
|
|
185
208
|
.artifacts-corner-btn:hover { color: var(--dsw-alias-label-primary); }
|
|
186
209
|
.artifacts-preview-body { flex: 1; min-height: 0; overflow-y: auto; position: relative; }
|
|
@@ -494,6 +517,54 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
494
517
|
}
|
|
495
518
|
};
|
|
496
519
|
store.open = !!settingsStore.get().defaultOpen;
|
|
520
|
+
const slideState = {
|
|
521
|
+
visible: store.open,
|
|
522
|
+
slidOut: !store.open
|
|
523
|
+
};
|
|
524
|
+
const slideListeners = [];
|
|
525
|
+
const setSlide = (patch) => {
|
|
526
|
+
let changed = false;
|
|
527
|
+
for (const key of ["visible", "slidOut"]) {
|
|
528
|
+
const v = patch[key];
|
|
529
|
+
if (v !== void 0 && v !== slideState[key]) {
|
|
530
|
+
slideState[key] = v;
|
|
531
|
+
changed = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!changed) return;
|
|
535
|
+
const next = { ...slideState };
|
|
536
|
+
for (const fn of slideListeners) try {
|
|
537
|
+
fn(next);
|
|
538
|
+
} catch {}
|
|
539
|
+
};
|
|
540
|
+
let slideTimer = null;
|
|
541
|
+
store.subscribe((open) => {
|
|
542
|
+
if (open) {
|
|
543
|
+
if (slideTimer) {
|
|
544
|
+
clearTimeout(slideTimer);
|
|
545
|
+
slideTimer = null;
|
|
546
|
+
}
|
|
547
|
+
setSlide({ visible: true });
|
|
548
|
+
requestAnimationFrame(() => requestAnimationFrame(() => setSlide({ slidOut: false })));
|
|
549
|
+
} else {
|
|
550
|
+
setSlide({ slidOut: true });
|
|
551
|
+
slideTimer = setTimeout(() => {
|
|
552
|
+
slideTimer = null;
|
|
553
|
+
setSlide({ visible: false });
|
|
554
|
+
}, 240);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
const useSlide = () => {
|
|
558
|
+
const [s, setS] = React.useState({ ...slideState });
|
|
559
|
+
React.useEffect(() => {
|
|
560
|
+
slideListeners.push(setS);
|
|
561
|
+
return () => {
|
|
562
|
+
const i = slideListeners.indexOf(setS);
|
|
563
|
+
if (i >= 0) slideListeners.splice(i, 1);
|
|
564
|
+
};
|
|
565
|
+
}, []);
|
|
566
|
+
return s;
|
|
567
|
+
};
|
|
497
568
|
const useSettings = () => {
|
|
498
569
|
const [s, setS] = React.useState(settingsStore.get());
|
|
499
570
|
React.useEffect(() => settingsStore.subscribe(setS), []);
|
|
@@ -1361,14 +1432,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1361
1432
|
copyRef(entry.path);
|
|
1362
1433
|
}
|
|
1363
1434
|
}, "@引用");
|
|
1364
|
-
const renderNode = (entry, depth) => {
|
|
1435
|
+
const renderNode = (entry, depth, flashDelay) => {
|
|
1365
1436
|
const pad = { paddingLeft: 6 + depth * 20 };
|
|
1366
1437
|
const isSelected = selectedPath === entry.path;
|
|
1367
1438
|
const rowClass = "artifacts-tree-row" + (entry.hidden ? " artifacts-tree-hidden" : "") + (isSelected ? " is-selected" : "");
|
|
1368
1439
|
if (entry.isDir) {
|
|
1369
1440
|
const isExpanded = !!expanded[entry.path];
|
|
1370
1441
|
const node = children[entry.path];
|
|
1371
|
-
return /* @__PURE__ */ h("div", {
|
|
1442
|
+
return /* @__PURE__ */ h("div", {
|
|
1443
|
+
key: entry.path,
|
|
1444
|
+
className: flashDelay != null ? "artifacts-tree-node artifacts-flash-in" : "artifacts-tree-node",
|
|
1445
|
+
style: flashDelay != null ? { animationDelay: flashDelay + "ms" } : void 0
|
|
1446
|
+
}, /* @__PURE__ */ h("div", {
|
|
1372
1447
|
role: "button",
|
|
1373
1448
|
tabIndex: 0,
|
|
1374
1449
|
className: rowClass + " artifacts-tree-dir",
|
|
@@ -1392,8 +1467,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1392
1467
|
return /* @__PURE__ */ h("div", {
|
|
1393
1468
|
role: "button",
|
|
1394
1469
|
tabIndex: 0,
|
|
1395
|
-
className: rowClass,
|
|
1396
|
-
style:
|
|
1470
|
+
className: rowClass + (flashDelay != null ? " artifacts-flash-in" : ""),
|
|
1471
|
+
style: flashDelay != null ? {
|
|
1472
|
+
...pad,
|
|
1473
|
+
animationDelay: flashDelay + "ms"
|
|
1474
|
+
} : pad,
|
|
1397
1475
|
onClick: () => {
|
|
1398
1476
|
if (onOpen) onOpen(entry.path);
|
|
1399
1477
|
},
|
|
@@ -1406,7 +1484,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1406
1484
|
title: entry.path
|
|
1407
1485
|
}, /* @__PURE__ */ h(FileCodeIcon, { size: 14 }), /* @__PURE__ */ h("span", { className: "artifacts-tree-name" }, entry.name), rowActions(entry));
|
|
1408
1486
|
};
|
|
1409
|
-
return /* @__PURE__ */ h("div", { className: "artifacts-tree" }, /* @__PURE__ */ h("div", { className: "artifacts-tree-body" }, !root ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "加载文件树…") : !root.entries || !root.entries.length ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "(空目录)") : root.entries.map((e) => renderNode(e, 0))));
|
|
1487
|
+
return /* @__PURE__ */ h("div", { className: "artifacts-tree" }, /* @__PURE__ */ h("div", { className: "artifacts-tree-body" }, !root ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "加载文件树…") : !root.entries || !root.entries.length ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "(空目录)") : root.entries.map((e, i) => renderNode(e, 0, Math.min(i, 12) * 45))));
|
|
1410
1488
|
}
|
|
1411
1489
|
function ArtifactsPanel() {
|
|
1412
1490
|
const open = useOpen();
|
|
@@ -1433,13 +1511,22 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1433
1511
|
const [activeView, setActiveView] = React.useState(() => settings.showFileTree ? "tree" : "git");
|
|
1434
1512
|
const [treeRefresh, setTreeRefresh] = React.useState(0);
|
|
1435
1513
|
const [gitRefresh, setGitRefresh] = React.useState(0);
|
|
1514
|
+
const [gitRefreshing, setGitRefreshing] = React.useState(false);
|
|
1515
|
+
const [gitFlash, setGitFlash] = React.useState(0);
|
|
1436
1516
|
const noticeTimer = React.useRef(null);
|
|
1517
|
+
const gitForceRef = React.useRef(false);
|
|
1437
1518
|
React.useEffect(() => {
|
|
1438
1519
|
if (!open || activeView !== "git") return;
|
|
1439
1520
|
let alive = true;
|
|
1440
1521
|
const load = () => {
|
|
1441
|
-
|
|
1522
|
+
const force = gitForceRef.current;
|
|
1523
|
+
gitForceRef.current = false;
|
|
1524
|
+
host.gitStatus(currentSessionId(), force).then((res) => {
|
|
1442
1525
|
if (!alive) return;
|
|
1526
|
+
if (force) {
|
|
1527
|
+
setGitRefreshing(false);
|
|
1528
|
+
setGitFlash((n) => n + 1);
|
|
1529
|
+
}
|
|
1443
1530
|
if (res && res.ok) {
|
|
1444
1531
|
setGitFiles(Array.isArray(res.entries) ? res.entries : []);
|
|
1445
1532
|
setGitError(null);
|
|
@@ -1448,6 +1535,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1448
1535
|
setGitError(res && res.error || "git status 失败");
|
|
1449
1536
|
}
|
|
1450
1537
|
}).catch((e) => {
|
|
1538
|
+
if (force) setGitRefreshing(false);
|
|
1451
1539
|
if (alive) setGitError(e instanceof Error && e.message ? String(e.message) : String(e));
|
|
1452
1540
|
});
|
|
1453
1541
|
};
|
|
@@ -1523,7 +1611,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1523
1611
|
document.body.removeAttribute("data-dsh-flyout-dragging");
|
|
1524
1612
|
};
|
|
1525
1613
|
}, [resizing]);
|
|
1526
|
-
|
|
1614
|
+
const { visible, slidOut } = useSlide();
|
|
1615
|
+
if (!visible) return null;
|
|
1527
1616
|
const flyoutHref = "/flyout-sidebar" + (sessionId ? "?sessionId=" + encodeURIComponent(sessionId) : "");
|
|
1528
1617
|
const startResize = (e) => {
|
|
1529
1618
|
e.preventDefault();
|
|
@@ -1671,12 +1760,14 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1671
1760
|
key: "empty",
|
|
1672
1761
|
className: "artifacts-empty"
|
|
1673
1762
|
}, "没有未提交的变更"));
|
|
1674
|
-
(gitFiles || []).forEach((e) => {
|
|
1763
|
+
(gitFiles || []).forEach((e, idx) => {
|
|
1675
1764
|
const label = gitLabel(e);
|
|
1676
1765
|
const isActive = !!(activeTab && activeTab.git && activeTab.path === e.path);
|
|
1766
|
+
const flashKey = gitFlash > 0 ? gitFlash + ":" : "";
|
|
1677
1767
|
gitListRows.push(/* @__PURE__ */ h("div", {
|
|
1678
|
-
key: e.path,
|
|
1679
|
-
className: "artifacts-item" + (isActive ? " is-active" : "")
|
|
1768
|
+
key: flashKey + e.path,
|
|
1769
|
+
className: "artifacts-item" + (isActive ? " is-active" : "") + (flashKey ? " artifacts-flash-in" : ""),
|
|
1770
|
+
style: flashKey ? { animationDelay: Math.min(idx, 12) * 45 + "ms" } : void 0
|
|
1680
1771
|
}, /* @__PURE__ */ h("button", {
|
|
1681
1772
|
type: "button",
|
|
1682
1773
|
className: "artifacts-item-main",
|
|
@@ -1695,7 +1786,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1695
1786
|
}, "@"))));
|
|
1696
1787
|
});
|
|
1697
1788
|
const previewOverlay = tabs.length && !previewHidden ? /* @__PURE__ */ h("div", {
|
|
1698
|
-
className: "artifacts-preview-overlay",
|
|
1789
|
+
className: "artifacts-preview-overlay" + (slidOut ? " artifacts-slid-out" : ""),
|
|
1699
1790
|
role: "region",
|
|
1700
1791
|
"aria-label": "文件预览"
|
|
1701
1792
|
}, /* @__PURE__ */ h("div", { className: "artifacts-preview-overlay-tabs" }, /* @__PURE__ */ h("div", { className: "artifacts-ptabs-scroll" }, tabs.map((t) => /* @__PURE__ */ h("div", {
|
|
@@ -1718,7 +1809,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1718
1809
|
onClick: () => setPreviewHidden(true)
|
|
1719
1810
|
}, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab) : null) : null;
|
|
1720
1811
|
return /* @__PURE__ */ h(Fragment, null, previewOverlay, /* @__PURE__ */ h("div", {
|
|
1721
|
-
className: "artifacts-panel" + (resizing ? " artifacts-resizing" : ""),
|
|
1812
|
+
className: "artifacts-panel" + (slidOut ? " artifacts-slid-out" : "") + (resizing ? " artifacts-resizing" : ""),
|
|
1722
1813
|
style: { width: widthPx },
|
|
1723
1814
|
role: "dialog",
|
|
1724
1815
|
"aria-label": "Artifacts"
|
|
@@ -1743,7 +1834,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1743
1834
|
title: activeView === "tree" ? "刷新文件树" : "刷新变更列表",
|
|
1744
1835
|
onClick: () => {
|
|
1745
1836
|
if (activeView === "tree") setTreeRefresh((n) => n + 1);
|
|
1746
|
-
else
|
|
1837
|
+
else {
|
|
1838
|
+
gitForceRef.current = true;
|
|
1839
|
+
setGitRefreshing(true);
|
|
1840
|
+
setGitRefresh((n) => n + 1);
|
|
1841
|
+
}
|
|
1747
1842
|
}
|
|
1748
1843
|
}, /* @__PURE__ */ h(RefreshIcon, { size: 16 })), settings.showFileTree ? /* @__PURE__ */ h("button", {
|
|
1749
1844
|
type: "button",
|
|
@@ -1752,7 +1847,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1752
1847
|
"aria-pressed": activeView === "git",
|
|
1753
1848
|
onClick: () => setActiveView(activeView === "tree" ? "git" : "tree")
|
|
1754
1849
|
}, activeView === "tree" ? /* @__PURE__ */ h(GitBranchIcon, { size: 16 }) : /* @__PURE__ */ h(FolderClosedIcon, { size: 16 })) : null), /* @__PURE__ */ h("div", { className: "artifacts-main" }, /* @__PURE__ */ h("div", {
|
|
1755
|
-
className: "artifacts-body",
|
|
1850
|
+
className: "artifacts-body" + (activeView === "git" && gitRefreshing ? " artifacts-refreshing" : ""),
|
|
1756
1851
|
style: { flex: "1 1 auto" }
|
|
1757
1852
|
}, activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h(FileTree, {
|
|
1758
1853
|
onOpen: openFile,
|
|
@@ -1762,10 +1857,9 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
|
|
|
1762
1857
|
}
|
|
1763
1858
|
function CornerButton() {
|
|
1764
1859
|
const open = useOpen();
|
|
1765
|
-
if (open) return null;
|
|
1766
1860
|
return /* @__PURE__ */ h("button", {
|
|
1767
1861
|
type: "button",
|
|
1768
|
-
className: "artifacts-corner-btn",
|
|
1862
|
+
className: "artifacts-corner-btn" + (open ? " artifacts-slid-out" : ""),
|
|
1769
1863
|
title: "弹出式侧边栏",
|
|
1770
1864
|
"aria-expanded": open,
|
|
1771
1865
|
onClick: () => store.toggle()
|
package/dist/index.js
CHANGED
|
@@ -574,12 +574,28 @@ function attachGitTracking(ctx) {
|
|
|
574
574
|
for (const cwd of Array.from(statusCache.keys())) refreshStatus(cwd, false);
|
|
575
575
|
}, 15e3);
|
|
576
576
|
}
|
|
577
|
-
async function gitStatus(ctx, sessionId) {
|
|
577
|
+
async function gitStatus(ctx, sessionId, opts) {
|
|
578
578
|
const cwd = await resolveCwdCached(ctx, sessionId);
|
|
579
579
|
if (!cwd) return {
|
|
580
580
|
ok: false,
|
|
581
581
|
error: "workspace unavailable"
|
|
582
582
|
};
|
|
583
|
+
if (opts?.force) {
|
|
584
|
+
await refreshStatus(cwd, true);
|
|
585
|
+
const fresh = statusCache.get(cwd);
|
|
586
|
+
if (!fresh) return {
|
|
587
|
+
ok: false,
|
|
588
|
+
error: "git status 失败",
|
|
589
|
+
root: cwd
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
ok: fresh.ok,
|
|
593
|
+
error: fresh.error || void 0,
|
|
594
|
+
entries: fresh.entries,
|
|
595
|
+
root: cwd,
|
|
596
|
+
cachedAt: fresh.at
|
|
597
|
+
};
|
|
598
|
+
}
|
|
583
599
|
const cached = statusCache.get(cwd);
|
|
584
600
|
if (cached) {
|
|
585
601
|
scheduleStatusRefresh(cwd);
|
|
@@ -699,7 +715,7 @@ const sharedScript = [
|
|
|
699
715
|
].map(toInlineScript).join("\n");
|
|
700
716
|
if (/<\/script/i.test(sharedScript)) throw new Error("shared inline script must not contain a literal <\/script> sequence");
|
|
701
717
|
function buildFlyoutPage() {
|
|
702
|
-
return String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>弹出式侧边栏</title>\n<script>\n (function () {\n // Follow DSH's light/dark setting: the main tab publishes the theme to\n // localStorage (THEME_KEY); the ?scheme= query is the fallback.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n })();\n<\/script>\n<style>\n :root {\n color-scheme: light;\n --p-bg: rgb(255, 255, 255);\n --p-bg-layer-1: rgb(255, 255, 255);\n --p-border-l1: rgba(0, 0, 0, 0.04);\n --p-border-l2: rgba(0, 0, 0, 0.1);\n --p-text: rgb(15, 17, 21);\n --p-text-secondary: rgb(97, 102, 107);\n --p-text-tertiary: rgb(129, 133, 140);\n --p-text-caption: rgb(173, 178, 184);\n --p-hover: rgba(38, 49, 72, 0.06);\n --p-accent: rgb(65, 118, 230);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(230, 250, 237);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(254, 245, 231);\n --p-error: rgb(236, 19, 19);\n --p-code-bg: rgb(250, 250, 250);\n --p-code-fg: rgb(97, 102, 107);\n --p-shadow: 0 4px 12px 0 rgba(0,0,0,0.02), 0 2px 8px 0 rgba(0,0,0,0.04);\n }\n :root[data-ds-dark-theme] {\n color-scheme: dark;\n --p-bg: rgb(21, 21, 23);\n --p-bg-layer-1: rgb(35, 35, 36);\n --p-border-l1: rgba(255, 255, 255, 0.06);\n --p-border-l2: rgba(255, 255, 255, 0.12);\n --p-text: rgb(249, 250, 251);\n --p-text-secondary: rgb(207, 211, 214);\n --p-text-tertiary: rgb(173, 178, 184);\n --p-text-caption: rgb(129, 133, 140);\n --p-hover: rgba(255, 255, 255, 0.08);\n --p-accent: rgb(103, 158, 254);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(35, 60, 44);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(39, 36, 31);\n --p-error: rgb(242, 90, 90);\n --p-code-bg: rgb(27, 27, 28);\n --p-code-fg: rgb(207, 211, 214);\n --p-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body {\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--p-bg); color: var(--p-text);\n display: flex; flex-direction: column;\n }\n main { flex: 1; display: flex; min-height: 0; }\n /* Content (preview) on the LEFT, file panel on the RIGHT — mirrors the\n in-app layout where the preview covers the area left of the sidebar.\n Panel width is adjustable via an invisible handle straddling the panel's\n left border (same pattern as the in-app panel, no visible gap). */\n .sidebar { width: var(--flyout-panel-w, 240px); flex: none; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--p-border-l2); position: relative; }\n .divider { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }\n .divider::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }\n .divider:hover::after, .divider.dragging::after { background: var(--p-accent); }\n body.panel-dragging iframe, body.panel-dragging embed { pointer-events: none; }\n body.panel-dragging { user-select: none; }\n .list { flex: 1; min-height: 0; overflow-y: auto; }\n .list .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .item { display: flex; align-items: stretch; border-bottom: 1px solid var(--p-border-l1); }\n /* Selected artifact: left accent bar distinguishes it from the file tree. */\n .item.active { background: var(--p-hover); box-shadow: inset 3px 0 0 var(--p-accent); }\n .item-main { flex: 1; min-width: 0; text-align: left; padding: 10px 14px; border: none; background: transparent; color: inherit; cursor: pointer; font: inherit; }\n .item-main:hover { background: var(--p-hover); }\n .item .row { display: flex; align-items: center; gap: 8px; }\n .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; flex: none; }\n .badge.create { background: var(--p-success-bg); color: var(--p-success-fg); }\n .badge.edit { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .item .base { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .item .full { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: none; font-size: 11px; color: var(--p-text-tertiary); margin-right: 2px; }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .gtoggle-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--p-text-secondary); font-size: 13px; }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\" title=\"拖动调整面板宽度\"></div>\n <div class=\"gtoggle\">\n <span class=\"gtoggle-label\" id=\"viewLabel\">文件列表</span>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n var gitFiles = null;\n var gitError = null;\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function basename(p) {\n var parts = String(p).split('/');\n return parts[parts.length - 1] || p;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U';\n return (e.y !== ' ' ? e.y : e.x) || 'M';\n }\n function gitTitle(e) {\n var label = gitLabel(e);\n var map = { U: '未跟踪', A: '新增', M: '修改', D: '删除', R: '重命名', C: '复制' };\n var staged = e.x !== ' ' && e.x !== '?';\n return (map[label] || label) + (staged ? '(已暂存)' : '(未暂存)');\n }\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', '加载变更列表…'));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', '没有未提交的变更'));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (e.origPath) row.appendChild(el('span', 'git-orig', '← ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = '复制路径';\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, '已复制路径'); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = '复制 @path 引用';\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, '已复制 @引用'); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', '没有未提交的变更(相对于 HEAD)'));\n return wrap;\n }\n var lines = String(text).replace(/\n$/, '').split('\n');\n lines.forEach(function (line) {\n var cls = 'gd-line';\n if (line.indexOf('@@') === 0) cls += ' gd-hunk';\n else if (line.charAt(0) === '+' && line.indexOf('+++') !== 0) cls += ' gd-add';\n else if (line.charAt(0) === '-' && line.indexOf('---') !== 0) cls += ' gd-del';\n else if (line.indexOf('diff ') === 0 || line.indexOf('index ') === 0 || line.indexOf('--- ') === 0 ||\n line.indexOf('+++ ') === 0 || line.indexOf('new file') === 0 || line.indexOf('deleted file') === 0 ||\n line.indexOf('old mode') === 0 || line.indexOf('new mode') === 0 || line.indexOf('rename ') === 0 ||\n line.indexOf('similarity ') === 0 || line.indexOf('copy ') === 0 || line.indexOf('Binary files') === 0 ||\n line.charAt(0) === '\\') cls += ' gd-meta';\n wrap.appendChild(el('div', cls, line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? '[diff] ' : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = '关闭标签页';\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\n$/, '').split('\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? '点击右侧变更文件查看 diff' : '点击右侧文件查看内容'));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', '加载中…')); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || '读取失败')); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path);\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode('图片加载失败')); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', '(truncated preview)'));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path)).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n var label = document.getElementById('viewLabel');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? '查看 Git 变更(未提交)' : '返回文件列表';\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? '刷新文件树' : '刷新变更列表';\n label.textContent = view === 'tree' ? '文件列表' : 'Git 变更(未提交)';\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries) {\n treeRoot = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', '加载文件树…'));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry briefly so the tree\n // self-corrects instead of sitting on an error/empty state.\n var left = typeof retries === 'number' ? retries : 3;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1); }, 400);\n return;\n } else {\n treeRoot = { path: null, entries: [] };\n }\n renderTree();\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1); }, 400); return; }\n treeRoot = { path: null, entries: [] };\n renderTree();\n document.getElementById('treeBody').appendChild(el('div', 'tree-error', '加载失败'));\n });\n }\n\n function renderTree() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) return;\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', '(空目录)'));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, '已复制 @引用');\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', '@引用');\n refBtn.type = 'button';\n refBtn.title = '复制 @path 引用';\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', '加载中…');\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || '读取失败' };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: '读取失败' };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n if (currentView === 'tree') loadTreeRoot();\n else loadGit();\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n function loadGit() {\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n var st = document.getElementById('status');\n if (data && data.ok === true) {\n gitFiles = Array.isArray(data.entries) ? data.entries : [];\n gitError = null;\n st.textContent = 'live';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-success-fg').trim() || '#34c55e';\n } else {\n gitFiles = [];\n gitError = (data && data.error) || 'git status 失败';\n st.textContent = 'git error';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n }\n renderGit();\n }).catch(function () {\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n });\n }\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the right (default) or the left ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = false;\n try { panelLeft = localStorage.getItem(PANEL_SIDE_KEY) === '1'; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? '将文件面板移到右侧' : '将文件面板移到左侧';\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n }\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>弹出式侧边栏</title>\n<script>\n (function () {\n // Follow DSH's light/dark setting: the main tab publishes the theme to\n // localStorage (THEME_KEY); the ?scheme= query is the fallback.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n })();\n<\/script>\n<style>\n :root {\n color-scheme: light;\n --p-bg: rgb(255, 255, 255);\n --p-bg-layer-1: rgb(255, 255, 255);\n --p-border-l1: rgba(0, 0, 0, 0.04);\n --p-border-l2: rgba(0, 0, 0, 0.1);\n --p-text: rgb(15, 17, 21);\n --p-text-secondary: rgb(97, 102, 107);\n --p-text-tertiary: rgb(129, 133, 140);\n --p-text-caption: rgb(173, 178, 184);\n --p-hover: rgba(38, 49, 72, 0.06);\n --p-accent: rgb(65, 118, 230);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(230, 250, 237);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(254, 245, 231);\n --p-error: rgb(236, 19, 19);\n --p-code-bg: rgb(250, 250, 250);\n --p-code-fg: rgb(97, 102, 107);\n --p-shadow: 0 4px 12px 0 rgba(0,0,0,0.02), 0 2px 8px 0 rgba(0,0,0,0.04);\n }\n :root[data-ds-dark-theme] {\n color-scheme: dark;\n --p-bg: rgb(21, 21, 23);\n --p-bg-layer-1: rgb(35, 35, 36);\n --p-border-l1: rgba(255, 255, 255, 0.06);\n --p-border-l2: rgba(255, 255, 255, 0.12);\n --p-text: rgb(249, 250, 251);\n --p-text-secondary: rgb(207, 211, 214);\n --p-text-tertiary: rgb(173, 178, 184);\n --p-text-caption: rgb(129, 133, 140);\n --p-hover: rgba(255, 255, 255, 0.08);\n --p-accent: rgb(103, 158, 254);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(35, 60, 44);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(39, 36, 31);\n --p-error: rgb(242, 90, 90);\n --p-code-bg: rgb(27, 27, 28);\n --p-code-fg: rgb(207, 211, 214);\n --p-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body {\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--p-bg); color: var(--p-text);\n display: flex; flex-direction: column;\n }\n main { flex: 1; display: flex; min-height: 0; }\n /* Content (preview) on the LEFT, file panel on the RIGHT — mirrors the\n in-app layout where the preview covers the area left of the sidebar.\n Panel width is adjustable via an invisible handle straddling the panel's\n left border (same pattern as the in-app panel, no visible gap). */\n .sidebar { width: var(--flyout-panel-w, 240px); flex: none; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--p-border-l2); position: relative; }\n .divider { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }\n .divider::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }\n .divider:hover::after, .divider.dragging::after { background: var(--p-accent); }\n body.panel-dragging iframe, body.panel-dragging embed { pointer-events: none; }\n body.panel-dragging { user-select: none; }\n .list { flex: 1; min-height: 0; overflow-y: auto; }\n .list .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .item { display: flex; align-items: stretch; border-bottom: 1px solid var(--p-border-l1); }\n /* Selected artifact: left accent bar distinguishes it from the file tree. */\n .item.active { background: var(--p-hover); box-shadow: inset 3px 0 0 var(--p-accent); }\n .item-main { flex: 1; min-width: 0; text-align: left; padding: 10px 14px; border: none; background: transparent; color: inherit; cursor: pointer; font: inherit; }\n .item-main:hover { background: var(--p-hover); }\n .item .row { display: flex; align-items: center; gap: 8px; }\n .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; flex: none; }\n .badge.create { background: var(--p-success-bg); color: var(--p-success-fg); }\n .badge.edit { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .item .base { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .item .full { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: none; font-size: 11px; color: var(--p-text-tertiary); margin-right: 2px; }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .gtoggle-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--p-text-secondary); font-size: 13px; }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\" title=\"拖动调整面板宽度\"></div>\n <div class=\"gtoggle\">\n <span class=\"gtoggle-label\" id=\"viewLabel\">文件列表</span>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n var gitFiles = null;\n var gitError = null;\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function basename(p) {\n var parts = String(p).split('/');\n return parts[parts.length - 1] || p;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U';\n return (e.y !== ' ' ? e.y : e.x) || 'M';\n }\n function gitTitle(e) {\n var label = gitLabel(e);\n var map = { U: '未跟踪', A: '新增', M: '修改', D: '删除', R: '重命名', C: '复制' };\n var staged = e.x !== ' ' && e.x !== '?';\n return (map[label] || label) + (staged ? '(已暂存)' : '(未暂存)');\n }\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', '加载变更列表…'));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', '没有未提交的变更'));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (e.origPath) row.appendChild(el('span', 'git-orig', '← ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = '复制路径';\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, '已复制路径'); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = '复制 @path 引用';\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, '已复制 @引用'); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', '没有未提交的变更(相对于 HEAD)'));\n return wrap;\n }\n var lines = String(text).replace(/\\n$/, '').split('\\n');\n lines.forEach(function (line) {\n var cls = 'gd-line';\n if (line.indexOf('@@') === 0) cls += ' gd-hunk';\n else if (line.charAt(0) === '+' && line.indexOf('+++') !== 0) cls += ' gd-add';\n else if (line.charAt(0) === '-' && line.indexOf('---') !== 0) cls += ' gd-del';\n else if (line.indexOf('diff ') === 0 || line.indexOf('index ') === 0 || line.indexOf('--- ') === 0 ||\n line.indexOf('+++ ') === 0 || line.indexOf('new file') === 0 || line.indexOf('deleted file') === 0 ||\n line.indexOf('old mode') === 0 || line.indexOf('new mode') === 0 || line.indexOf('rename ') === 0 ||\n line.indexOf('similarity ') === 0 || line.indexOf('copy ') === 0 || line.indexOf('Binary files') === 0 ||\n line.charAt(0) === '\\\\') cls += ' gd-meta';\n wrap.appendChild(el('div', cls, line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? '[diff] ' : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = '关闭标签页';\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\\n$/, '').split('\\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? '点击右侧变更文件查看 diff' : '点击右侧文件查看内容'));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', '加载中…')); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || '读取失败')); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path);\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode('图片加载失败')); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', '(truncated preview)'));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path)).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n var label = document.getElementById('viewLabel');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? '查看 Git 变更(未提交)' : '返回文件列表';\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? '刷新文件树' : '刷新变更列表';\n label.textContent = view === 'tree' ? '文件列表' : 'Git 变更(未提交)';\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries) {\n treeRoot = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', '加载文件树…'));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry briefly so the tree\n // self-corrects instead of sitting on an error/empty state.\n var left = typeof retries === 'number' ? retries : 3;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1); }, 400);\n return;\n } else {\n treeRoot = { path: null, entries: [] };\n }\n renderTree();\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1); }, 400); return; }\n treeRoot = { path: null, entries: [] };\n renderTree();\n document.getElementById('treeBody').appendChild(el('div', 'tree-error', '加载失败'));\n });\n }\n\n function renderTree() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) return;\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', '(空目录)'));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, '已复制 @引用');\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', '@引用');\n refBtn.type = 'button';\n refBtn.title = '复制 @path 引用';\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', '加载中…');\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || '读取失败' };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: '读取失败' };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n if (currentView === 'tree') loadTreeRoot();\n else loadGit();\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n function loadGit() {\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n var st = document.getElementById('status');\n if (data && data.ok === true) {\n gitFiles = Array.isArray(data.entries) ? data.entries : [];\n gitError = null;\n st.textContent = 'live';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-success-fg').trim() || '#34c55e';\n } else {\n gitFiles = [];\n gitError = (data && data.error) || 'git status 失败';\n st.textContent = 'git error';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n }\n renderGit();\n }).catch(function () {\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n });\n }\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the right (default) or the left ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = false;\n try { panelLeft = localStorage.getItem(PANEL_SIDE_KEY) === '1'; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? '将文件面板移到右侧' : '将文件面板移到左侧';\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n }\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"])), sharedScript);
|
|
718
|
+
return String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>弹出式侧边栏</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2250.000000%22%20height%3D%2250.000000%22%20viewBox%3D%220%200%2050%2050%22%20fill%3D%22none%22%3E%0A%09%3ClinearGradient%20id%3D%22wg%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%232a7fbf%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231b9a6b%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20id%3D%22path%22%20d%3D%22M48.8354%2010.0479C48.3232%209.79199%2048.1025%2010.2798%2047.8032%2010.5278C47.7007%2010.6079%2047.6143%2010.7119%2047.5273%2010.8076C46.7793%2011.624%2045.9048%2012.1597%2044.7622%2012.0957C43.0923%2012%2041.666%2012.5356%2040.4058%2013.8398C40.1377%2012.2319%2039.2476%2011.272%2037.8926%2010.6558C37.1836%2010.3359%2036.4668%2010.0156%2035.9702%209.31982C35.6235%208.82373%2035.5293%208.27197%2035.356%207.72754C35.2456%207.3999%2035.1353%207.06396%2034.7651%207.00781C34.3633%206.94385%2034.2056%207.2876%2034.0479%207.57568C33.418%208.75195%2033.1733%2010.0479%2033.1973%2011.3599C33.2524%2014.312%2034.4736%2016.6641%2036.8999%2018.3359C37.1758%2018.5278%2037.2466%2018.7197%2037.1597%2019C36.9946%2019.5757%2036.7974%2020.1357%2036.624%2020.7119C36.5137%2021.0801%2036.3486%2021.1597%2035.9624%2021C34.6309%2020.4321%2033.481%2019.5918%2032.4644%2018.5757C30.7393%2016.8721%2029.1792%2014.9917%2027.2334%2013.52C26.7764%2013.1758%2026.3193%2012.856%2025.8467%2012.5518C23.8618%2010.584%2026.1069%208.96777%2026.627%208.77588C27.1704%208.57568%2026.8159%207.8877%2025.0591%207.896C23.3022%207.90381%2021.6953%208.50391%2019.647%209.30371C19.3477%209.42383%2019.0322%209.51172%2018.7095%209.58398C16.8501%209.22363%2014.9199%209.14355%2012.9033%209.37598C9.10596%209.80762%206.07275%2011.6396%203.84326%2014.7681C1.16455%2018.5278%200.53418%2022.7998%201.30664%2027.2559C2.11768%2031.9521%204.46582%2035.8398%208.07373%2038.8799C11.8159%2042.0322%2016.1255%2043.5762%2021.041%2043.2803C24.0269%2043.104%2027.3516%2042.6963%2031.1016%2039.4561C32.0469%2039.936%2033.0396%2040.1279%2034.686%2040.272C35.9546%2040.3921%2037.1758%2040.208%2038.1211%2040.0078C39.6021%2039.688%2039.4995%2038.2881%2038.9639%2038.0322C34.623%2035.9678%2035.5762%2036.8081%2034.71%2036.1279C36.9155%2033.4639%2040.2402%2030.6958%2041.54%2021.728C41.6426%2021.0161%2041.5557%2020.5679%2041.54%2019.9917C41.5322%2019.6396%2041.6108%2019.5039%2042.0049%2019.4639C43.0923%2019.3359%2044.1479%2019.0317%2045.1167%2018.4878C47.9292%2016.9199%2049.064%2014.3438%2049.3315%2011.2559C49.3711%2010.7837%2049.3237%2010.2959%2048.8354%2010.0479ZM24.3262%2037.8398C20.1196%2034.4639%2018.0791%2033.3521%2017.2358%2033.3999C16.4482%2033.4482%2016.5898%2034.3682%2016.7632%2034.9678C16.9443%2035.5601%2017.1812%2035.9683%2017.5117%2036.4878C17.7402%2036.832%2017.8979%2037.3442%2017.2832%2037.728C15.9282%2038.584%2013.5728%2037.4399%2013.4624%2037.3838C10.7207%2035.7358%208.42822%2033.5601%206.81348%2030.584C5.25342%2027.7197%204.34766%2024.6479%204.19775%2021.3677C4.1582%2020.5757%204.38672%2020.2959%205.15869%2020.1519C6.17529%2019.96%207.22314%2019.9199%208.23926%2020.0718C12.5327%2020.7119%2016.1885%2022.6719%2019.2529%2025.7759C21.002%2027.5439%2022.3252%2029.6558%2023.6885%2031.7202C25.1377%2033.9121%2026.6978%2036%2028.6831%2037.7119C29.3843%2038.312%2029.9434%2038.7681%2030.479%2039.104C28.8643%2039.2881%2026.1699%2039.3281%2024.3262%2037.8398ZM26.3433%2024.6001C26.3433%2024.248%2026.6191%2023.9678%2026.9658%2023.9678C27.0444%2023.9678%2027.1152%2023.9839%2027.1782%2024.0078C27.2651%2024.04%2027.3438%2024.0879%2027.4067%2024.1602C27.5171%2024.272%2027.5801%2024.4321%2027.5801%2024.6001C27.5801%2024.9521%2027.3042%2025.2319%2026.9575%2025.2319C26.6108%2025.2319%2026.3433%2024.9521%2026.3433%2024.6001ZM32.6064%2027.8799C32.2046%2028.0479%2031.8027%2028.1919%2031.4165%2028.208C30.8179%2028.2397%2030.1641%2027.9922%2029.8096%2027.688C29.2583%2027.2158%2028.8643%2026.9521%2028.6987%2026.1279C28.6279%2025.7759%2028.6675%2025.2319%2028.7305%2024.9199C28.8721%2024.248%2028.7144%2023.8159%2028.2495%2023.4238C27.8716%2023.104%2027.3911%2023.0161%2026.8633%2023.0161C26.666%2023.0161%2026.4849%2022.9277%2026.3511%2022.856C26.1304%2022.7441%2025.9492%2022.4639%2026.1226%2022.1201C26.1777%2022.0078%2026.4458%2021.7358%2026.5088%2021.688C27.2256%2021.272%2028.0527%2021.4077%2028.8169%2021.7197C29.5259%2022.0161%2030.0615%2022.5601%2030.834%2023.3281C31.6216%2024.2559%2031.7632%2024.5117%2032.2124%2025.208C32.5669%2025.752%2032.8901%2026.312%2033.1104%2026.9521C33.2446%2027.3521%2033.0713%2027.6802%2032.6064%2027.8799Z%22%20fill%3D%22url(%23wg)%22%20fill-opacity%3D%221.000000%22%20fill-rule%3D%22nonzero%22%2F%3E%0A%3C%2Fsvg%3E%0A\" />\n<script>\n (function () {\n // Follow DSH's light/dark setting: the main tab publishes the theme to\n // localStorage (THEME_KEY); the ?scheme= query is the fallback.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n })();\n<\/script>\n<style>\n :root {\n color-scheme: light;\n --p-bg: rgb(255, 255, 255);\n --p-bg-layer-1: rgb(255, 255, 255);\n --p-border-l1: rgba(0, 0, 0, 0.04);\n --p-border-l2: rgba(0, 0, 0, 0.1);\n --p-text: rgb(15, 17, 21);\n --p-text-secondary: rgb(97, 102, 107);\n --p-text-tertiary: rgb(129, 133, 140);\n --p-text-caption: rgb(173, 178, 184);\n --p-hover: rgba(38, 49, 72, 0.06);\n --p-accent: rgb(65, 118, 230);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(230, 250, 237);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(254, 245, 231);\n --p-error: rgb(236, 19, 19);\n --p-code-bg: rgb(250, 250, 250);\n --p-code-fg: rgb(97, 102, 107);\n --p-shadow: 0 4px 12px 0 rgba(0,0,0,0.02), 0 2px 8px 0 rgba(0,0,0,0.04);\n }\n :root[data-ds-dark-theme] {\n color-scheme: dark;\n --p-bg: rgb(21, 21, 23);\n --p-bg-layer-1: rgb(35, 35, 36);\n --p-border-l1: rgba(255, 255, 255, 0.06);\n --p-border-l2: rgba(255, 255, 255, 0.12);\n --p-text: rgb(249, 250, 251);\n --p-text-secondary: rgb(207, 211, 214);\n --p-text-tertiary: rgb(173, 178, 184);\n --p-text-caption: rgb(129, 133, 140);\n --p-hover: rgba(255, 255, 255, 0.08);\n --p-accent: rgb(103, 158, 254);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(35, 60, 44);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(39, 36, 31);\n --p-error: rgb(242, 90, 90);\n --p-code-bg: rgb(27, 27, 28);\n --p-code-fg: rgb(207, 211, 214);\n --p-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body {\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--p-bg); color: var(--p-text);\n display: flex; flex-direction: column;\n }\n main { flex: 1; display: flex; min-height: 0; }\n /* Content (preview) on the LEFT, file panel on the RIGHT — mirrors the\n in-app layout where the preview covers the area left of the sidebar.\n Panel width is adjustable via an invisible handle straddling the panel's\n left border (same pattern as the in-app panel, no visible gap). */\n .sidebar { width: var(--flyout-panel-w, 240px); flex: none; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--p-border-l2); position: relative; }\n .divider { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }\n .divider::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }\n .divider:hover::after, .divider.dragging::after { background: var(--p-accent); }\n body.panel-dragging iframe, body.panel-dragging embed { pointer-events: none; }\n body.panel-dragging { user-select: none; }\n .list { flex: 1; min-height: 0; overflow-y: auto; transition: opacity .15s; }\n .list.is-refreshing { opacity: .45; }\n /* 刷新后逐行浮现:延迟由 JS 按行号注入 */\n .item.flash-in, .tree-row.flash-in { animation: flash-in .3s ease both; }\n @keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } }\n .list .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .item { display: flex; align-items: stretch; border-bottom: 1px solid var(--p-border-l1); }\n /* Selected artifact: left accent bar distinguishes it from the file tree. */\n .item.active { background: var(--p-hover); box-shadow: inset 3px 0 0 var(--p-accent); }\n .item-main { flex: 1; min-width: 0; text-align: left; padding: 10px 14px; border: none; background: transparent; color: inherit; cursor: pointer; font: inherit; }\n .item-main:hover { background: var(--p-hover); }\n .item .row { display: flex; align-items: center; gap: 8px; }\n .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; flex: none; }\n .badge.create { background: var(--p-success-bg); color: var(--p-success-fg); }\n .badge.edit { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .item .base { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .item .full { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--p-text-tertiary); }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\" title=\"拖动调整面板宽度\"></div>\n <div class=\"gtoggle\">\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n var gitFiles = null;\n var gitError = null;\n var gitSig = null; // 上次渲染的变更签名;轮询数据未变时跳过重渲染,避免冲掉刷新动画\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function basename(p) {\n var parts = String(p).split('/');\n return parts[parts.length - 1] || p;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U';\n return (e.y !== ' ' ? e.y : e.x) || 'M';\n }\n function gitTitle(e) {\n var label = gitLabel(e);\n var map = { U: '未跟踪', A: '新增', M: '修改', D: '删除', R: '重命名', C: '复制' };\n var staged = e.x !== ' ' && e.x !== '?';\n return (map[label] || label) + (staged ? '(已暂存)' : '(未暂存)');\n }\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', '加载变更列表…'));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', '没有未提交的变更'));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (e.origPath) row.appendChild(el('span', 'git-orig', '← ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = '复制路径';\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, '已复制路径'); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = '复制 @path 引用';\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, '已复制 @引用'); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', '没有未提交的变更(相对于 HEAD)'));\n return wrap;\n }\n var lines = String(text).replace(/\n$/, '').split('\n');\n lines.forEach(function (line) {\n var cls = 'gd-line';\n if (line.indexOf('@@') === 0) cls += ' gd-hunk';\n else if (line.charAt(0) === '+' && line.indexOf('+++') !== 0) cls += ' gd-add';\n else if (line.charAt(0) === '-' && line.indexOf('---') !== 0) cls += ' gd-del';\n else if (line.indexOf('diff ') === 0 || line.indexOf('index ') === 0 || line.indexOf('--- ') === 0 ||\n line.indexOf('+++ ') === 0 || line.indexOf('new file') === 0 || line.indexOf('deleted file') === 0 ||\n line.indexOf('old mode') === 0 || line.indexOf('new mode') === 0 || line.indexOf('rename ') === 0 ||\n line.indexOf('similarity ') === 0 || line.indexOf('copy ') === 0 || line.indexOf('Binary files') === 0 ||\n line.charAt(0) === '\\') cls += ' gd-meta';\n wrap.appendChild(el('div', cls, line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? '[diff] ' : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = '关闭标签页';\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\n$/, '').split('\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? '点击右侧变更文件查看 diff' : '点击右侧文件查看内容'));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', '加载中…')); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || '读取失败')); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path);\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode('图片加载失败')); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', '(truncated preview)'));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path)).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? '查看 Git 变更(未提交)' : '返回文件列表';\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? '刷新文件树' : '刷新变更列表';\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries) {\n treeRoot = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', '加载文件树…'));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry briefly so the tree\n // self-corrects instead of sitting on an error/empty state.\n var left = typeof retries === 'number' ? retries : 3;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1); }, 400);\n return;\n } else {\n treeRoot = { path: null, entries: [] };\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1); }, 400); return; }\n treeRoot = { path: null, entries: [] };\n renderTree();\n document.getElementById('treeBody').appendChild(el('div', 'tree-error', '加载失败'));\n });\n }\n\n function renderTree() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) return;\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', '(空目录)'));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, '已复制 @引用');\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', '@引用');\n refBtn.type = 'button';\n refBtn.title = '复制 @path 引用';\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', '加载中…');\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || '读取失败' };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: '读取失败' };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n if (currentView === 'tree') loadTreeRoot();\n else loadGit(true);\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n // force=1 绕过 host 的 stale-while-revalidate 缓存,由刷新按钮使用。\n // 刷新后给列表行加交错浮现动画(从上往下);selector 区分 git 列表与文件树。\n function staggerList(list, selector) {\n var items = list.querySelectorAll(selector || '.item');\n for (var i = 0; i < items.length; i++) {\n var it = items[i];\n it.classList.remove('flash-in');\n void it.offsetWidth; // 重置动画,使连续刷新也能重放\n it.classList.add('flash-in');\n it.style.animationDelay = (Math.min(i, 12) * 45) + 'ms';\n }\n }\n function loadGit(force) {\n var list = document.getElementById('list');\n // 强制刷新时列表短暂变暗,响应返回后恢复,给出「刷新过了」的可见反馈。\n if (force && list) list.classList.add('is-refreshing');\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || '') + (force ? '&force=1' : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n var ok = data && data.ok === true;\n if (ok) {\n st.textContent = 'live';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-success-fg').trim() || '#34c55e';\n } else {\n st.textContent = 'git error';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || 'git status 失败');\n var sig = JSON.stringify([nextError, nextFiles]);\n if (!force && sig === gitSig) return;\n gitSig = sig;\n gitFiles = nextFiles;\n gitError = nextError;\n renderGit();\n if (force && list) staggerList(list);\n }).catch(function () {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n });\n }\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the left (default) or the right ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = true;\n try { var _savedSide = localStorage.getItem(PANEL_SIDE_KEY); if (_savedSide === '0') panelLeft = false; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? '将文件面板移到右侧' : '将文件面板移到左侧';\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n }\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>弹出式侧边栏</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2250.000000%22%20height%3D%2250.000000%22%20viewBox%3D%220%200%2050%2050%22%20fill%3D%22none%22%3E%0A%09%3ClinearGradient%20id%3D%22wg%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%232a7fbf%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231b9a6b%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20id%3D%22path%22%20d%3D%22M48.8354%2010.0479C48.3232%209.79199%2048.1025%2010.2798%2047.8032%2010.5278C47.7007%2010.6079%2047.6143%2010.7119%2047.5273%2010.8076C46.7793%2011.624%2045.9048%2012.1597%2044.7622%2012.0957C43.0923%2012%2041.666%2012.5356%2040.4058%2013.8398C40.1377%2012.2319%2039.2476%2011.272%2037.8926%2010.6558C37.1836%2010.3359%2036.4668%2010.0156%2035.9702%209.31982C35.6235%208.82373%2035.5293%208.27197%2035.356%207.72754C35.2456%207.3999%2035.1353%207.06396%2034.7651%207.00781C34.3633%206.94385%2034.2056%207.2876%2034.0479%207.57568C33.418%208.75195%2033.1733%2010.0479%2033.1973%2011.3599C33.2524%2014.312%2034.4736%2016.6641%2036.8999%2018.3359C37.1758%2018.5278%2037.2466%2018.7197%2037.1597%2019C36.9946%2019.5757%2036.7974%2020.1357%2036.624%2020.7119C36.5137%2021.0801%2036.3486%2021.1597%2035.9624%2021C34.6309%2020.4321%2033.481%2019.5918%2032.4644%2018.5757C30.7393%2016.8721%2029.1792%2014.9917%2027.2334%2013.52C26.7764%2013.1758%2026.3193%2012.856%2025.8467%2012.5518C23.8618%2010.584%2026.1069%208.96777%2026.627%208.77588C27.1704%208.57568%2026.8159%207.8877%2025.0591%207.896C23.3022%207.90381%2021.6953%208.50391%2019.647%209.30371C19.3477%209.42383%2019.0322%209.51172%2018.7095%209.58398C16.8501%209.22363%2014.9199%209.14355%2012.9033%209.37598C9.10596%209.80762%206.07275%2011.6396%203.84326%2014.7681C1.16455%2018.5278%200.53418%2022.7998%201.30664%2027.2559C2.11768%2031.9521%204.46582%2035.8398%208.07373%2038.8799C11.8159%2042.0322%2016.1255%2043.5762%2021.041%2043.2803C24.0269%2043.104%2027.3516%2042.6963%2031.1016%2039.4561C32.0469%2039.936%2033.0396%2040.1279%2034.686%2040.272C35.9546%2040.3921%2037.1758%2040.208%2038.1211%2040.0078C39.6021%2039.688%2039.4995%2038.2881%2038.9639%2038.0322C34.623%2035.9678%2035.5762%2036.8081%2034.71%2036.1279C36.9155%2033.4639%2040.2402%2030.6958%2041.54%2021.728C41.6426%2021.0161%2041.5557%2020.5679%2041.54%2019.9917C41.5322%2019.6396%2041.6108%2019.5039%2042.0049%2019.4639C43.0923%2019.3359%2044.1479%2019.0317%2045.1167%2018.4878C47.9292%2016.9199%2049.064%2014.3438%2049.3315%2011.2559C49.3711%2010.7837%2049.3237%2010.2959%2048.8354%2010.0479ZM24.3262%2037.8398C20.1196%2034.4639%2018.0791%2033.3521%2017.2358%2033.3999C16.4482%2033.4482%2016.5898%2034.3682%2016.7632%2034.9678C16.9443%2035.5601%2017.1812%2035.9683%2017.5117%2036.4878C17.7402%2036.832%2017.8979%2037.3442%2017.2832%2037.728C15.9282%2038.584%2013.5728%2037.4399%2013.4624%2037.3838C10.7207%2035.7358%208.42822%2033.5601%206.81348%2030.584C5.25342%2027.7197%204.34766%2024.6479%204.19775%2021.3677C4.1582%2020.5757%204.38672%2020.2959%205.15869%2020.1519C6.17529%2019.96%207.22314%2019.9199%208.23926%2020.0718C12.5327%2020.7119%2016.1885%2022.6719%2019.2529%2025.7759C21.002%2027.5439%2022.3252%2029.6558%2023.6885%2031.7202C25.1377%2033.9121%2026.6978%2036%2028.6831%2037.7119C29.3843%2038.312%2029.9434%2038.7681%2030.479%2039.104C28.8643%2039.2881%2026.1699%2039.3281%2024.3262%2037.8398ZM26.3433%2024.6001C26.3433%2024.248%2026.6191%2023.9678%2026.9658%2023.9678C27.0444%2023.9678%2027.1152%2023.9839%2027.1782%2024.0078C27.2651%2024.04%2027.3438%2024.0879%2027.4067%2024.1602C27.5171%2024.272%2027.5801%2024.4321%2027.5801%2024.6001C27.5801%2024.9521%2027.3042%2025.2319%2026.9575%2025.2319C26.6108%2025.2319%2026.3433%2024.9521%2026.3433%2024.6001ZM32.6064%2027.8799C32.2046%2028.0479%2031.8027%2028.1919%2031.4165%2028.208C30.8179%2028.2397%2030.1641%2027.9922%2029.8096%2027.688C29.2583%2027.2158%2028.8643%2026.9521%2028.6987%2026.1279C28.6279%2025.7759%2028.6675%2025.2319%2028.7305%2024.9199C28.8721%2024.248%2028.7144%2023.8159%2028.2495%2023.4238C27.8716%2023.104%2027.3911%2023.0161%2026.8633%2023.0161C26.666%2023.0161%2026.4849%2022.9277%2026.3511%2022.856C26.1304%2022.7441%2025.9492%2022.4639%2026.1226%2022.1201C26.1777%2022.0078%2026.4458%2021.7358%2026.5088%2021.688C27.2256%2021.272%2028.0527%2021.4077%2028.8169%2021.7197C29.5259%2022.0161%2030.0615%2022.5601%2030.834%2023.3281C31.6216%2024.2559%2031.7632%2024.5117%2032.2124%2025.208C32.5669%2025.752%2032.8901%2026.312%2033.1104%2026.9521C33.2446%2027.3521%2033.0713%2027.6802%2032.6064%2027.8799Z%22%20fill%3D%22url(%23wg)%22%20fill-opacity%3D%221.000000%22%20fill-rule%3D%22nonzero%22%2F%3E%0A%3C%2Fsvg%3E%0A\" />\n<script>\n (function () {\n // Follow DSH's light/dark setting: the main tab publishes the theme to\n // localStorage (THEME_KEY); the ?scheme= query is the fallback.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n })();\n<\/script>\n<style>\n :root {\n color-scheme: light;\n --p-bg: rgb(255, 255, 255);\n --p-bg-layer-1: rgb(255, 255, 255);\n --p-border-l1: rgba(0, 0, 0, 0.04);\n --p-border-l2: rgba(0, 0, 0, 0.1);\n --p-text: rgb(15, 17, 21);\n --p-text-secondary: rgb(97, 102, 107);\n --p-text-tertiary: rgb(129, 133, 140);\n --p-text-caption: rgb(173, 178, 184);\n --p-hover: rgba(38, 49, 72, 0.06);\n --p-accent: rgb(65, 118, 230);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(230, 250, 237);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(254, 245, 231);\n --p-error: rgb(236, 19, 19);\n --p-code-bg: rgb(250, 250, 250);\n --p-code-fg: rgb(97, 102, 107);\n --p-shadow: 0 4px 12px 0 rgba(0,0,0,0.02), 0 2px 8px 0 rgba(0,0,0,0.04);\n }\n :root[data-ds-dark-theme] {\n color-scheme: dark;\n --p-bg: rgb(21, 21, 23);\n --p-bg-layer-1: rgb(35, 35, 36);\n --p-border-l1: rgba(255, 255, 255, 0.06);\n --p-border-l2: rgba(255, 255, 255, 0.12);\n --p-text: rgb(249, 250, 251);\n --p-text-secondary: rgb(207, 211, 214);\n --p-text-tertiary: rgb(173, 178, 184);\n --p-text-caption: rgb(129, 133, 140);\n --p-hover: rgba(255, 255, 255, 0.08);\n --p-accent: rgb(103, 158, 254);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(35, 60, 44);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(39, 36, 31);\n --p-error: rgb(242, 90, 90);\n --p-code-bg: rgb(27, 27, 28);\n --p-code-fg: rgb(207, 211, 214);\n --p-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body {\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--p-bg); color: var(--p-text);\n display: flex; flex-direction: column;\n }\n main { flex: 1; display: flex; min-height: 0; }\n /* Content (preview) on the LEFT, file panel on the RIGHT — mirrors the\n in-app layout where the preview covers the area left of the sidebar.\n Panel width is adjustable via an invisible handle straddling the panel's\n left border (same pattern as the in-app panel, no visible gap). */\n .sidebar { width: var(--flyout-panel-w, 240px); flex: none; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--p-border-l2); position: relative; }\n .divider { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }\n .divider::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }\n .divider:hover::after, .divider.dragging::after { background: var(--p-accent); }\n body.panel-dragging iframe, body.panel-dragging embed { pointer-events: none; }\n body.panel-dragging { user-select: none; }\n .list { flex: 1; min-height: 0; overflow-y: auto; transition: opacity .15s; }\n .list.is-refreshing { opacity: .45; }\n /* 刷新后逐行浮现:延迟由 JS 按行号注入 */\n .item.flash-in, .tree-row.flash-in { animation: flash-in .3s ease both; }\n @keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } }\n .list .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .item { display: flex; align-items: stretch; border-bottom: 1px solid var(--p-border-l1); }\n /* Selected artifact: left accent bar distinguishes it from the file tree. */\n .item.active { background: var(--p-hover); box-shadow: inset 3px 0 0 var(--p-accent); }\n .item-main { flex: 1; min-width: 0; text-align: left; padding: 10px 14px; border: none; background: transparent; color: inherit; cursor: pointer; font: inherit; }\n .item-main:hover { background: var(--p-hover); }\n .item .row { display: flex; align-items: center; gap: 8px; }\n .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; flex: none; }\n .badge.create { background: var(--p-success-bg); color: var(--p-success-fg); }\n .badge.edit { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .item .base { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .item .full { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--p-text-tertiary); }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\" title=\"拖动调整面板宽度\"></div>\n <div class=\"gtoggle\">\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n var gitFiles = null;\n var gitError = null;\n var gitSig = null; // 上次渲染的变更签名;轮询数据未变时跳过重渲染,避免冲掉刷新动画\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function basename(p) {\n var parts = String(p).split('/');\n return parts[parts.length - 1] || p;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U';\n return (e.y !== ' ' ? e.y : e.x) || 'M';\n }\n function gitTitle(e) {\n var label = gitLabel(e);\n var map = { U: '未跟踪', A: '新增', M: '修改', D: '删除', R: '重命名', C: '复制' };\n var staged = e.x !== ' ' && e.x !== '?';\n return (map[label] || label) + (staged ? '(已暂存)' : '(未暂存)');\n }\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', '加载变更列表…'));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', '没有未提交的变更'));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (e.origPath) row.appendChild(el('span', 'git-orig', '← ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = '复制路径';\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, '已复制路径'); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = '复制 @path 引用';\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, '已复制 @引用'); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', '没有未提交的变更(相对于 HEAD)'));\n return wrap;\n }\n var lines = String(text).replace(/\\n$/, '').split('\\n');\n lines.forEach(function (line) {\n var cls = 'gd-line';\n if (line.indexOf('@@') === 0) cls += ' gd-hunk';\n else if (line.charAt(0) === '+' && line.indexOf('+++') !== 0) cls += ' gd-add';\n else if (line.charAt(0) === '-' && line.indexOf('---') !== 0) cls += ' gd-del';\n else if (line.indexOf('diff ') === 0 || line.indexOf('index ') === 0 || line.indexOf('--- ') === 0 ||\n line.indexOf('+++ ') === 0 || line.indexOf('new file') === 0 || line.indexOf('deleted file') === 0 ||\n line.indexOf('old mode') === 0 || line.indexOf('new mode') === 0 || line.indexOf('rename ') === 0 ||\n line.indexOf('similarity ') === 0 || line.indexOf('copy ') === 0 || line.indexOf('Binary files') === 0 ||\n line.charAt(0) === '\\\\') cls += ' gd-meta';\n wrap.appendChild(el('div', cls, line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? '[diff] ' : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = '关闭标签页';\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\\n$/, '').split('\\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? '点击右侧变更文件查看 diff' : '点击右侧文件查看内容'));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', '加载中…')); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || '读取失败')); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path);\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode('图片加载失败')); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', '(truncated preview)'));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path)).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || '读取失败' }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? '查看 Git 变更(未提交)' : '返回文件列表';\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? '刷新文件树' : '刷新变更列表';\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries) {\n treeRoot = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', '加载文件树…'));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry briefly so the tree\n // self-corrects instead of sitting on an error/empty state.\n var left = typeof retries === 'number' ? retries : 3;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1); }, 400);\n return;\n } else {\n treeRoot = { path: null, entries: [] };\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1); }, 400); return; }\n treeRoot = { path: null, entries: [] };\n renderTree();\n document.getElementById('treeBody').appendChild(el('div', 'tree-error', '加载失败'));\n });\n }\n\n function renderTree() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) return;\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', '(空目录)'));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, '已复制 @引用');\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', '@引用');\n refBtn.type = 'button';\n refBtn.title = '复制 @path 引用';\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', '加载中…');\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || '读取失败' };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: '读取失败' };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n if (currentView === 'tree') loadTreeRoot();\n else loadGit(true);\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n // force=1 绕过 host 的 stale-while-revalidate 缓存,由刷新按钮使用。\n // 刷新后给列表行加交错浮现动画(从上往下);selector 区分 git 列表与文件树。\n function staggerList(list, selector) {\n var items = list.querySelectorAll(selector || '.item');\n for (var i = 0; i < items.length; i++) {\n var it = items[i];\n it.classList.remove('flash-in');\n void it.offsetWidth; // 重置动画,使连续刷新也能重放\n it.classList.add('flash-in');\n it.style.animationDelay = (Math.min(i, 12) * 45) + 'ms';\n }\n }\n function loadGit(force) {\n var list = document.getElementById('list');\n // 强制刷新时列表短暂变暗,响应返回后恢复,给出「刷新过了」的可见反馈。\n if (force && list) list.classList.add('is-refreshing');\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || '') + (force ? '&force=1' : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n var ok = data && data.ok === true;\n if (ok) {\n st.textContent = 'live';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-success-fg').trim() || '#34c55e';\n } else {\n st.textContent = 'git error';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || 'git status 失败');\n var sig = JSON.stringify([nextError, nextFiles]);\n if (!force && sig === gitSig) return;\n gitSig = sig;\n gitFiles = nextFiles;\n gitError = nextError;\n renderGit();\n if (force && list) staggerList(list);\n }).catch(function () {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = getComputedStyle(document.documentElement).getPropertyValue('--p-error').trim() || '#ef4444';\n });\n }\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the left (default) or the right ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = true;\n try { var _savedSide = localStorage.getItem(PANEL_SIDE_KEY); if (_savedSide === '0') panelLeft = false; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? '将文件面板移到右侧' : '将文件面板移到左侧';\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n }\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"])), sharedScript);
|
|
703
719
|
}
|
|
704
720
|
//#endregion
|
|
705
721
|
//#region src/host/routes.ts
|
|
@@ -818,7 +834,8 @@ function registerRoutes(ctx, webServer) {
|
|
|
818
834
|
kind: "exact",
|
|
819
835
|
path: "/flyout-sidebar/gitstatus",
|
|
820
836
|
handler: async (req, res) => {
|
|
821
|
-
|
|
837
|
+
const q = queryParams(req.url);
|
|
838
|
+
sendJson(res, await gitStatus(ctx, q.get("sessionId") || void 0, { force: q.get("force") === "1" }), true);
|
|
822
839
|
}
|
|
823
840
|
}, "git: status route");
|
|
824
841
|
register({
|