dsh-flyout-sidebar 0.1.4 → 0.1.6

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,7 +233,7 @@ 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%; }
@@ -248,7 +248,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
248
248
  .artifacts-diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }
249
249
  .artifacts-diff-del .artifacts-diff-label { color: var(--dsw-alias-state-error-primary); background: rgba(236,19,19,0.06); }
250
250
  .artifacts-diff-add .artifacts-diff-label { color: var(--dsw-alias-state-success-primary); background: rgba(34,197,94,0.08); }
251
- .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); }
252
252
  .artifacts-diff-del .artifacts-diff-pre { background: rgba(236,19,19,0.05); }
253
253
  .artifacts-diff-add .artifacts-diff-pre { background: rgba(34,197,94,0.06); }
254
254
  .artifacts-resize { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 3; touch-action: none; }
@@ -257,7 +257,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
257
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; }
258
258
  .artifacts-viewbtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
259
259
  .artifacts-viewbtn.is-active { color: var(--dsw-alias-state-business-primary); }
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, 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); }
261
261
  .artifacts-git-badge-M { background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); }
262
262
  .artifacts-git-badge-A { background: var(--dsw-alias-state-success-tertiary); color: var(--dsw-alias-state-success-primary); }
263
263
  .artifacts-git-badge-D { background: rgba(236,19,19,0.1); color: var(--dsw-alias-state-error-primary); }
@@ -265,7 +265,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
265
265
  .artifacts-git-badge-U { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-tertiary); }
266
266
  .artifacts-git-orig { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
267
267
  .artifacts-git-error { padding: 14px 12px; word-break: break-all; }
268
- .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; }
269
269
  .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }
270
270
  .gd-meta { color: var(--dsw-alias-label-tertiary); background: var(--dsw-alias-bg-layer-1); padding: 2px 12px; }
271
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; }
@@ -321,18 +321,23 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
321
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; }
322
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; }
323
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; }
324
329
  .artifacts-code { display: flex; flex-direction: column; height: 100%; min-height: 0; }
325
330
  /* 代码视图:逐行渲染(行 = 行号 + 代码),软换行时行号仍对齐 */
326
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))); }
327
332
  .artifacts-code-line { display: flex; align-items: flex-start; min-width: fit-content; }
328
- .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); }
329
- .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; }
330
335
  .artifacts-code-pre code { font: inherit; color: inherit; }
331
336
  .artifacts-code-wrap .artifacts-code-pre { white-space: pre-wrap; word-break: break-all; }
332
337
  .artifacts-imgview { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: grab; user-select: none; }
333
338
  .artifacts-imgview.is-dragging { cursor: grabbing; }
334
339
  .artifacts-imgview .artifacts-img { flex: none; margin: 16px; transform-origin: center; will-change: transform; }
