dsh-flyout-sidebar 0.1.3 → 0.1.5

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 CHANGED
@@ -200,7 +200,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
200
200
  .artifacts-minibtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
201
201
  .artifacts-notice { color: var(--dsw-alias-state-business-primary); font-size: 12px; }
202
202
  .artifacts-hint { padding: 24px 16px; color: var(--dsw-alias-label-tertiary); text-align: center; }
203
- .artifacts-error { padding: 16px; color: var(--dsw-alias-state-error-primary); font-family: var(--dsh-font-mono, monospace); word-break: break-all; }
203
+ .artifacts-error { padding: 16px; color: var(--dsw-alias-state-error-primary); font-family: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); word-break: break-all; }
204
204
  .artifacts-corner-btn {
205
205
  position: fixed; top: 0; right: calc(var(--dsh-sidebar-width, 0px) + 12px);
206
206
  z-index: 10000; width: 36px; height: 28px; padding: 0;
@@ -233,19 +233,22 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
233
233
  .artifacts-markdown h1, .artifacts-markdown h2, .artifacts-markdown h3, .artifacts-markdown h4, .artifacts-markdown h5, .artifacts-markdown h6 { margin: 14px 0 8px; line-height: 1.3; }
234
234
  .artifacts-markdown h1 { font-size: 1.45em; border-bottom: 1px solid var(--dsw-alias-border-l2); padding-bottom: 6px; }
235
235
  .artifacts-markdown h2 { font-size: 1.25em; border-bottom: 1px solid var(--dsw-alias-border-l1); padding-bottom: 4px; }
236
- .artifacts-markdown code { background: var(--dsw-alias-bg-layer-1); padding: 1px 5px; border-radius: 4px; font-family: var(--dsh-font-mono, ui-monospace, monospace); font-size: 0.9em; }
236
+ .artifacts-markdown code { background: var(--dsw-alias-bg-layer-1); padding: 1px 5px; border-radius: 4px; font-family: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); font-size: 0.9em; }
237
237
  .artifacts-markdown pre { background: var(--dsw-alias-bg-layer-1); padding: 10px 12px; border-radius: 6px; overflow: auto; }
238
238
  .artifacts-markdown pre code { background: transparent; padding: 0; }
239
239
  .artifacts-markdown img { max-width: 100%; }
240
240
  .artifacts-markdown blockquote { border-left: 3px solid var(--dsw-alias-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--dsw-alias-label-secondary); }
241
241
  .artifacts-markdown ul, .artifacts-markdown ol { padding-left: 24px; }
242
242
  .artifacts-markdown a { color: var(--dsw-alias-state-business-primary); }
243
+ .artifacts-markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 10px 0; }
244
+ .artifacts-markdown th, .artifacts-markdown td { border: 1px solid var(--dsw-alias-border-l2); padding: 4px 10px; text-align: left; }
245
+ .artifacts-markdown th { background: var(--dsw-alias-bg-layer-1); font-weight: 600; }
243
246
  .artifacts-diff { border-top: 1px solid var(--dsw-alias-border-l2); }
244
247
  .artifacts-diff-block { border-bottom: 1px solid var(--dsw-alias-border-l1); }
245
248
  .artifacts-diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }
246
249
  .artifacts-diff-del .artifacts-diff-label { color: var(--dsw-alias-state-error-primary); background: rgba(236,19,19,0.06); }
247
250
  .artifacts-diff-add .artifacts-diff-label { color: var(--dsw-alias-state-success-primary); background: rgba(34,197,94,0.08); }
248
- .artifacts-diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 var(--dsh-font-mono, ui-monospace, monospace); white-space: pre-wrap; word-break: break-word; color: var(--dsw-alias-label-secondary); }
251
+ .artifacts-diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); white-space: pre-wrap; word-break: break-word; color: var(--dsw-alias-label-secondary); }
249
252
  .artifacts-diff-del .artifacts-diff-pre { background: rgba(236,19,19,0.05); }
250
253
  .artifacts-diff-add .artifacts-diff-pre { background: rgba(34,197,94,0.06); }
251
254
  .artifacts-resize { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 3; touch-action: none; }
@@ -254,7 +257,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
254
257
  .artifacts-viewbtn { display: inline-flex; align-items: center; justify-content: center; height: 26px; width: 26px; padding: 0; border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; border-radius: 6px; }
255
258
  .artifacts-viewbtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
256
259
  .artifacts-viewbtn.is-active { color: var(--dsw-alias-state-business-primary); }
257
- .artifacts-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: var(--dsh-font-mono, ui-monospace, monospace); }
260
+ .artifacts-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: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); }
258
261
  .artifacts-git-badge-M { background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); }
259
262
  .artifacts-git-badge-A { background: var(--dsw-alias-state-success-tertiary); color: var(--dsw-alias-state-success-primary); }
260
263
  .artifacts-git-badge-D { background: rgba(236,19,19,0.1); color: var(--dsw-alias-state-error-primary); }
@@ -262,7 +265,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
262
265
  .artifacts-git-badge-U { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-tertiary); }
263
266
  .artifacts-git-orig { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
264
267
  .artifacts-git-error { padding: 14px 12px; word-break: break-all; }
265
- .artifacts-gitdiff { font-family: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.6; }
268
+ .artifacts-gitdiff { font-family: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); font-size: 12px; line-height: 1.6; }
266
269
  .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }
267
270
  .gd-meta { color: var(--dsw-alias-label-tertiary); background: var(--dsw-alias-bg-layer-1); padding: 2px 12px; }
268
271
  .gd-hunk { color: var(--dsw-alias-state-business-primary); background: var(--dsw-alias-state-business-tertiary, rgba(65,118,230,0.08)); padding: 2px 12px; }
@@ -318,18 +321,23 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
318
321
  .artifacts-widthinput { width: 76px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 6px; padding: 4px 8px; }
319
322
  .artifacts-langselect { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 6px; padding: 4px 8px; cursor: pointer; }
320
323
  .artifacts-suffix { color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 22px; }
324
+ .artifacts-ctxmenu-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10001; }
325
+ .artifacts-ctxmenu { position: fixed; z-index: 10002; min-width: 140px; padding: 4px 0; background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; box-shadow: var(--dsw-shadow-lv2); font-size: 12px; }
326
+ .artifacts-ctxmenu-item { display: block; width: 100%; padding: 6px 14px; border: none; background: transparent; color: var(--dsw-alias-label-primary); text-align: left; cursor: pointer; font: inherit; }
327
+ .artifacts-ctxmenu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
328
+ .artifacts-ctxmenu-item:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
321
329
  .artifacts-code { display: flex; flex-direction: column; height: 100%; min-height: 0; }
322
330
  /* 代码视图:逐行渲染(行 = 行号 + 代码),软换行时行号仍对齐 */
323
331
  .artifacts-code-scroll { flex: 1; min-height: 0; overflow: auto; padding: 12px 0; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); }
324
332
  .artifacts-code-line { display: flex; align-items: flex-start; min-width: fit-content; }
325
- .artifacts-code-gutter { flex: none; min-width: 2.2em; padding: 0 6px 0 8px; text-align: right; color: var(--dsw-alias-label-tertiary); border-right: 1px solid var(--dsw-alias-border-l1); position: sticky; left: 0; user-select: none; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
326
- .artifacts-code-pre { flex: 1 0 auto; margin: 0; padding: 0 12px; color: var(--shiki-foreground, var(--dsw-alias-label-primary)); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); white-space: pre; }
333
+ .artifacts-code-gutter { flex: none; min-width: 2.2em; padding: 0 6px 0 8px; text-align: right; color: var(--dsw-alias-label-tertiary); border-right: 1px solid var(--dsw-alias-border-l1); position: sticky; left: 0; user-select: none; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); }
334
+ .artifacts-code-pre { flex: 1 0 auto; margin: 0; padding: 0 12px; color: var(--shiki-foreground, var(--dsw-alias-label-primary)); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); white-space: pre; }
327
335
  .artifacts-code-pre code { font: inherit; color: inherit; }
328
336
  .artifacts-code-wrap .artifacts-code-pre { white-space: pre-wrap; word-break: break-all; }
329
337
  .artifacts-imgview { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: grab; user-select: none; }
330
338
  .artifacts-imgview.is-dragging { cursor: grabbing; }
331
339
  .artifacts-imgview .artifacts-img { flex: none; margin: 16px; transform-origin: center; will-change: transform; }
332
- .artifacts-git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
340
+ .artifacts-git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace); }
333
341
  .artifacts-git-adds { color: var(--p-success-fg, #1a7f37); }
334
342
  .artifacts-git-dels { color: var(--p-error, #cf222e); }
335
343
  .tok-comment { color: #868e96; }
@@ -471,6 +479,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
471
479
  backToFiles: "返回文件列表",
472
480
  hidePreview: "隐藏预览(标签页保留)",
473
481
  closeTab: "关闭标签页",
482
+ closeOthers: "关闭其他",
483
+ closeRight: "关闭右侧标签",
474
484
  previewRegion: "文件预览",
475
485
  diffTabPrefix: "[diff] ",
476
486
  searchPlaceholder: "搜索文件…",
@@ -556,6 +566,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
556
566
  backToFiles: "Back to file list",
557
567
  hidePreview: "Hide preview (tabs are kept)",
558
568
  closeTab: "Close tab",
569
+ closeOthers: "Close others",
570
+ closeRight: "Close tabs to the right",
559
571
  previewRegion: "File preview",
560
572
  diffTabPrefix: "[diff] ",
561
573
  searchPlaceholder: "Search files…",
@@ -1305,6 +1317,39 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1305
1317
  s = s.replace(/\u0000(\d+)\u0000/g, (m, i) => codes[Number(i)] ?? "");
1306
1318
  return s;
1307
1319
  }
1320
+ /**
1321
+ * GFM 管道表格辅助:按未转义的 | 切分单元格(行内 \| 还原为字面量竖线),
1322
+ * 并去掉首尾管道产生的空单元格。
1323
+ * @param {string} line @returns {string[]}
1324
+ */
1325
+ function mdSplitCells(line) {
1326
+ const parts = String(line).replace(/\\\|/g, "").split("|");
1327
+ for (let i = 0; i < parts.length; i += 1) parts[i] = (parts[i] || "").replace(/\u0002/g, "|").trim();
1328
+ return parts;
1329
+ }
1330
+ /** @param {string[]} cells @returns {string[]} */
1331
+ function mdTrimEdgeCells(cells) {
1332
+ if (cells.length && cells[0] === "") cells.shift();
1333
+ if (cells.length && cells[cells.length - 1] === "") cells.pop();
1334
+ return cells;
1335
+ }
1336
+ /** @param {string[]} cells @returns {boolean} 是否为 GFM 分隔行(:--- / :---: / ---: 形态) */
1337
+ function mdIsDelimiterRow(cells) {
1338
+ return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
1339
+ }
1340
+ /**
1341
+ * @param {string} cell @returns {string} 'center' | 'right' | ''(left 与默认一致,不输出)
1342
+ */
1343
+ function mdCellAlign(cell) {
1344
+ if (/^:-+:$/.test(cell)) return "center";
1345
+ if (/^:-+$/.test(cell)) return "left";
1346
+ if (/^-+:$/.test(cell)) return "right";
1347
+ return "";
1348
+ }
1349
+ /** @param {string} a @returns {string} 单元格对齐的内联样式属性 */
1350
+ function mdAlignAttr(a) {
1351
+ return a === "center" || a === "right" ? " style=\"text-align:" + a + "\"" : "";
1352
+ }
1308
1353
  /** @param {string} src @returns {string} HTML */
1309
1354
  function mdToHtml(src) {
1310
1355
  const lines = String(src || "").replace(/\r\n/g, "\n").split("\n");
@@ -1373,6 +1418,35 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1373
1418
  i += 1;
1374
1419
  continue;
1375
1420
  }
1421
+ if (line.indexOf("|") >= 0 && i + 1 < lines.length) {
1422
+ const head = mdTrimEdgeCells(mdSplitCells(line));
1423
+ const delim = mdTrimEdgeCells(mdSplitCells(lines[i + 1]));
1424
+ if (head.length && mdIsDelimiterRow(delim)) {
1425
+ const aligns = delim.map(mdCellAlign);
1426
+ const colCount = head.length;
1427
+ /** @type {string[][]} */
1428
+ const rows = [];
1429
+ i += 2;
1430
+ while (i < lines.length && lines[i].trim() !== "" && lines[i].indexOf("|") >= 0) {
1431
+ const cells = mdTrimEdgeCells(mdSplitCells(lines[i]));
1432
+ /** @type {string[]} */
1433
+ const padded = [];
1434
+ for (let c = 0; c < colCount; c += 1) padded.push(cells[c] || "");
1435
+ rows.push(padded);
1436
+ i += 1;
1437
+ }
1438
+ let table = "<table><thead><tr>";
1439
+ for (let c = 0; c < colCount; c += 1) table += "<th" + mdAlignAttr(aligns[c] || "") + ">" + mdInline(mdEscape(head[c] || "")) + "</th>";
1440
+ table += "</tr></thead><tbody>";
1441
+ for (const cells of rows) {
1442
+ table += "<tr>";
1443
+ for (let c = 0; c < colCount; c += 1) table += "<td" + mdAlignAttr(aligns[c] || "") + ">" + mdInline(mdEscape(cells[c] || "")) + "</td>";
1444
+ table += "</tr>";
1445
+ }
1446
+ out.push(table + "</tbody></table>");
1447
+ continue;
1448
+ }
1449
+ }
1376
1450
  out.push("<p>" + mdInline(mdEscape(line)) + "</p>");
1377
1451
  i += 1;
1378
1452
  }
@@ -2209,6 +2283,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2209
2283
  return remaining[Math.min(Math.max(closedIdx, 0), remaining.length - 1)]?.key ?? null;
2210
2284
  });
2211
2285
  };
2286
+ const closeOthers = (key) => {
2287
+ setTabs((prev) => prev.filter((tb) => tb.key === key));
2288
+ setActiveKey(key);
2289
+ };
2290
+ const closeRight = (key) => {
2291
+ const idx = tabsRef.current.findIndex((tb) => tb.key === key);
2292
+ if (idx < 0) return;
2293
+ const removedKeys = new Set(tabsRef.current.slice(idx + 1).map((tb) => tb.key));
2294
+ if (!removedKeys.size) return;
2295
+ setTabs((prev) => prev.slice(0, idx + 1));
2296
+ setActiveKey((cur) => cur && removedKeys.has(cur) ? key : cur);
2297
+ };
2212
2298
  const tabsRef = React.useRef([]);
2213
2299
  React.useEffect(() => {
2214
2300
  tabsRef.current = tabs;
@@ -2287,7 +2373,9 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2287
2373
  setPreviewHidden,
2288
2374
  openFile,
2289
2375
  openGitDiff,
2290
- closeTab
2376
+ closeTab,
2377
+ closeOthers,
2378
+ closeRight
2291
2379
  };
2292
2380
  }
2293
2381
  /** git 变更列表(文件面板的「变更」视图) */
@@ -2337,9 +2425,10 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2337
2425
  const settings = useSettings();
2338
2426
  useLang();
2339
2427
  const sessionId = useSessionId();
2340
- const { tabs, activeKey, setActiveKey, previewHidden, setPreviewHidden, openFile, openGitDiff, closeTab } = usePreviewTabs(sessionId);
2428
+ const { tabs, activeKey, setActiveKey, previewHidden, setPreviewHidden, openFile, openGitDiff, closeTab, closeOthers, closeRight } = usePreviewTabs(sessionId);
2341
2429
  const activeTab = tabs.find((t) => t.key === activeKey) || null;
2342
2430
  const [notice, setNotice] = React.useState("");
2431
+ const [ctxMenu, setCtxMenu] = React.useState(null);
2343
2432
  const [winW, setWinW] = React.useState(() => typeof window !== "undefined" ? window.innerWidth : 1400);
2344
2433
  const [gitFiles, setGitFiles] = React.useState(null);
2345
2434
  const [gitError, setGitError] = React.useState(null);
@@ -2545,7 +2634,15 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2545
2634
  key: tab.key,
2546
2635
  className: "artifacts-ptab" + (tab.key === activeKey ? " is-active" : ""),
2547
2636
  title: (tab.git ? t("diffTabPrefix") : "") + (tab.path || ""),