335
- .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); }
336
341
  .artifacts-git-adds { color: var(--p-success-fg, #1a7f37); }
337
342
  .artifacts-git-dels { color: var(--p-error, #cf222e); }
338
343
  .tok-comment { color: #868e96; }
@@ -474,6 +479,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
474
479
  backToFiles: "返回文件列表",
475
480
  hidePreview: "隐藏预览(标签页保留)",
476
481
  closeTab: "关闭标签页",
482
+ closeOthers: "关闭其他",
483
+ closeRight: "关闭右侧标签",
477
484
  previewRegion: "文件预览",
478
485
  diffTabPrefix: "[diff] ",
479
486
  searchPlaceholder: "搜索文件…",
@@ -559,6 +566,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
559
566
  backToFiles: "Back to file list",
560
567
  hidePreview: "Hide preview (tabs are kept)",
561
568
  closeTab: "Close tab",
569
+ closeOthers: "Close others",
570
+ closeRight: "Close tabs to the right",
562
571
  previewRegion: "File preview",
563
572
  diffTabPrefix: "[diff] ",
564
573
  searchPlaceholder: "Search files…",
@@ -2274,6 +2283,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2274
2283
  return remaining[Math.min(Math.max(closedIdx, 0), remaining.length - 1)]?.key ?? null;
2275
2284
  });
2276
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
+ };
2277
2298
  const tabsRef = React.useRef([]);
2278
2299
  React.useEffect(() => {
2279
2300
  tabsRef.current = tabs;
@@ -2352,7 +2373,9 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2352
2373
  setPreviewHidden,
2353
2374
  openFile,
2354
2375
  openGitDiff,
2355
- closeTab
2376
+ closeTab,
2377
+ closeOthers,
2378
+ closeRight
2356
2379
  };
2357
2380
  }
2358
2381
  /** git 变更列表(文件面板的「变更」视图) */
@@ -2402,9 +2425,47 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2402
2425
  const settings = useSettings();
2403
2426
  useLang();
2404
2427
  const sessionId = useSessionId();
2405
- 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);
2406
2429
  const activeTab = tabs.find((t) => t.key === activeKey) || null;
2407
2430
  const [notice, setNotice] = React.useState("");
2431
+ const [ctxMenu, setCtxMenu] = React.useState(null);
2432
+ React.useEffect(() => {
2433
+ if (!ctxMenu) return;
2434
+ const idx = tabs.findIndex((tb) => tb.key === ctxMenu.key);
2435
+ const menu = document.createElement("div");
2436
+ menu.className = "artifacts-ctxmenu";
2437
+ menu.style.left = ctxMenu.x + "px";
2438
+ menu.style.top = ctxMenu.y + "px";
2439
+ const close = () => setCtxMenu(null);
2440
+ const addItem = (label, fn, disabled) => {
2441
+ const b = document.createElement("button");
2442
+ b.type = "button";
2443
+ b.className = "artifacts-ctxmenu-item";
2444
+ b.textContent = label;
2445
+ if (disabled) b.disabled = true;
2446
+ b.addEventListener("click", () => {
2447
+ close();
2448
+ fn();
2449
+ });
2450
+ menu.appendChild(b);
2451
+ };
2452
+ addItem(t("closeTab"), () => closeTab(ctxMenu.key), false);
2453
+ addItem(t("closeOthers"), () => closeOthers(ctxMenu.key), tabs.length <= 1);
2454
+ addItem(t("closeRight"), () => closeRight(ctxMenu.key), idx >= tabs.length - 1);
2455
+ const backdrop = document.createElement("div");
2456
+ backdrop.className = "artifacts-ctxmenu-backdrop";
2457
+ backdrop.addEventListener("click", close);
2458
+ backdrop.addEventListener("contextmenu", (e) => {
2459
+ e.preventDefault();
2460
+ close();
2461
+ });
2462
+ document.body.appendChild(backdrop);
2463
+ document.body.appendChild(menu);
2464
+ return () => {
2465
+ menu.remove();
2466
+ backdrop.remove();
2467
+ };
2468
+ }, [ctxMenu]);
2408
2469
  const [winW, setWinW] = React.useState(() => typeof window !== "undefined" ? window.innerWidth : 1400);
2409
2470
  const [gitFiles, setGitFiles] = React.useState(null);
2410
2471
  const [gitError, setGitError] = React.useState(null);
@@ -2610,7 +2671,15 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2610
2671
  key: tab.key,
2611
2672
  className: "artifacts-ptab" + (tab.key === activeKey ? " is-active" : ""),
2612
2673
  title: (tab.git ? t("diffTabPrefix") : "") + (tab.path || ""),
2613
- onClick: () => setActiveKey(tab.key)
2674
+ onClick: () => setActiveKey(tab.key),
2675
+ onContextMenu: (e) => {
2676
+ e.preventDefault();
2677
+ setCtxMenu({
2678
+ x: e.clientX,
2679
+ y: e.clientY,
2680
+ key: tab.key
2681
+ });
2682
+ }
2614
2683
  }, /* @__PURE__ */ h("span", { className: "artifacts-ptab-name" }, basename(tab.path || "")), /* @__PURE__ */ h("button", {
2615
2684
  type: "button",
2616
2685
  className: "artifacts-ptab-close",
package/dist/index.js CHANGED
@@ -768,10 +768,9 @@ function isWithinWorkspace(fs, target, cwd) {
768
768
  if (!cwd) return true;
769
769
  const norm = (p) => String(typeof fs?.processPath === "function" ? fs.processPath(p) : p).replace(/\\/g, "/").replace(/\/+$/, "");
770
770
  const root = norm(cwd);
771
- if (!root) return true;
771
+ if (!root || root === "undefined") return true;
772
772
  const forms = /* @__PURE__ */ new Set([String(target), norm(target)]);
773
773
  for (const t of forms) if (t === root || t.startsWith(root + "/")) return true;
774
- console.warn("[flyout-sidebar] resolved path outside workspace root, allowing (fs backend anchoring trusted):", forms, "root:", root);
775
774
  return true;
776
775
  }
777
776
  /** 文本内容读取(代码预览)。超长内容截断并打标。cwdHint 允许调用方直传
@@ -1023,7 +1022,7 @@ var highlight_default = "/**\n * 共享:零依赖语法高亮器(无模板
1023
1022
  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
1023
  //#endregion
1025
1024
  //#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";
1025
+ 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
1026
  //#endregion
1028
1027
  //#region src/shared/gitui.js?raw
1029
1028
  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 +1054,7 @@ const sharedScript = [
1055
1054
  ].map(toInlineScript).join("\n");
1056
1055
  if (/<\/script/i.test(sharedScript)) throw new Error("shared inline script must not contain a literal <\/script> sequence");
1057
1056
  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 .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, 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 .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, 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);
1057
+ 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 var TREE_EXPANDED_KEY = 'dsh-flyout-sidebar:tree-expanded';\n function loadTreeExpanded() {\n try {\n var v = JSON.parse(localStorage.getItem(TREE_EXPANDED_KEY) || '{}');\n if (v && typeof v === 'object') return v;\n } catch (e) {}\n return {};\n }\n function saveTreeExpanded() {\n try { localStorage.setItem(TREE_EXPANDED_KEY, JSON.stringify(treeExpanded)); } catch (e) {}\n }\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 = loadTreeExpanded();\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 var b = document.getElementById('ctxMenuBackdrop');\n if (b) b.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.id = 'ctxMenuBackdrop';\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 // 重载树时保留用户展开的目录,而不是全部折叠\n treeExpanded = loadTreeExpanded();\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 saveTreeExpanded();\n return;\n }\n treeExpanded[path] = true;\n saveTreeExpanded();\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 var TREE_EXPANDED_KEY = 'dsh-flyout-sidebar:tree-expanded';\n function loadTreeExpanded() {\n try {\n var v = JSON.parse(localStorage.getItem(TREE_EXPANDED_KEY) || '{}');\n if (v && typeof v === 'object') return v;\n } catch (e) {}\n return {};\n }\n function saveTreeExpanded() {\n try { localStorage.setItem(TREE_EXPANDED_KEY, JSON.stringify(treeExpanded)); } catch (e) {}\n }\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 = loadTreeExpanded();\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 var b = document.getElementById('ctxMenuBackdrop');\n if (b) b.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.id = 'ctxMenuBackdrop';\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 // 重载树时保留用户展开的目录,而不是全部折叠\n treeExpanded = loadTreeExpanded();\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 saveTreeExpanded();\n return;\n }\n treeExpanded[path] = true;\n saveTreeExpanded();\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
1058
  }
1060
1059
  //#endregion
1061
1060
  //#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.4",
3
+ "version": "0.1.6",
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",