2548
- onClick: () => setActiveKey(tab.key)
2637
+ onClick: () => setActiveKey(tab.key),
2638
+ onContextMenu: (e) => {
2639
+ e.preventDefault();
2640
+ setCtxMenu({
2641
+ x: e.clientX,
2642
+ y: e.clientY,
2643
+ key: tab.key
2644
+ });
2645
+ }
2549
2646
  }, /* @__PURE__ */ h("span", { className: "artifacts-ptab-name" }, basename(tab.path || "")), /* @__PURE__ */ h("button", {
2550
2647
  type: "button",
2551
2648
  className: "artifacts-ptab-close",
@@ -2559,7 +2656,43 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2559
2656
  className: "artifacts-preview-hide",
2560
2657
  title: t("hidePreview"),
2561
2658
  onClick: () => setPreviewHidden(true)
2562
- }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null) : null;
2659
+ }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null, ctxMenu ? /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h("div", {
2660
+ className: "artifacts-ctxmenu-backdrop",
2661
+ onClick: () => setCtxMenu(null),
2662
+ onContextMenu: (e) => {
2663
+ e.preventDefault();
2664
+ setCtxMenu(null);
2665
+ }
2666
+ }), /* @__PURE__ */ h("div", {
2667
+ className: "artifacts-ctxmenu",
2668
+ style: {
2669
+ left: ctxMenu.x,
2670
+ top: ctxMenu.y
2671
+ }
2672
+ }, /* @__PURE__ */ h("button", {
2673
+ type: "button",
2674
+ className: "artifacts-ctxmenu-item",
2675
+ onClick: () => {
2676
+ closeTab(ctxMenu.key);
2677
+ setCtxMenu(null);
2678
+ }
2679
+ }, t("closeTab")), /* @__PURE__ */ h("button", {
2680
+ type: "button",
2681
+ className: "artifacts-ctxmenu-item",
2682
+ onClick: () => {
2683
+ closeOthers(ctxMenu.key);
2684
+ setCtxMenu(null);
2685
+ },
2686
+ disabled: tabs.length <= 1
2687
+ }, t("closeOthers")), /* @__PURE__ */ h("button", {
2688
+ type: "button",
2689
+ className: "artifacts-ctxmenu-item",
2690
+ onClick: () => {
2691
+ closeRight(ctxMenu.key);
2692
+ setCtxMenu(null);
2693
+ },
2694
+ disabled: tabs.findIndex((tb) => tb.key === ctxMenu.key) >= tabs.length - 1
2695
+ }, t("closeRight")))) : null) : null;
2563
2696
  return /* @__PURE__ */ h(Fragment, null, previewOverlay, /* @__PURE__ */ h("div", {
2564
2697
  className: "artifacts-panel" + (slidOut ? " artifacts-slid-out" : "") + (resizing ? " artifacts-resizing" : ""),
2565
2698
  style: { width: widthPx },
package/dist/index.js CHANGED
@@ -1020,10 +1020,10 @@ var ext_default = "/**\n * 共享:扩展名 → 预览类型 判定。\n *\n *
1020
1020
  var highlight_default = "/**\n * 共享:零依赖语法高亮器(无模板字面量以外的构建期依赖,正则引擎全部内联)。\n *\n * 可移植性约束见 ext.ts 顶部说明:本文件经 `?raw` 原样内联进独立弹出页的\n * 经典 <script>,因此只能使用 JSDoc 标注类型。输出 span.tok-* 令牌,颜色\n * 由两端各自的 CSS(styles.ts / page 模板)负责。\n */\n\n/** @param {string} s @returns {string} */\nexport function escHtml(s) {\n return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/**\n * @param {[string, string][]} specs [令牌类名, 正则源] 列表,顺序即优先级\n * @param {string} [flags]\n * @returns {(code: string) => string}\n */\nexport function makeHl(specs, flags) {\n let src = ''\n for (let i = 0; i < specs.length; i += 1) {\n const spec = specs[i]\n if (!spec) continue\n src += (i ? '|' : '') + '(' + spec[1] + ')'\n }\n const re = new RegExp(src, flags || 'g')\n return (code) => {\n re.lastIndex = 0\n let out = ''\n let last = 0\n let m\n while ((m = re.exec(code)) !== null) {\n if (m.index > last) out += escHtml(code.slice(last, m.index))\n for (let g = 1; g < m.length; g += 1) {\n const tok = m[g]\n if (tok !== undefined) {\n const spec = specs[g - 1]\n if (spec) out += '<span class=\"tok-' + spec[0] + '\">' + escHtml(tok) + '</span>'\n break\n }\n }\n last = re.lastIndex\n if (m[0].length === 0) {\n re.lastIndex += 1\n last = re.lastIndex\n }\n }\n if (last < code.length) out += escHtml(code.slice(last))\n return out\n }\n}\n\nconst S_DQ = \"\\\"(?:[^\\\"\\\\\\\\\\\\n]|\\\\\\\\.)*\\\"\"\nconst S_SQ = \"\\\\x27(?:[^\\\\x27\\\\\\\\\\\\n]|\\\\\\\\.)*\\\\x27\"\nconst S_BT = \"\\\\x60(?:[^\\\\x60\\\\\\\\]|\\\\\\\\.)*\\\\x60\"\nconst NUM = \"\\\\b(?:0[xX][0-9a-fA-F]+|\\\\d+(?:\\\\.\\\\d+)?(?:[eE][+-]?\\\\d+)?)\\\\b\"\nconst C_LINE = \"//[^\\\\n]*\"\nconst C_BLK = \"/\\\\*[\\\\s\\\\S]*?\\\\*/\"\nconst HASH = \"#[^\\\\n]*\"\nconst SQL_LINE = \"--[^\\\\n]*\"\nconst HTML_COMMENT = \"<!--[\\\\s\\\\S]*?-->\"\nconst PY_TRI = \"(?:\\\"\\\"\\\"[\\\\s\\\\S]*?\\\"\\\"\\\"|\\\\x27\\\\x27\\\\x27[\\\\s\\\\S]*?\\\\x27\\\\x27\\\\x27)\"\nconst PY_STR = \"(?:[rfbuRFBU]{0,2})(?:\\\"(?:[^\\\"\\\\\\\\\\\\n]|\\\\\\\\.)*\\\"|\\\\x27(?:[^\\\\x27\\\\\\\\\\\\n]|\\\\\\\\.)*\\\\x27)\"\nconst CSS_NUM = \"\\\\b\\\\d+(?:\\\\.\\\\d+)?(?:[a-zA-Z%]*)\\\\b\"\nconst HEX = \"#[0-9a-fA-F]{3,8}\\\\b\"\nconst AT = \"@[\\\\w-]+\"\nconst PROP = \"[\\\\w-]+(?=\\\\s*:)\"\nconst TAG = \"</?[\\\\w-]+|/?>\"\nconst ATTR = \"[\\\\w-]+(?==)\"\nconst VAR = \"\\\\$(?:\\\\{[\\\\w]+\\\\}|[\\\\w]+)\"\nconst VAR_PHP = \"\\\\$\\\\w+\"\nconst DECORATOR = \"@[\\\\w.]+\"\nconst IMPORTANT = \"!important\\\\b\"\nconst FUNC = \"\\\\b[A-Za-z_$][\\\\w$]*(?=\\\\s*\\\\()\"\nconst FUNC_PY = \"\\\\b[A-Za-z_][\\\\w]*(?=\\\\s*\\\\()\"\nconst CLASS = \"\\\\b[A-Z][\\\\w$]*\\\\b\"\nconst YAML_KEY = \"^\\\\s*(?:-\\\\s+)?[\\\\w.@-]+(?=\\\\s*:)\"\n\n/** @param {string} kw 空白分隔的关键字列表 @returns {string} */\nfunction kwWord(kw) {\n return '\\\\b(?:' + kw.replace(/\\s+/g, '|') + ')\\\\b'\n}\n\nconst JS_KW = 'break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield async await of get set null undefined true false'\nconst PY_KW = 'and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield True False None self'\nconst SH_KW = 'if then elif else fi for while do done case esac function select in until return exit set unset export readonly local shift source'\nconst SQL_KW = 'select from where insert into update delete create drop alter table index view join left right inner outer full on as and or not null group by order having limit offset union all distinct values set primary key foreign references default like between is in exists asc desc'\nconst C_KW = 'auto break case const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while'\nconst GO_KW = 'break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var'\nconst RUST_KW = 'as async await break const continue crate dyn else enum extern fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait type union unsafe use where while'\nconst JAVA_KW = 'abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while'\nconst RB_KW = 'begin case class def do else elsif end ensure for if module next nil not or redo rescue retry return self super then true false undef unless until when while yield'\nconst PHP_KW = 'abstract and array as break callable case catch class clone const continue declare default do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile extends final finally fn for foreach function global if implements include instanceof insteadof interface isset list namespace new or print private protected public require return static switch throw trait try unset use var while xor yield'\n\n/** @param {string} kw @returns {(code: string) => string} */\nfunction cFamily(kw) {\n return makeHl([\n ['comment', C_LINE + '|' + C_BLK],\n ['string', S_BT + '|' + S_DQ + '|' + S_SQ],\n ['number', NUM],\n ['keyword', kwWord(kw)],\n ['function', FUNC],\n ['class', CLASS],\n ])\n}\n\n/** @type {Record<string, (code: string) => string>} */\nconst HL_ENGINES = {\n js: makeHl([\n ['comment', C_LINE + '|' + C_BLK],\n ['string', S_BT + '|' + S_DQ + '|' + S_SQ],\n ['number', NUM],\n ['keyword', kwWord(JS_KW)],\n ['builtin', '\\\\b(?:console|Math|JSON|Promise|Array|Object|String|Number|Boolean|RegExp|Date|Map|Set|WeakMap|WeakSet|Symbol|BigInt|Infinity|NaN|window|document|process|require|module|exports|setTimeout|clearTimeout|fetch|globalThis)\\\\b'],\n ['function', FUNC],\n ['class', CLASS],\n ]),\n py: makeHl([\n ['comment', HASH],\n ['string', PY_TRI + '|' + PY_STR],\n ['number', NUM],\n ['keyword', kwWord(PY_KW)],\n ['builtin', '\\\\b(?:print|len|range|enumerate|zip|map|filter|int|str|float|bool|list|dict|set|tuple|type|isinstance|super|open|input|repr|format|sorted|reversed|sum|min|max|abs|round|any|all|next|iter|dir|vars|getattr|setattr|hasattr|id|hash|bytes|bytearray|complex|frozenset|object|classmethod|staticmethod|property|Exception|ValueError|TypeError|KeyError|IndexError|ImportError|RuntimeError|StopIteration)\\\\b'],\n ['decorator', DECORATOR],\n ['function', FUNC_PY],\n ]),\n css: makeHl([\n ['comment', C_BLK],\n ['string', S_DQ + '|' + S_SQ],\n ['atrule', AT],\n ['property', PROP],\n ['number', CSS_NUM],\n ['hex', HEX],\n ['important', IMPORTANT],\n ]),\n html: makeHl([\n ['comment', HTML_COMMENT],\n ['string', S_DQ + '|' + S_SQ],\n ['tag', TAG],\n ['attr', ATTR],\n ]),\n sh: makeHl([\n ['comment', HASH],\n ['string', S_DQ + '|' + S_SQ + '|' + S_BT],\n ['variable', VAR],\n ['number', NUM],\n ['keyword', kwWord(SH_KW)],\n ]),\n yaml: makeHl([\n ['comment', HASH],\n ['string', S_DQ + '|' + S_SQ],\n ['number', NUM],\n ['bool', '\\\\b(?:true|false|null|yes|no|on|off)\\\\b'],\n ['key', YAML_KEY],\n ], 'gm'),\n sql: makeHl([\n ['comment', SQL_LINE + '|' + C_BLK],\n ['string', S_SQ + '|' + S_DQ],\n ['number', NUM],\n ['keyword', kwWord(SQL_KW)],\n ['function', FUNC_PY],\n ], 'gi'),\n json: makeHl([\n ['string', S_DQ],\n ['number', NUM],\n ['bool', '\\\\b(?:true|false|null)\\\\b'],\n ]),\n c: cFamily(C_KW),\n cpp: cFamily(C_KW),\n go: cFamily(GO_KW),\n rust: cFamily(RUST_KW),\n java: cFamily(JAVA_KW),\n rb: makeHl([\n ['comment', HASH],\n ['string', S_DQ + '|' + S_SQ],\n ['number', NUM],\n ['keyword', kwWord(RB_KW)],\n ['function', FUNC_PY],\n ['class', CLASS],\n ]),\n php: makeHl([\n ['comment', C_LINE + '|' + C_BLK + '|' + HASH],\n ['string', S_DQ + '|' + S_SQ],\n ['variable', VAR_PHP],\n ['number', NUM],\n ['keyword', kwWord(PHP_KW)],\n ['function', FUNC_PY],\n ]),\n}\n\n/** @type {Record<string, string>} */\nconst HL_LANG_MAP = {\n js: 'js', mjs: 'js', cjs: 'js', jsx: 'js', javascript: 'js',\n ts: 'js', tsx: 'js', mts: 'js', cts: 'js', typescript: 'js',\n json: 'json', jsonc: 'json', json5: 'js',\n py: 'py', python: 'py', pyw: 'py',\n rb: 'rb', ruby: 'rb',\n go: 'go', golang: 'go',\n rs: 'rust', rust: 'rust',\n java: 'java',\n c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', cxx: 'cpp', hpp: 'cpp', cs: 'c', csharp: 'c',\n kotlin: 'c', kt: 'c', swift: 'c',\n php: 'php',\n yaml: 'yaml', yml: 'yaml', toml: 'sh', ini: 'sh', conf: 'sh', properties: 'sh', env: 'sh',\n md: 'md', markdown: 'md', mdx: 'md',\n html: 'html', htm: 'html', xhtml: 'html', vue: 'html', xml: 'html', svg: 'html',\n css: 'css', scss: 'css', less: 'css',\n sql: 'sql',\n lua: 'c',\n sh: 'sh', bash: 'sh', shell: 'sh', zsh: 'sh', fish: 'sh',\n}\n\n/** @type {Record<string, string>} */\nconst HL_LANG_NAMES = {\n js: 'JavaScript', py: 'Python', css: 'CSS', html: 'HTML/XML', sh: 'Shell',\n yaml: 'YAML', sql: 'SQL', c: 'C/C++', cpp: 'C++', go: 'Go', rust: 'Rust',\n java: 'Java', rb: 'Ruby', php: 'PHP', json: 'JSON', plain: 'Text',\n}\n\n/** @param {string} hint 扩展名或语言名 @returns {string} 引擎键名 */\nexport function hlLangOf(hint) {\n let h = String(hint || '').toLowerCase()\n if (h.charAt(0) === '.') h = h.slice(1)\n return HL_LANG_MAP[h] || 'plain'\n}\n\n/** @param {string} hint @returns {string} 语言显示名 */\nexport function hlLangLabel(hint) {\n return HL_LANG_NAMES[hlLangOf(hint)] || 'Text'\n}\n\n/** @param {string} src @param {string} hint @returns {string} HTML */\nexport function highlightCode(src, hint) {\n const fn = HL_ENGINES[hlLangOf(hint)]\n return fn ? fn(String(src)) : escHtml(src)\n}\n\n/**\n * 把 highlightCode 输出的 HTML 按源码行拆分成逐行 HTML 数组。跨行 token\n * (块注释、Python 三引号串等)在行边界处闭合、下一行重新打开,保证逐行\n * 渲染时着色与整块渲染一致。逐行渲染使行号与代码天然对齐(含软换行模式),\n * 也是行级查找高亮的基础。\n * @param {string} html highlightCode 的输出\n * @returns {string[]}\n */\nexport function splitHlLines(html) {\n var lines = []\n var cur = ''\n /** @type {string[]} */\n var stack = [] // 打开中的 tok-* class 名\n var i = 0\n var openAll = function () {\n var s = ''\n for (var k = 0; k < stack.length; k++) s += '<span class=\"' + stack[k] + '\">'\n return s\n }\n var closeAll = function () {\n var s = ''\n for (var k = 0; k < stack.length; k++) s += '</span>'\n return s\n }\n while (i < html.length) {\n if (html.indexOf('<span class=\"', i) === i) {\n var end = html.indexOf('>', i)\n if (end < 0) {\n cur += html.slice(i)\n break\n }\n cur += html.slice(i, end + 1)\n stack.push(html.slice(i + 13, end - 1))\n i = end + 1\n continue\n }\n if (html.indexOf('</span>', i) === i) {\n cur += '</span>'\n stack.pop()\n i += 7\n continue\n }\n var nl = html.indexOf('\\n', i)\n var nextTag = html.indexOf('<', i)\n if (nextTag >= 0 && (nl < 0 || nextTag < nl)) {\n cur += html.slice(i, nextTag)\n i = nextTag\n continue\n }\n if (nl < 0) {\n cur += html.slice(i)\n break\n }\n cur += html.slice(i, nl)\n lines.push(cur + closeAll())\n cur = openAll()\n i = nl + 1\n }\n lines.push(cur + closeAll())\n return lines\n}\n";
1021
1021
  //#endregion
1022
1022
  //#region src/shared/markdown.js?raw
1023
- var markdown_default = "/**\n * 共享:极简 Markdown → HTML 渲染器。\n *\n * 可移植性约束见 ext.ts 顶部说明:本文件经 `?raw` 原样内联进独立弹出页的\n * 经典 <script>,只能使用 JSDoc 标注类型。围栏代码块通过 shared/highlight\n * 的 highlightCode 高亮。\n */\nimport { highlightCode } from './highlight.js'\n\n/** @param {string} s @returns {string} */\nfunction mdEscape(s) {\n return String(s).replace(/[\\u0000\\u0001]/g, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\n/**\n * URL 白名单:只放行 http(s)、站内相对路径、页内锚点与 data:image/*,\n * 其余(javascript:、vbscript: 及实体编码变体等)一律替换为 '#',防止\n * 预览渲染出来的链接/图片在点击时执行脚本。\n * @param {string} u @returns {string}\n */\nfunction mdSafeUrl(u) {\n var s = String(u || '').replace(/[\\s\\u0000-\\u001f]/g, '')\n if (/^https?:\\/\\//i.test(s)) return s\n if (/^\\/(?!\\/)/.test(s) || /^\\.{1,2}\\//.test(s) || s.charAt(0) === '#') return s\n if (/^data:image\\/(?:png|gif|jpeg|webp|bmp|avif);/i.test(s)) return s\n return '#'\n}\n\n/** @param {string} s @returns {string} */\nfunction mdInline(s) {\n // code span 先摘出为占位符再跑后续规则:反引号内的 `[x](url)`、`**b**`\n // 应原样输出,不被行内链接/强调规则二次渲染(占位符字符已在 mdEscape 剥除)。\n /** @type {string[]} */\n const codes = []\n s = s.replace(/`([^`]+)`/g, (m, c) => {\n codes.push('<code>' + c + '</code>')\n return '\\u0000' + (codes.length - 1) + '\\u0000'\n })\n s = s.replace(/!\\[([^\\]]*)\\]\\(([^)\\s]+)\\)/g, (m, alt, u) => '<img alt=\"' + alt + '\" src=\"' + mdSafeUrl(u) + '\">')\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, (m, text, u) => '<a href=\"' + mdSafeUrl(u) + '\" target=\"_blank\" rel=\"noopener noreferrer\">' + text + '</a>')\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n s = s.replace(/\\*([^*\\n]+)\\*/g, '<em>$1</em>')\n s = s.replace(/\\u0000(\\d+)\\u0000/g, (m, i) => codes[Number(i)] ?? '')\n return s\n}\n\n/** @param {string} src @returns {string} HTML */\nexport function mdToHtml(src) {\n const lines = String(src || '').replace(/\\r\\n/g, '\\n').split('\\n')\n /** @type {string[]} */\n const out = []\n let i = 0\n while (i < lines.length) {\n const line = /** @type {string} */ (lines[i])\n if (/^\\s*```/.test(line)) {\n const fence = /^\\s*```([\\w+-]*)/.exec(line)\n const langHint = fence ? fence[1] || '' : ''\n /** @type {string[]} */\n const buf = []\n i += 1\n while (i < lines.length && !/^\\s*```/.test(/** @type {string} */ (lines[i]))) {\n buf.push(/** @type {string} */ (lines[i]))\n i += 1\n }\n i += 1\n out.push('<pre><code>' + highlightCode(buf.join('\\n'), langHint) + '</code></pre>')\n continue\n }\n const h = /^(#{1,6})\\s+(.*)$/.exec(line)\n if (h) {\n const lv = (h[1] || '').length\n out.push('<h' + lv + '>' + mdInline(mdEscape(/** @type {string} */ (h[2]))) + '</h' + lv + '>')\n i += 1\n continue\n }\n if (/^\\s*(---+|\\*\\*\\*+|___+)\\s*$/.test(line)) {\n out.push('<hr>')\n i += 1\n continue\n }\n if (/^\\s*>\\s?/.test(line)) {\n /** @type {string[]} */\n const q = []\n while (i < lines.length && /^\\s*>\\s?/.test(/** @type {string} */ (lines[i]))) {\n q.push((/** @type {string} */ (lines[i])).replace(/^\\s*>\\s?/, ''))\n i += 1\n }\n out.push('<blockquote>' + mdInline(mdEscape(q.join(' '))) + '</blockquote>')\n continue\n }\n if (/^\\s*[-*+]\\s+/.test(line)) {\n /** @type {string[]} */\n const lis = []\n while (i < lines.length && /^\\s*[-*+]\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*[-*+]\\s+/, ''))))\n i += 1\n }\n out.push('<ul>' + lis.map((x) => '<li>' + x + '</li>').join('') + '</ul>')\n continue\n }\n if (/^\\s*\\d+\\.\\s+/.test(line)) {\n /** @type {string[]} */\n const lis2 = []\n while (i < lines.length && /^\\s*\\d+\\.\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis2.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*\\d+\\.\\s+/, ''))))\n i += 1\n }\n out.push('<ol>' + lis2.map((x) => '<li>' + x + '</li>').join('') + '</ol>')\n continue\n }\n if (line.trim() === '') {\n i += 1\n continue\n }\n out.push('<p>' + mdInline(mdEscape(line)) + '</p>')\n i += 1\n }\n return out.join('\\n')\n}\n";
1023
+ var markdown_default = "/**\n * 共享:极简 Markdown → HTML 渲染器。\n *\n * 可移植性约束见 ext.ts 顶部说明:本文件经 `?raw` 原样内联进独立弹出页的\n * 经典 <script>,只能使用 JSDoc 标注类型。围栏代码块通过 shared/highlight\n * 的 highlightCode 高亮。\n */\nimport { highlightCode } from './highlight.js'\n\n/** @param {string} s @returns {string} */\nfunction mdEscape(s) {\n return String(s).replace(/[\\u0000\\u0001]/g, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\n/**\n * URL 白名单:只放行 http(s)、站内相对路径、页内锚点与 data:image/*,\n * 其余(javascript:、vbscript: 及实体编码变体等)一律替换为 '#',防止\n * 预览渲染出来的链接/图片在点击时执行脚本。\n * @param {string} u @returns {string}\n */\nfunction mdSafeUrl(u) {\n var s = String(u || '').replace(/[\\s\\u0000-\\u001f]/g, '')\n if (/^https?:\\/\\//i.test(s)) return s\n if (/^\\/(?!\\/)/.test(s) || /^\\.{1,2}\\//.test(s) || s.charAt(0) === '#') return s\n if (/^data:image\\/(?:png|gif|jpeg|webp|bmp|avif);/i.test(s)) return s\n return '#'\n}\n\n/** @param {string} s @returns {string} */\nfunction mdInline(s) {\n // code span 先摘出为占位符再跑后续规则:反引号内的 `[x](url)`、`**b**`\n // 应原样输出,不被行内链接/强调规则二次渲染(占位符字符已在 mdEscape 剥除)。\n /** @type {string[]} */\n const codes = []\n s = s.replace(/`([^`]+)`/g, (m, c) => {\n codes.push('<code>' + c + '</code>')\n return '\\u0000' + (codes.length - 1) + '\\u0000'\n })\n s = s.replace(/!\\[([^\\]]*)\\]\\(([^)\\s]+)\\)/g, (m, alt, u) => '<img alt=\"' + alt + '\" src=\"' + mdSafeUrl(u) + '\">')\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, (m, text, u) => '<a href=\"' + mdSafeUrl(u) + '\" target=\"_blank\" rel=\"noopener noreferrer\">' + text + '</a>')\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n s = s.replace(/\\*([^*\\n]+)\\*/g, '<em>$1</em>')\n s = s.replace(/\\u0000(\\d+)\\u0000/g, (m, i) => codes[Number(i)] ?? '')\n return s\n}\n\n/**\n * GFM 管道表格辅助:按未转义的 | 切分单元格(行内 \\| 还原为字面量竖线),\n * 并去掉首尾管道产生的空单元格。\n * @param {string} line @returns {string[]}\n */\nfunction mdSplitCells(line) {\n const parts = String(line).replace(/\\\\\\|/g, '\\u0002').split('|')\n for (let i = 0; i < parts.length; i += 1) {\n parts[i] = (parts[i] || '').replace(/\\u0002/g, '|').trim()\n }\n return parts\n}\n\n/** @param {string[]} cells @returns {string[]} */\nfunction mdTrimEdgeCells(cells) {\n if (cells.length && cells[0] === '') cells.shift()\n if (cells.length && cells[cells.length - 1] === '') cells.pop()\n return cells\n}\n\n/** @param {string[]} cells @returns {boolean} 是否为 GFM 分隔行(:--- / :---: / ---: 形态) */\nfunction mdIsDelimiterRow(cells) {\n return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c))\n}\n\n/**\n * @param {string} cell @returns {string} 'center' | 'right' | ''(left 与默认一致,不输出)\n */\nfunction mdCellAlign(cell) {\n if (/^:-+:$/.test(cell)) return 'center'\n if (/^:-+$/.test(cell)) return 'left'\n if (/^-+:$/.test(cell)) return 'right'\n return ''\n}\n\n/** @param {string} a @returns {string} 单元格对齐的内联样式属性 */\nfunction mdAlignAttr(a) {\n return a === 'center' || a === 'right' ? ' style=\"text-align:' + a + '\"' : ''\n}\n\n/** @param {string} src @returns {string} HTML */\nexport function mdToHtml(src) {\n const lines = String(src || '').replace(/\\r\\n/g, '\\n').split('\\n')\n /** @type {string[]} */\n const out = []\n let i = 0\n while (i < lines.length) {\n const line = /** @type {string} */ (lines[i])\n if (/^\\s*```/.test(line)) {\n const fence = /^\\s*```([\\w+-]*)/.exec(line)\n const langHint = fence ? fence[1] || '' : ''\n /** @type {string[]} */\n const buf = []\n i += 1\n while (i < lines.length && !/^\\s*```/.test(/** @type {string} */ (lines[i]))) {\n buf.push(/** @type {string} */ (lines[i]))\n i += 1\n }\n i += 1\n out.push('<pre><code>' + highlightCode(buf.join('\\n'), langHint) + '</code></pre>')\n continue\n }\n const h = /^(#{1,6})\\s+(.*)$/.exec(line)\n if (h) {\n const lv = (h[1] || '').length\n out.push('<h' + lv + '>' + mdInline(mdEscape(/** @type {string} */ (h[2]))) + '</h' + lv + '>')\n i += 1\n continue\n }\n if (/^\\s*(---+|\\*\\*\\*+|___+)\\s*$/.test(line)) {\n out.push('<hr>')\n i += 1\n continue\n }\n if (/^\\s*>\\s?/.test(line)) {\n /** @type {string[]} */\n const q = []\n while (i < lines.length && /^\\s*>\\s?/.test(/** @type {string} */ (lines[i]))) {\n q.push((/** @type {string} */ (lines[i])).replace(/^\\s*>\\s?/, ''))\n i += 1\n }\n out.push('<blockquote>' + mdInline(mdEscape(q.join(' '))) + '</blockquote>')\n continue\n }\n if (/^\\s*[-*+]\\s+/.test(line)) {\n /** @type {string[]} */\n const lis = []\n while (i < lines.length && /^\\s*[-*+]\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*[-*+]\\s+/, ''))))\n i += 1\n }\n out.push('<ul>' + lis.map((x) => '<li>' + x + '</li>').join('') + '</ul>')\n continue\n }\n if (/^\\s*\\d+\\.\\s+/.test(line)) {\n /** @type {string[]} */\n const lis2 = []\n while (i < lines.length && /^\\s*\\d+\\.\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis2.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*\\d+\\.\\s+/, ''))))\n i += 1\n }\n out.push('<ol>' + lis2.map((x) => '<li>' + x + '</li>').join('') + '</ol>')\n continue\n }\n if (line.trim() === '') {\n i += 1\n continue\n }\n // GFM 管道表格:表头行(含 |)+ 分隔行(:---/:---:/---:)+ 数据行,\n // 空行或不含 | 的行结束;列数以表头为准,数据行多删少补,\n // 单元格内容走 mdEscape + mdInline,对齐声明转成内联 text-align。\n if (line.indexOf('|') >= 0 && i + 1 < lines.length) {\n const head = mdTrimEdgeCells(mdSplitCells(line))\n const delim = mdTrimEdgeCells(mdSplitCells(/** @type {string} */ (lines[i + 1])))\n if (head.length && mdIsDelimiterRow(delim)) {\n const aligns = delim.map(mdCellAlign)\n const colCount = head.length\n /** @type {string[][]} */\n const rows = []\n i += 2\n while (i < lines.length && (/** @type {string} */ (lines[i])).trim() !== '' && (/** @type {string} */ (lines[i])).indexOf('|') >= 0) {\n const cells = mdTrimEdgeCells(mdSplitCells(/** @type {string} */ (lines[i])))\n /** @type {string[]} */\n const padded = []\n for (let c = 0; c < colCount; c += 1) padded.push(cells[c] || '')\n rows.push(padded)\n i += 1\n }\n let table = '<table><thead><tr>'\n for (let c = 0; c < colCount; c += 1) {\n table += '<th' + mdAlignAttr(aligns[c] || '') + '>' + mdInline(mdEscape(head[c] || '')) + '</th>'\n }\n table += '</tr></thead><tbody>'\n for (const cells of rows) {\n table += '<tr>'\n for (let c = 0; c < colCount; c += 1) {\n table += '<td' + mdAlignAttr(aligns[c] || '') + '>' + mdInline(mdEscape(cells[c] || '')) + '</td>'\n }\n table += '</tr>'\n }\n out.push(table + '</tbody></table>')\n continue\n }\n }\n out.push('<p>' + mdInline(mdEscape(line)) + '</p>')\n i += 1\n }\n return out.join('\\n')\n}\n";
1024
1024
  //#endregion
1025
1025
  //#region src/shared/i18n.js?raw
1026
- var i18n_default = "/**\n * 共享:中英双语 UI 文案。\n *\n * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注\n * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:\n * 1. tsdown 打包进 client 侧(浏览器 React bundle);\n * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar\n * 的经典 <script>(见 src/host/page.ts)。\n *\n * 语言选择:localStorage `dsh-flyout-sidebar:lang`('zh' | 'en',主面板的\n * 设置项写入、弹出页读取),未设置时按浏览器语言自动判定。`t(key)` 取当前\n * 语言文案,缺失时回退另一语言、再回退 key 本身。\n */\n\n/** @type {Record<string, string>} */\nconst ZH = {\n // 通用状态\n loading: '加载中…',\n loadingTree: '加载文件树…',\n loadingChanges: '加载变更列表…',\n loadingPdf: '加载 PDF…',\n searching: '搜索中…',\n emptyDir: '(空目录)',\n noChanges: '没有未提交的变更',\n noChangesHead: '没有未提交的变更(相对于 HEAD)',\n noResults: '没有匹配的文件',\n readFailed: '读取失败',\n retry: '重试',\n gitStatusFailed: 'git status 失败',\n imageLoadFailed: '图片加载失败',\n pdfLoadFailed: 'pdf.js 加载失败',\n searchFailed: '搜索失败',\n treeLoadFailed: '加载失败',\n truncated: '(已截断的预览)',\n // 复制 / 引用\n copied: '已复制',\n copiedPath: '已复制路径',\n copiedRef: '已复制 @引用(未能写入输入框)',\n insertedInput: '已插入输入框',\n refBtn: '@引用',\n refTitle: '引用到输入框(失败则复制 @path)',\n refFlyoutTitle: '复制 @path 引用',\n copyPath: '复制路径',\n refInput: '@引用到输入框',\n // 面板头部 / 控件\n collapsePanel: '收起侧边栏',\n panelAria: '文件面板',\n flyoutTitle: '弹出式侧边栏',\n flyoutOpen: '弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)',\n resizeHandle: '拖动调整宽度',\n resizePanel: '拖动调整面板宽度',\n refreshTree: '刷新文件树',\n refreshChanges: '刷新变更列表',\n refresh: '刷新',\n viewGit: '查看 Git 变更(未提交)',\n backToFiles: '返回文件列表',\n hidePreview: '隐藏预览(标签页保留)',\n closeTab: '关闭标签页',\n previewRegion: '文件预览',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: '搜索文件…',\n searchToggle: '搜索文件',\n hintClickGit: '点击右侧变更文件查看 diff',\n hintClickTree: '点击右侧文件查看内容',\n movePanelLeft: '将文件面板移到左侧',\n movePanelRight: '将文件面板移到右侧',\n // git 状态\n statusU: '未跟踪',\n statusA: '新增',\n statusM: '修改',\n statusD: '删除',\n statusR: '重命名',\n statusC: '复制',\n statusT: '类型变更',\n staged: '(已暂存)',\n unstaged: '(未暂存)',\n // diff / 预览控件\n diffDeleted: '- 删除',\n diffAdded: '+ 新增',\n zoomOut: '缩小',\n zoomIn: '放大',\n prevPage: '上一页',\n nextPage: '下一页',\n // 设置面板\n settingsIntro: '管理「Flyout Sidebar」的显示与行为。',\n setDefaultOpen: '默认展开',\n setDefaultOpenDesc: '页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。',\n setAutoRefresh: '自动刷新',\n setAutoRefreshDesc: '开启后侧边栏展开时将即时同步并更新产物列表',\n setFileTree: '文件树',\n setFileTreeDesc: '在侧边栏显示「文件树」标签页,浏览工作区目录。',\n setMinWidth: '最短面板宽度',\n setMinWidthDesc: '面板的最小宽度(占窗口宽度的百分比,20–60);更宽可通过拖动面板左边缘调整。',\n setCodeWrap: '代码换行',\n setCodeWrapDesc: '代码预览长行软换行;关闭则横向滚动。',\n wordWrap: '自动换行',\n openInEditor: '在系统编辑器打开',\n openedInEditor: '已在编辑器打开',\n openFailed: '打开失败',\n setLang: '界面语言',\n setLangDesc: '侧边栏与独立弹出页的显示语言;独立弹出页需刷新后生效。',\n langAuto: '跟随浏览器',\n langZh: '中文',\n langEn: 'English',\n}\n\n/** @type {Record<string, string>} */\nconst EN = {\n loading: 'Loading…',\n loadingTree: 'Loading file tree…',\n loadingChanges: 'Loading change list…',\n loadingPdf: 'Loading PDF…',\n searching: 'Searching…',\n emptyDir: '(empty directory)',\n noChanges: 'No uncommitted changes',\n noChangesHead: 'No uncommitted changes (relative to HEAD)',\n noResults: 'No matching files',\n readFailed: 'Failed to read',\n retry: 'Retry',\n gitStatusFailed: 'git status failed',\n imageLoadFailed: 'Failed to load image',\n pdfLoadFailed: 'Failed to load pdf.js',\n searchFailed: 'Search failed',\n treeLoadFailed: 'Failed to load',\n truncated: '(truncated preview)',\n copied: 'Copied',\n copiedPath: 'Path copied',\n copiedRef: 'Copied @reference (could not write to input box)',\n insertedInput: 'Inserted into input box',\n refBtn: '@ref',\n refTitle: 'Quote into input box (falls back to copying @path)',\n refFlyoutTitle: 'Copy @path reference',\n copyPath: 'Copy path',\n refInput: '@reference into input box',\n collapsePanel: 'Collapse sidebar',\n panelAria: 'File panel',\n flyoutTitle: 'Flyout sidebar',\n flyoutOpen: 'Flyout sidebar — open in a new tab (drag to another display)',\n resizeHandle: 'Drag to resize',\n resizePanel: 'Drag to resize panel',\n refreshTree: 'Refresh file tree',\n refreshChanges: 'Refresh change list',\n refresh: 'Refresh',\n viewGit: 'View Git changes (uncommitted)',\n backToFiles: 'Back to file list',\n hidePreview: 'Hide preview (tabs are kept)',\n closeTab: 'Close tab',\n previewRegion: 'File preview',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: 'Search files…',\n searchToggle: 'Search files',\n hintClickGit: 'Click a changed file to view its diff',\n hintClickTree: 'Click a file to view its content',\n movePanelLeft: 'Move file panel to the left',\n movePanelRight: 'Move file panel to the right',\n statusU: 'Untracked',\n statusA: 'Added',\n statusM: 'Modified',\n statusD: 'Deleted',\n statusR: 'Renamed',\n statusC: 'Copied',\n statusT: 'Type change',\n staged: '(staged)',\n unstaged: '(unstaged)',\n diffDeleted: '- Deleted',\n diffAdded: '+ Added',\n zoomOut: 'Zoom out',\n zoomIn: 'Zoom in',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n settingsIntro: 'Manage the display and behavior of \"Flyout Sidebar\".',\n setDefaultOpen: 'Open by default',\n setDefaultOpenDesc: 'Expand the sidebar on page load; when off it stays collapsed until the corner icon is clicked.',\n setAutoRefresh: 'Auto refresh',\n setAutoRefreshDesc: 'Keep the artifact list in sync while the sidebar is open',\n setFileTree: 'File tree',\n setFileTreeDesc: 'Show the \"File tree\" tab in the sidebar to browse the workspace directory.',\n setMinWidth: 'Minimum panel width',\n setMinWidthDesc: 'Minimum width of the panel (percentage of window width, 20–60); drag the panel edge to go wider.',\n setCodeWrap: 'Code wrap',\n setCodeWrapDesc: 'Soft-wrap long lines in code previews; when off they scroll horizontally.',\n wordWrap: 'Word wrap',\n openInEditor: 'Open in system editor',\n openedInEditor: 'Opened in editor',\n openFailed: 'Failed to open',\n setLang: 'Interface language',\n setLangDesc: 'Display language for the sidebar and the standalone flyout page (flyout requires a reload).',\n langAuto: 'Auto (browser)',\n langZh: '中文',\n langEn: 'English',\n}\n\n/** @type {Record<string, Record<string, string>>} */\nconst DICTS = { zh: ZH, en: EN }\n\nconst LANG_KEY = 'dsh-flyout-sidebar:lang'\n\n/** @type {'zh' | 'en' | null} 已显式选择的语言(null = 跟随浏览器自动判定) */\nlet explicitLang = null\n/** @type {Array<(lang: string) => void>} */\nlet listeners = []\n\nfunction readStoredLang() {\n try {\n var v = localStorage.getItem(LANG_KEY)\n return v === 'zh' || v === 'en' ? v : null\n } catch (e) {\n return null\n }\n}\n\n/** 浏览器语言自动判定:非 zh 开头一律英文 */\nfunction autoLang() {\n try {\n var langs = navigator.languages || (navigator.language ? [navigator.language] : [])\n for (var i = 0; i < langs.length; i++) {\n var l = String(langs[i] || '').toLowerCase()\n if (l.indexOf('zh') === 0) return 'zh'\n if (l.indexOf('en') === 0) return 'en'\n }\n } catch (e) {\n // navigator 不可用时按英文处理\n }\n return 'en'\n}\n\nfunction currentLang() {\n if (explicitLang === null) explicitLang = readStoredLang()\n return explicitLang || autoLang()\n}\n\n/**\n * @param {string} key 文案键\n * @returns {string} 当前语言文案;两级回退(另一语言 → key)\n */\nexport function t(key) {\n var lang = currentLang()\n var dict = DICTS[lang] || EN\n if (dict[key] != null) return dict[key]\n var other = DICTS[lang === 'zh' ? 'en' : 'zh']\n if (!other) return key\n return other[key] != null ? other[key] : key\n}\n\n/** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */\nexport function getLang() {\n return currentLang()\n}\n\n/** @returns {'zh' | 'en' | null} 用户显式选择的语言(null = 跟随浏览器) */\nexport function getExplicitLang() {\n if (explicitLang === null) explicitLang = readStoredLang()\n return explicitLang\n}\n\n/**\n * 显式设置语言并持久化(null = 恢复跟随浏览器)。\n * @param {'zh' | 'en' | null} lang\n */\nexport function setLang(lang) {\n explicitLang = lang\n try {\n if (lang === null) localStorage.removeItem(LANG_KEY)\n else localStorage.setItem(LANG_KEY, lang)\n } catch (e) {\n // localStorage 不可用时仅保存在内存\n }\n var next = listeners.slice()\n for (var i = 0; i < next.length; i++) {\n var fn = next[i]\n if (!fn) continue\n try {\n fn(currentLang())\n } catch (e) {\n // 单个订阅者异常不阻断其余\n }\n }\n}\n\n/** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */\nexport function subscribeLang(fn) {\n listeners.push(fn)\n return function () {\n listeners = listeners.filter(function (f) {\n return f !== fn\n })\n }\n}\n";
1026
+ var i18n_default = "/**\n * 共享:中英双语 UI 文案。\n *\n * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注\n * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:\n * 1. tsdown 打包进 client 侧(浏览器 React bundle);\n * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar\n * 的经典 <script>(见 src/host/page.ts)。\n *\n * 语言选择:localStorage `dsh-flyout-sidebar:lang`('zh' | 'en',主面板的\n * 设置项写入、弹出页读取),未设置时按浏览器语言自动判定。`t(key)` 取当前\n * 语言文案,缺失时回退另一语言、再回退 key 本身。\n */\n\n/** @type {Record<string, string>} */\nconst ZH = {\n // 通用状态\n loading: '加载中…',\n loadingTree: '加载文件树…',\n loadingChanges: '加载变更列表…',\n loadingPdf: '加载 PDF…',\n searching: '搜索中…',\n emptyDir: '(空目录)',\n noChanges: '没有未提交的变更',\n noChangesHead: '没有未提交的变更(相对于 HEAD)',\n noResults: '没有匹配的文件',\n readFailed: '读取失败',\n retry: '重试',\n gitStatusFailed: 'git status 失败',\n imageLoadFailed: '图片加载失败',\n pdfLoadFailed: 'pdf.js 加载失败',\n searchFailed: '搜索失败',\n treeLoadFailed: '加载失败',\n truncated: '(已截断的预览)',\n // 复制 / 引用\n copied: '已复制',\n copiedPath: '已复制路径',\n copiedRef: '已复制 @引用(未能写入输入框)',\n insertedInput: '已插入输入框',\n refBtn: '@引用',\n refTitle: '引用到输入框(失败则复制 @path)',\n refFlyoutTitle: '复制 @path 引用',\n copyPath: '复制路径',\n refInput: '@引用到输入框',\n // 面板头部 / 控件\n collapsePanel: '收起侧边栏',\n panelAria: '文件面板',\n flyoutTitle: '弹出式侧边栏',\n flyoutOpen: '弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)',\n resizeHandle: '拖动调整宽度',\n resizePanel: '拖动调整面板宽度',\n refreshTree: '刷新文件树',\n refreshChanges: '刷新变更列表',\n refresh: '刷新',\n viewGit: '查看 Git 变更(未提交)',\n backToFiles: '返回文件列表',\n hidePreview: '隐藏预览(标签页保留)',\n closeTab: '关闭标签页',\n closeOthers: '关闭其他',\n closeRight: '关闭右侧标签',\n previewRegion: '文件预览',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: '搜索文件…',\n searchToggle: '搜索文件',\n hintClickGit: '点击右侧变更文件查看 diff',\n hintClickTree: '点击右侧文件查看内容',\n movePanelLeft: '将文件面板移到左侧',\n movePanelRight: '将文件面板移到右侧',\n // git 状态\n statusU: '未跟踪',\n statusA: '新增',\n statusM: '修改',\n statusD: '删除',\n statusR: '重命名',\n statusC: '复制',\n statusT: '类型变更',\n staged: '(已暂存)',\n unstaged: '(未暂存)',\n // diff / 预览控件\n diffDeleted: '- 删除',\n diffAdded: '+ 新增',\n zoomOut: '缩小',\n zoomIn: '放大',\n prevPage: '上一页',\n nextPage: '下一页',\n // 设置面板\n settingsIntro: '管理「Flyout Sidebar」的显示与行为。',\n setDefaultOpen: '默认展开',\n setDefaultOpenDesc: '页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。',\n setAutoRefresh: '自动刷新',\n setAutoRefreshDesc: '开启后侧边栏展开时将即时同步并更新产物列表',\n setFileTree: '文件树',\n setFileTreeDesc: '在侧边栏显示「文件树」标签页,浏览工作区目录。',\n setMinWidth: '最短面板宽度',\n setMinWidthDesc: '面板的最小宽度(占窗口宽度的百分比,20–60);更宽可通过拖动面板左边缘调整。',\n setCodeWrap: '代码换行',\n setCodeWrapDesc: '代码预览长行软换行;关闭则横向滚动。',\n wordWrap: '自动换行',\n openInEditor: '在系统编辑器打开',\n openedInEditor: '已在编辑器打开',\n openFailed: '打开失败',\n setLang: '界面语言',\n setLangDesc: '侧边栏与独立弹出页的显示语言;独立弹出页需刷新后生效。',\n langAuto: '跟随浏览器',\n langZh: '中文',\n langEn: 'English',\n}\n\n/** @type {Record<string, string>} */\nconst EN = {\n loading: 'Loading…',\n loadingTree: 'Loading file tree…',\n loadingChanges: 'Loading change list…',\n loadingPdf: 'Loading PDF…',\n searching: 'Searching…',\n emptyDir: '(empty directory)',\n noChanges: 'No uncommitted changes',\n noChangesHead: 'No uncommitted changes (relative to HEAD)',\n noResults: 'No matching files',\n readFailed: 'Failed to read',\n retry: 'Retry',\n gitStatusFailed: 'git status failed',\n imageLoadFailed: 'Failed to load image',\n pdfLoadFailed: 'Failed to load pdf.js',\n searchFailed: 'Search failed',\n treeLoadFailed: 'Failed to load',\n truncated: '(truncated preview)',\n copied: 'Copied',\n copiedPath: 'Path copied',\n copiedRef: 'Copied @reference (could not write to input box)',\n insertedInput: 'Inserted into input box',\n refBtn: '@ref',\n refTitle: 'Quote into input box (falls back to copying @path)',\n refFlyoutTitle: 'Copy @path reference',\n copyPath: 'Copy path',\n refInput: '@reference into input box',\n collapsePanel: 'Collapse sidebar',\n panelAria: 'File panel',\n flyoutTitle: 'Flyout sidebar',\n flyoutOpen: 'Flyout sidebar — open in a new tab (drag to another display)',\n resizeHandle: 'Drag to resize',\n resizePanel: 'Drag to resize panel',\n refreshTree: 'Refresh file tree',\n refreshChanges: 'Refresh change list',\n refresh: 'Refresh',\n viewGit: 'View Git changes (uncommitted)',\n backToFiles: 'Back to file list',\n hidePreview: 'Hide preview (tabs are kept)',\n closeTab: 'Close tab',\n closeOthers: 'Close others',\n closeRight: 'Close tabs to the right',\n previewRegion: 'File preview',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: 'Search files…',\n searchToggle: 'Search files',\n hintClickGit: 'Click a changed file to view its diff',\n hintClickTree: 'Click a file to view its content',\n movePanelLeft: 'Move file panel to the left',\n movePanelRight: 'Move file panel to the right',\n statusU: 'Untracked',\n statusA: 'Added',\n statusM: 'Modified',\n statusD: 'Deleted',\n statusR: 'Renamed',\n statusC: 'Copied',\n statusT: 'Type change',\n staged: '(staged)',\n unstaged: '(unstaged)',\n diffDeleted: '- Deleted',\n diffAdded: '+ Added',\n zoomOut: 'Zoom out',\n zoomIn: 'Zoom in',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n settingsIntro: 'Manage the display and behavior of \"Flyout Sidebar\".',\n setDefaultOpen: 'Open by default',\n setDefaultOpenDesc: 'Expand the sidebar on page load; when off it stays collapsed until the corner icon is clicked.',\n setAutoRefresh: 'Auto refresh',\n setAutoRefreshDesc: 'Keep the artifact list in sync while the sidebar is open',\n setFileTree: 'File tree',\n setFileTreeDesc: 'Show the \"File tree\" tab in the sidebar to browse the workspace directory.',\n setMinWidth: 'Minimum panel width',\n setMinWidthDesc: 'Minimum width of the panel (percentage of window width, 20–60); drag the panel edge to go wider.',\n setCodeWrap: 'Code wrap',\n setCodeWrapDesc: 'Soft-wrap long lines in code previews; when off they scroll horizontally.',\n wordWrap: 'Word wrap',\n openInEditor: 'Open in system editor',\n openedInEditor: 'Opened in editor',\n openFailed: 'Failed to open',\n setLang: 'Interface language',\n setLangDesc: 'Display language for the sidebar and the standalone flyout page (flyout requires a reload).',\n langAuto: 'Auto (browser)',\n langZh: '中文',\n langEn: 'English',\n}\n\n/** @type {Record<string, Record<string, string>>} */\nconst DICTS = { zh: ZH, en: EN }\n\nconst LANG_KEY = 'dsh-flyout-sidebar:lang'\n\n/** @type {'zh' | 'en' | null} 已显式选择的语言(null = 跟随浏览器自动判定) */\nlet explicitLang = null\n/** @type {Array<(lang: string) => void>} */\nlet listeners = []\n\nfunction readStoredLang() {\n try {\n var v = localStorage.getItem(LANG_KEY)\n return v === 'zh' || v === 'en' ? v : null\n } catch (e) {\n return null\n }\n}\n\n/** 浏览器语言自动判定:非 zh 开头一律英文 */\nfunction autoLang() {\n try {\n var langs = navigator.languages || (navigator.language ? [navigator.language] : [])\n for (var i = 0; i < langs.length; i++) {\n var l = String(langs[i] || '').toLowerCase()\n if (l.indexOf('zh') === 0) return 'zh'\n if (l.indexOf('en') === 0) return 'en'\n }\n } catch (e) {\n // navigator 不可用时按英文处理\n }\n return 'en'\n}\n\nfunction currentLang() {\n if (explicitLang === null) explicitLang = readStoredLang()\n return explicitLang || autoLang()\n}\n\n/**\n * @param {string} key 文案键\n * @returns {string} 当前语言文案;两级回退(另一语言 → key)\n */\nexport function t(key) {\n var lang = currentLang()\n var dict = DICTS[lang] || EN\n if (dict[key] != null) return dict[key]\n var other = DICTS[lang === 'zh' ? 'en' : 'zh']\n if (!other) return key\n return other[key] != null ? other[key] : key\n}\n\n/** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */\nexport function getLang() {\n return currentLang()\n}\n\n/** @returns {'zh' | 'en' | null} 用户显式选择的语言(null = 跟随浏览器) */\nexport function getExplicitLang() {\n if (explicitLang === null) explicitLang = readStoredLang()\n return explicitLang\n}\n\n/**\n * 显式设置语言并持久化(null = 恢复跟随浏览器)。\n * @param {'zh' | 'en' | null} lang\n */\nexport function setLang(lang) {\n explicitLang = lang\n try {\n if (lang === null) localStorage.removeItem(LANG_KEY)\n else localStorage.setItem(LANG_KEY, lang)\n } catch (e) {\n // localStorage 不可用时仅保存在内存\n }\n var next = listeners.slice()\n for (var i = 0; i < next.length; i++) {\n var fn = next[i]\n if (!fn) continue\n try {\n fn(currentLang())\n } catch (e) {\n // 单个订阅者异常不阻断其余\n }\n }\n}\n\n/** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */\nexport function subscribeLang(fn) {\n listeners.push(fn)\n return function () {\n listeners = listeners.filter(function (f) {\n return f !== fn\n })\n }\n}\n";
1027
1027
  //#endregion
1028
1028
  //#region src/shared/gitui.js?raw
1029
1029
  var gitui_default = "/**\n * 共享:git 变更列表 / diff 的纯展示逻辑。\n *\n * client 侧 React 组件与独立弹出页(page.ts 内联脚本)共用,避免双端各自\n * 实现后细节漂移。与 ext/highlight/markdown 一样遵守「可移植源码」约束:\n * 只能使用 JSDoc 标注类型、依赖仅限本目录(i18n 的 t 在内联场景由同作用域\n * 函数声明提升提供)。\n */\nimport { t } from './i18n.js'\n\n/** @param {string} p @returns {string} */\nexport function basename(p) {\n var parts = String(p).split('/')\n return parts[parts.length - 1] || p\n}\n\n/**\n * 变更文件的状态徽章字母:未跟踪为 U,否则取工作树状态(y),再退到暂存\n * 状态(x),空缺按 M。\n * @param {{ x: string, y: string }} e @returns {string}\n */\nexport function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U'\n return (e.y !== ' ' ? e.y : e.x) || 'M'\n}\n\n/**\n * 变更文件行 hover 提示:状态名 + 暂存/未暂存。\n * @param {{ x: string, y: string }} e @returns {string}\n */\nexport function gitTitle(e) {\n var label = gitLabel(e)\n /** @type {Record<string, string>} */\n var map = {\n U: t('statusU'), A: t('statusA'), M: t('statusM'),\n D: t('statusD'), R: t('statusR'), C: t('statusC'), T: t('statusT'),\n }\n var staged = e.x !== ' ' && e.x !== '?'\n return (map[label] || label) + (staged ? t('staged') : t('unstaged'))\n}\n\n/**\n * unified diff 单行的样式类:hunk / 增 / 删 / 元信息(diff --git、index、\n * rename 等),其余为普通行。\n * @param {string} line @returns {string}\n */\nexport function gitDiffLineClass(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 (\n 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) === '\\\\'\n ) {\n cls += ' gd-meta'\n }\n return cls\n}\n";
@@ -1055,7 +1055,7 @@ const sharedScript = [
1055
1055
  ].map(toInlineScript).join("\n");
1056
1056
  if (/<\/script/i.test(sharedScript)) throw new Error("shared inline script must not contain a literal <\/script> sequence");
1057
1057
  function buildFlyoutPage() {
1058
- 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 // 语言:主面板设置项写入 localStorage(LANG_KEY),未设置时按浏览器语言。\n var LANG_KEY = 'dsh-flyout-sidebar:lang';\n var lang = null;\n try { lang = localStorage.getItem(LANG_KEY); } catch (e) {}\n if (lang !== 'zh' && lang !== 'en') {\n lang = (navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';\n }\n document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';\n document.title = lang === 'zh' ? '弹出式侧边栏' : 'Flyout Sidebar';\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 /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\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-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\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 .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\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\"></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=\"searchBtn\" 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=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\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 // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\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 SEARCH_URL = '/flyout-sidebar/search';\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 // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\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 treeError = null;\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 分支图标同风格\n function searchIcon() {\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 circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\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 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 // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\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', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\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 (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '−' + (e.dels || 0)));\n row.appendChild(stats);\n }\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 = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\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 // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\n$/, '').split('\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), 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 ? tr('diffTabPrefix') : '') + 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 = tr('closeTab');\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' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); 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) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\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) + sessionQuery() + '#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', tr('truncated')));\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) + sessionQuery()).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) || tr('readFailed') }); 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) || tr('readFailed') }); 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' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\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, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\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 treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\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, tr('copied'));\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', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\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', tr('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) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\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 // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\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 = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\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 = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\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 ? tr('movePanelRight') : tr('movePanelLeft');\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 (!document.hidden && 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 // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\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 readThemeColors();\n }\n readThemeColors();\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 // 语言:主面板设置项写入 localStorage(LANG_KEY),未设置时按浏览器语言。\n var LANG_KEY = 'dsh-flyout-sidebar:lang';\n var lang = null;\n try { lang = localStorage.getItem(LANG_KEY); } catch (e) {}\n if (lang !== 'zh' && lang !== 'en') {\n lang = (navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';\n }\n document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';\n document.title = lang === 'zh' ? '弹出式侧边栏' : 'Flyout Sidebar';\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 /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\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-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\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 .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\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\"></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=\"searchBtn\" 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=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\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 // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\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 SEARCH_URL = '/flyout-sidebar/search';\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 // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\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 treeError = null;\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 分支图标同风格\n function searchIcon() {\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 circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\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 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 // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\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', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\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 (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '\\u2212' + (e.dels || 0)));\n row.appendChild(stats);\n }\n if (e.origPath) row.appendChild(el('span', 'git-orig', '\\u2190 ' + 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 = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\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 // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\\n$/, '').split('\\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), 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 ? tr('diffTabPrefix') : '') + 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 = tr('closeTab');\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' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); 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) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\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) + sessionQuery() + '#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', tr('truncated')));\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) + sessionQuery()).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) || tr('readFailed') }); 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) || tr('readFailed') }); 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' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\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, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\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 treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\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, tr('copied'));\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', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\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', tr('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) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\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 // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\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 = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\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 = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\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 ? tr('movePanelRight') : tr('movePanelLeft');\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 (!document.hidden && 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 // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\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 readThemeColors();\n }\n readThemeColors();\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);
1058
+ 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 // 语言:主面板设置项写入 localStorage(LANG_KEY),未设置时按浏览器语言。\n var LANG_KEY = 'dsh-flyout-sidebar:lang';\n var lang = null;\n try { lang = localStorage.getItem(LANG_KEY); } catch (e) {}\n if (lang !== 'zh' && lang !== 'en') {\n lang = (navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';\n }\n document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';\n document.title = lang === 'zh' ? '弹出式侧边栏' : 'Flyout Sidebar';\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .ctx-menu-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; }\n .ctx-menu { position: fixed; z-index: 10000; min-width: 140px; padding: 4px 0; background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); border-radius: 8px; box-shadow: var(--p-shadow); font-size: 12px; }\n .ctx-menu-item { display: block; width: 100%; padding: 6px 14px; border: none; background: transparent; color: var(--p-text); text-align: left; cursor: pointer; font: inherit; }\n .ctx-menu-item:hover:not(:disabled) { background: var(--p-hover); }\n .ctx-menu-item:disabled { color: var(--p-text-caption); cursor: default; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 12px 0; }\n .markdown th, .markdown td { border: 1px solid var(--p-border-l2); padding: 4px 10px; text-align: left; }\n .markdown th { background: var(--p-code-bg); font-weight: 600; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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\"></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=\"searchBtn\" 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=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\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 // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\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 SEARCH_URL = '/flyout-sidebar/search';\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 // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\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 treeError = null;\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 分支图标同风格\n function searchIcon() {\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 circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\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 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 // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\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', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\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 (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '−' + (e.dels || 0)));\n row.appendChild(stats);\n }\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 = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\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 // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\n$/, '').split('\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), 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 ? tr('diffTabPrefix') : '') + 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 = tr('closeTab');\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n tab.addEventListener('contextmenu', function (ev) { showTabContextMenu(ev, 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 closeOthers(key) {\n var t = findTab(key);\n if (!t) return;\n tabs = [t];\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeRight(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 var removedActive = false;\n for (var j = tabs.length - 1; j > idx; j -= 1) {\n if (tabs[j].key === activeKey) removedActive = true;\n tabs.splice(j, 1);\n }\n if (removedActive) activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeContextMenu() {\n var m = document.getElementById('ctxMenu');\n if (m) m.remove();\n }\n function showTabContextMenu(ev, key) {\n ev.preventDefault();\n closeContextMenu();\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n var menu = el('div', 'ctx-menu');\n menu.id = 'ctxMenu';\n menu.style.left = ev.clientX + 'px';\n menu.style.top = ev.clientY + 'px';\n function addItem(label, fn, disabled) {\n var b = el('button', 'ctx-menu-item', label);\n b.type = 'button';\n if (disabled) { b.disabled = true; b.className += ' is-disabled'; }\n b.addEventListener('click', function () { closeContextMenu(); fn(); });\n menu.appendChild(b);\n }\n addItem(tr('closeTab'), function () { closeTab(key); }, false);\n addItem(tr('closeOthers'), function () { closeOthers(key); }, tabs.length <= 1);\n addItem(tr('closeRight'), function () { closeRight(key); }, idx >= tabs.length - 1);\n var backdrop = el('div', 'ctx-menu-backdrop');\n backdrop.addEventListener('click', closeContextMenu);\n backdrop.addEventListener('contextmenu', function (e) { e.preventDefault(); closeContextMenu(); });\n document.body.appendChild(backdrop);\n document.body.appendChild(menu);\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' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); 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) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\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) + sessionQuery() + '#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', tr('truncated')));\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) + sessionQuery()).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) || tr('readFailed') }); 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) || tr('readFailed') }); 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' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\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, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\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 treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\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, tr('copied'));\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', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\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', tr('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) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\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 // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\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 = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\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 = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\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 ? tr('movePanelRight') : tr('movePanelLeft');\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 (!document.hidden && 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 // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\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 readThemeColors();\n }\n readThemeColors();\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 // 语言:主面板设置项写入 localStorage(LANG_KEY),未设置时按浏览器语言。\n var LANG_KEY = 'dsh-flyout-sidebar:lang';\n var lang = null;\n try { lang = localStorage.getItem(LANG_KEY); } catch (e) {}\n if (lang !== 'zh' && lang !== 'en') {\n lang = (navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';\n }\n document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';\n document.title = lang === 'zh' ? '弹出式侧边栏' : 'Flyout Sidebar';\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .ctx-menu-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; }\n .ctx-menu { position: fixed; z-index: 10000; min-width: 140px; padding: 4px 0; background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); border-radius: 8px; box-shadow: var(--p-shadow); font-size: 12px; }\n .ctx-menu-item { display: block; width: 100%; padding: 6px 14px; border: none; background: transparent; color: var(--p-text); text-align: left; cursor: pointer; font: inherit; }\n .ctx-menu-item:hover:not(:disabled) { background: var(--p-hover); }\n .ctx-menu-item:disabled { color: var(--p-text-caption); cursor: default; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 12px 0; }\n .markdown th, .markdown td { border: 1px solid var(--p-border-l2); padding: 4px 10px; text-align: left; }\n .markdown th { background: var(--p-code-bg); font-weight: 600; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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 .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", 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\"></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=\"searchBtn\" 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=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\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 // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\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 SEARCH_URL = '/flyout-sidebar/search';\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 // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\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 treeError = null;\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 分支图标同风格\n function searchIcon() {\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 circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\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 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 // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\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', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\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 (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '\\u2212' + (e.dels || 0)));\n row.appendChild(stats);\n }\n if (e.origPath) row.appendChild(el('span', 'git-orig', '\\u2190 ' + 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 = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\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 // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\\n$/, '').split('\\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), 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 ? tr('diffTabPrefix') : '') + 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 = tr('closeTab');\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n tab.addEventListener('contextmenu', function (ev) { showTabContextMenu(ev, 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 closeOthers(key) {\n var t = findTab(key);\n if (!t) return;\n tabs = [t];\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeRight(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 var removedActive = false;\n for (var j = tabs.length - 1; j > idx; j -= 1) {\n if (tabs[j].key === activeKey) removedActive = true;\n tabs.splice(j, 1);\n }\n if (removedActive) activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeContextMenu() {\n var m = document.getElementById('ctxMenu');\n if (m) m.remove();\n }\n function showTabContextMenu(ev, key) {\n ev.preventDefault();\n closeContextMenu();\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n var menu = el('div', 'ctx-menu');\n menu.id = 'ctxMenu';\n menu.style.left = ev.clientX + 'px';\n menu.style.top = ev.clientY + 'px';\n function addItem(label, fn, disabled) {\n var b = el('button', 'ctx-menu-item', label);\n b.type = 'button';\n if (disabled) { b.disabled = true; b.className += ' is-disabled'; }\n b.addEventListener('click', function () { closeContextMenu(); fn(); });\n menu.appendChild(b);\n }\n addItem(tr('closeTab'), function () { closeTab(key); }, false);\n addItem(tr('closeOthers'), function () { closeOthers(key); }, tabs.length <= 1);\n addItem(tr('closeRight'), function () { closeRight(key); }, idx >= tabs.length - 1);\n var backdrop = el('div', 'ctx-menu-backdrop');\n backdrop.addEventListener('click', closeContextMenu);\n backdrop.addEventListener('contextmenu', function (e) { e.preventDefault(); closeContextMenu(); });\n document.body.appendChild(backdrop);\n document.body.appendChild(menu);\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' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); 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) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\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) + sessionQuery() + '#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', tr('truncated')));\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) + sessionQuery()).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) || tr('readFailed') }); 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) || tr('readFailed') }); 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' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\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, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\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 treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\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, tr('copied'));\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', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\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', tr('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) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\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 // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\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 = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\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 = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\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 ? tr('movePanelRight') : tr('movePanelLeft');\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 (!document.hidden && 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 // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\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 readThemeColors();\n }\n readThemeColors();\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);
1059
1059
  }
1060
1060
  //#endregion
1061
1061
  //#region src/host/routes.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-flyout-sidebar",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "可弹出侧边栏 · A DeepSeek Harness sidebar that shows artifacts and pops out into a larger web tab.",
5
5
  "license": "MIT",
6
6
  "type": "module",