dsh-flyout-sidebar 0.1.5 → 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
@@ -2429,6 +2429,43 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2429
2429
  const activeTab = tabs.find((t) => t.key === activeKey) || null;
2430
2430
  const [notice, setNotice] = React.useState("");
2431
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]);
2432
2469
  const [winW, setWinW] = React.useState(() => typeof window !== "undefined" ? window.innerWidth : 1400);
2433
2470
  const [gitFiles, setGitFiles] = React.useState(null);
2434
2471
  const [gitError, setGitError] = React.useState(null);
@@ -2656,43 +2693,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2656
2693
  className: "artifacts-preview-hide",
2657
2694
  title: t("hidePreview"),
2658
2695
  onClick: () => setPreviewHidden(true)
2659
- }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null, ctxMenu ? /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h("div", {
2660
- className: "artifacts-ctxmenu-backdrop",
2661
- onClick: () => setCtxMenu(null),
2662
- onContextMenu: (e) => {
2663
- e.preventDefault();
2664
- setCtxMenu(null);
2665
- }
2666
- }), /* @__PURE__ */ h("div", {
2667
- className: "artifacts-ctxmenu",
2668
- style: {
2669
- left: ctxMenu.x,
2670
- top: ctxMenu.y
2671
- }
2672
- }, /* @__PURE__ */ h("button", {
2673
- type: "button",
2674
- className: "artifacts-ctxmenu-item",
2675
- onClick: () => {
2676
- closeTab(ctxMenu.key);
2677
- setCtxMenu(null);
2678
- }
2679
- }, t("closeTab")), /* @__PURE__ */ h("button", {
2680
- type: "button",
2681
- className: "artifacts-ctxmenu-item",
2682
- onClick: () => {
2683
- closeOthers(ctxMenu.key);
2684
- setCtxMenu(null);
2685
- },
2686
- disabled: tabs.length <= 1
2687
- }, t("closeOthers")), /* @__PURE__ */ h("button", {
2688
- type: "button",
2689
- className: "artifacts-ctxmenu-item",
2690
- onClick: () => {
2691
- closeRight(ctxMenu.key);
2692
- setCtxMenu(null);
2693
- },
2694
- disabled: tabs.findIndex((tb) => tb.key === ctxMenu.key) >= tabs.length - 1
2695
- }, t("closeRight")))) : null) : null;
2696
+ }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null) : null;
2696
2697
  return /* @__PURE__ */ h(Fragment, null, previewOverlay, /* @__PURE__ */ h("div", {
2697
2698
  className: "artifacts-panel" + (slidOut ? " artifacts-slid-out" : "") + (resizing ? " artifacts-resizing" : ""),
2698
2699
  style: { width: widthPx },
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 允许调用方直传
@@ -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, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .ctx-menu-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; }\n .ctx-menu { position: fixed; z-index: 10000; min-width: 140px; padding: 4px 0; background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); border-radius: 8px; box-shadow: var(--p-shadow); font-size: 12px; }\n .ctx-menu-item { display: block; width: 100%; padding: 6px 14px; border: none; background: transparent; color: var(--p-text); text-align: left; cursor: pointer; font: inherit; }\n .ctx-menu-item:hover:not(:disabled) { background: var(--p-hover); }\n .ctx-menu-item:disabled { color: var(--p-text-caption); cursor: default; }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 12px 0; }\n .markdown th, .markdown td { border: 1px solid var(--p-border-l2); padding: 4px 10px; text-align: left; }\n .markdown th { background: var(--p-code-bg); font-weight: 600; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--p-text-tertiary); }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\"></div>\n <div class=\"gtoggle\">\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"searchBtn\" type=\"button\" title=\"搜索文件\"></button>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var SEARCH_URL = '/flyout-sidebar/search';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\n }\n var gitFiles = null;\n var gitError = null;\n var gitSig = null; // 上次渲染的变更签名;轮询数据未变时跳过重渲染,避免冲掉刷新动画\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var treeError = null;\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // 放大镜(描边圆 + 柄),与 git 分支图标同风格\n function searchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '−' + (e.dels || 0)));\n row.appendChild(stats);\n }\n if (e.origPath) row.appendChild(el('span', 'git-orig', '← ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\n$/, '').split('\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? tr('diffTabPrefix') : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = tr('closeTab');\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n tab.addEventListener('contextmenu', function (ev) { showTabContextMenu(ev, t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeOthers(key) {\n var t = findTab(key);\n if (!t) return;\n tabs = [t];\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeRight(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n var removedActive = false;\n for (var j = tabs.length - 1; j > idx; j -= 1) {\n if (tabs[j].key === activeKey) removedActive = true;\n tabs.splice(j, 1);\n }\n if (removedActive) activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeContextMenu() {\n var m = document.getElementById('ctxMenu');\n if (m) m.remove();\n }\n function showTabContextMenu(ev, key) {\n ev.preventDefault();\n closeContextMenu();\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n var menu = el('div', 'ctx-menu');\n menu.id = 'ctxMenu';\n menu.style.left = ev.clientX + 'px';\n menu.style.top = ev.clientY + 'px';\n function addItem(label, fn, disabled) {\n var b = el('button', 'ctx-menu-item', label);\n b.type = 'button';\n if (disabled) { b.disabled = true; b.className += ' is-disabled'; }\n b.addEventListener('click', function () { closeContextMenu(); fn(); });\n menu.appendChild(b);\n }\n addItem(tr('closeTab'), function () { closeTab(key); }, false);\n addItem(tr('closeOthers'), function () { closeOthers(key); }, tabs.length <= 1);\n addItem(tr('closeRight'), function () { closeRight(key); }, idx >= tabs.length - 1);\n var backdrop = el('div', 'ctx-menu-backdrop');\n backdrop.addEventListener('click', closeContextMenu);\n backdrop.addEventListener('contextmenu', function (e) { e.preventDefault(); closeContextMenu(); });\n document.body.appendChild(backdrop);\n document.body.appendChild(menu);\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\n$/, '').split('\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + sessionQuery() + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', tr('truncated')));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path) + sessionQuery()).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || tr('readFailed') }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || tr('readFailed') }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, tr('copied'));\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', tr('loading'));\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n // force=1 绕过 host 的 stale-while-revalidate 缓存,由刷新按钮使用。\n // 刷新后给列表行加交错浮现动画(从上往下);selector 区分 git 列表与文件树。\n function staggerList(list, selector) {\n var items = list.querySelectorAll(selector || '.item');\n for (var i = 0; i < items.length; i++) {\n var it = items[i];\n it.classList.remove('flash-in');\n void it.offsetWidth; // 重置动画,使连续刷新也能重放\n it.classList.add('flash-in');\n it.style.animationDelay = (Math.min(i, 12) * 45) + 'ms';\n }\n }\n function loadGit(force) {\n var list = document.getElementById('list');\n // 强制刷新时列表短暂变暗,响应返回后恢复,给出「刷新过了」的可见反馈。\n if (force && list) list.classList.add('is-refreshing');\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || '') + (force ? '&force=1' : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n var ok = data && data.ok === true;\n if (ok) {\n st.textContent = 'live';\n st.style.color = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\n var sig = JSON.stringify([nextError, nextFiles]);\n if (!force && sig === gitSig) return;\n gitSig = sig;\n gitFiles = nextFiles;\n gitError = nextError;\n renderGit();\n if (force && list) staggerList(list);\n }).catch(function () {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the left (default) or the right ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = true;\n try { var _savedSide = localStorage.getItem(PANEL_SIDE_KEY); if (_savedSide === '0') panelLeft = false; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? tr('movePanelRight') : tr('movePanelLeft');\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (!document.hidden && currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n readThemeColors();\n }\n readThemeColors();\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"], ["<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>弹出式侧边栏</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2250.000000%22%20height%3D%2250.000000%22%20viewBox%3D%220%200%2050%2050%22%20fill%3D%22none%22%3E%0A%09%3ClinearGradient%20id%3D%22wg%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%232a7fbf%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231b9a6b%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20id%3D%22path%22%20d%3D%22M48.8354%2010.0479C48.3232%209.79199%2048.1025%2010.2798%2047.8032%2010.5278C47.7007%2010.6079%2047.6143%2010.7119%2047.5273%2010.8076C46.7793%2011.624%2045.9048%2012.1597%2044.7622%2012.0957C43.0923%2012%2041.666%2012.5356%2040.4058%2013.8398C40.1377%2012.2319%2039.2476%2011.272%2037.8926%2010.6558C37.1836%2010.3359%2036.4668%2010.0156%2035.9702%209.31982C35.6235%208.82373%2035.5293%208.27197%2035.356%207.72754C35.2456%207.3999%2035.1353%207.06396%2034.7651%207.00781C34.3633%206.94385%2034.2056%207.2876%2034.0479%207.57568C33.418%208.75195%2033.1733%2010.0479%2033.1973%2011.3599C33.2524%2014.312%2034.4736%2016.6641%2036.8999%2018.3359C37.1758%2018.5278%2037.2466%2018.7197%2037.1597%2019C36.9946%2019.5757%2036.7974%2020.1357%2036.624%2020.7119C36.5137%2021.0801%2036.3486%2021.1597%2035.9624%2021C34.6309%2020.4321%2033.481%2019.5918%2032.4644%2018.5757C30.7393%2016.8721%2029.1792%2014.9917%2027.2334%2013.52C26.7764%2013.1758%2026.3193%2012.856%2025.8467%2012.5518C23.8618%2010.584%2026.1069%208.96777%2026.627%208.77588C27.1704%208.57568%2026.8159%207.8877%2025.0591%207.896C23.3022%207.90381%2021.6953%208.50391%2019.647%209.30371C19.3477%209.42383%2019.0322%209.51172%2018.7095%209.58398C16.8501%209.22363%2014.9199%209.14355%2012.9033%209.37598C9.10596%209.80762%206.07275%2011.6396%203.84326%2014.7681C1.16455%2018.5278%200.53418%2022.7998%201.30664%2027.2559C2.11768%2031.9521%204.46582%2035.8398%208.07373%2038.8799C11.8159%2042.0322%2016.1255%2043.5762%2021.041%2043.2803C24.0269%2043.104%2027.3516%2042.6963%2031.1016%2039.4561C32.0469%2039.936%2033.0396%2040.1279%2034.686%2040.272C35.9546%2040.3921%2037.1758%2040.208%2038.1211%2040.0078C39.6021%2039.688%2039.4995%2038.2881%2038.9639%2038.0322C34.623%2035.9678%2035.5762%2036.8081%2034.71%2036.1279C36.9155%2033.4639%2040.2402%2030.6958%2041.54%2021.728C41.6426%2021.0161%2041.5557%2020.5679%2041.54%2019.9917C41.5322%2019.6396%2041.6108%2019.5039%2042.0049%2019.4639C43.0923%2019.3359%2044.1479%2019.0317%2045.1167%2018.4878C47.9292%2016.9199%2049.064%2014.3438%2049.3315%2011.2559C49.3711%2010.7837%2049.3237%2010.2959%2048.8354%2010.0479ZM24.3262%2037.8398C20.1196%2034.4639%2018.0791%2033.3521%2017.2358%2033.3999C16.4482%2033.4482%2016.5898%2034.3682%2016.7632%2034.9678C16.9443%2035.5601%2017.1812%2035.9683%2017.5117%2036.4878C17.7402%2036.832%2017.8979%2037.3442%2017.2832%2037.728C15.9282%2038.584%2013.5728%2037.4399%2013.4624%2037.3838C10.7207%2035.7358%208.42822%2033.5601%206.81348%2030.584C5.25342%2027.7197%204.34766%2024.6479%204.19775%2021.3677C4.1582%2020.5757%204.38672%2020.2959%205.15869%2020.1519C6.17529%2019.96%207.22314%2019.9199%208.23926%2020.0718C12.5327%2020.7119%2016.1885%2022.6719%2019.2529%2025.7759C21.002%2027.5439%2022.3252%2029.6558%2023.6885%2031.7202C25.1377%2033.9121%2026.6978%2036%2028.6831%2037.7119C29.3843%2038.312%2029.9434%2038.7681%2030.479%2039.104C28.8643%2039.2881%2026.1699%2039.3281%2024.3262%2037.8398ZM26.3433%2024.6001C26.3433%2024.248%2026.6191%2023.9678%2026.9658%2023.9678C27.0444%2023.9678%2027.1152%2023.9839%2027.1782%2024.0078C27.2651%2024.04%2027.3438%2024.0879%2027.4067%2024.1602C27.5171%2024.272%2027.5801%2024.4321%2027.5801%2024.6001C27.5801%2024.9521%2027.3042%2025.2319%2026.9575%2025.2319C26.6108%2025.2319%2026.3433%2024.9521%2026.3433%2024.6001ZM32.6064%2027.8799C32.2046%2028.0479%2031.8027%2028.1919%2031.4165%2028.208C30.8179%2028.2397%2030.1641%2027.9922%2029.8096%2027.688C29.2583%2027.2158%2028.8643%2026.9521%2028.6987%2026.1279C28.6279%2025.7759%2028.6675%2025.2319%2028.7305%2024.9199C28.8721%2024.248%2028.7144%2023.8159%2028.2495%2023.4238C27.8716%2023.104%2027.3911%2023.0161%2026.8633%2023.0161C26.666%2023.0161%2026.4849%2022.9277%2026.3511%2022.856C26.1304%2022.7441%2025.9492%2022.4639%2026.1226%2022.1201C26.1777%2022.0078%2026.4458%2021.7358%2026.5088%2021.688C27.2256%2021.272%2028.0527%2021.4077%2028.8169%2021.7197C29.5259%2022.0161%2030.0615%2022.5601%2030.834%2023.3281C31.6216%2024.2559%2031.7632%2024.5117%2032.2124%2025.208C32.5669%2025.752%2032.8901%2026.312%2033.1104%2026.9521C33.2446%2027.3521%2033.0713%2027.6802%2032.6064%2027.8799Z%22%20fill%3D%22url(%23wg)%22%20fill-opacity%3D%221.000000%22%20fill-rule%3D%22nonzero%22%2F%3E%0A%3C%2Fsvg%3E%0A\" />\n<script>\n (function () {\n // Follow DSH's light/dark setting: the main tab publishes the theme to\n // localStorage (THEME_KEY); the ?scheme= query is the fallback.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n // 语言:主面板设置项写入 localStorage(LANG_KEY),未设置时按浏览器语言。\n var LANG_KEY = 'dsh-flyout-sidebar:lang';\n var lang = null;\n try { lang = localStorage.getItem(LANG_KEY); } catch (e) {}\n if (lang !== 'zh' && lang !== 'en') {\n lang = (navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';\n }\n document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';\n document.title = lang === 'zh' ? '弹出式侧边栏' : 'Flyout Sidebar';\n })();\n<\/script>\n<style>\n :root {\n color-scheme: light;\n --p-bg: rgb(255, 255, 255);\n --p-bg-layer-1: rgb(255, 255, 255);\n --p-border-l1: rgba(0, 0, 0, 0.04);\n --p-border-l2: rgba(0, 0, 0, 0.1);\n --p-text: rgb(15, 17, 21);\n --p-text-secondary: rgb(97, 102, 107);\n --p-text-tertiary: rgb(129, 133, 140);\n --p-text-caption: rgb(173, 178, 184);\n --p-hover: rgba(38, 49, 72, 0.06);\n --p-accent: rgb(65, 118, 230);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(230, 250, 237);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(254, 245, 231);\n --p-error: rgb(236, 19, 19);\n --p-code-bg: rgb(250, 250, 250);\n --p-code-fg: rgb(97, 102, 107);\n --p-shadow: 0 4px 12px 0 rgba(0,0,0,0.02), 0 2px 8px 0 rgba(0,0,0,0.04);\n }\n :root[data-ds-dark-theme] {\n color-scheme: dark;\n --p-bg: rgb(21, 21, 23);\n --p-bg-layer-1: rgb(35, 35, 36);\n --p-border-l1: rgba(255, 255, 255, 0.06);\n --p-border-l2: rgba(255, 255, 255, 0.12);\n --p-text: rgb(249, 250, 251);\n --p-text-secondary: rgb(207, 211, 214);\n --p-text-tertiary: rgb(173, 178, 184);\n --p-text-caption: rgb(129, 133, 140);\n --p-hover: rgba(255, 255, 255, 0.08);\n --p-accent: rgb(103, 158, 254);\n --p-success-fg: rgb(34, 197, 94);\n --p-success-bg: rgb(35, 60, 44);\n --p-warn-fg: rgb(221, 134, 41);\n --p-warn-bg: rgb(39, 36, 31);\n --p-error: rgb(242, 90, 90);\n --p-code-bg: rgb(27, 27, 28);\n --p-code-fg: rgb(207, 211, 214);\n --p-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body {\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--p-bg); color: var(--p-text);\n display: flex; flex-direction: column;\n }\n main { flex: 1; display: flex; min-height: 0; }\n /* Content (preview) on the LEFT, file panel on the RIGHT — mirrors the\n in-app layout where the preview covers the area left of the sidebar.\n Panel width is adjustable via an invisible handle straddling the panel's\n left border (same pattern as the in-app panel, no visible gap). */\n .sidebar { width: var(--flyout-panel-w, 240px); flex: none; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--p-border-l2); position: relative; }\n .divider { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }\n .divider::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }\n .divider:hover::after, .divider.dragging::after { background: var(--p-accent); }\n body.panel-dragging iframe, body.panel-dragging embed { pointer-events: none; }\n body.panel-dragging { user-select: none; }\n .list { flex: 1; min-height: 0; overflow-y: auto; transition: opacity .15s; }\n .list.is-refreshing { opacity: .45; }\n /* 刷新后逐行浮现:延迟由 JS 按行号注入 */\n .item.flash-in, .tree-row.flash-in { animation: flash-in .3s ease both; }\n @keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } }\n .list .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .item { display: flex; align-items: stretch; border-bottom: 1px solid var(--p-border-l1); }\n /* Selected artifact: left accent bar distinguishes it from the file tree. */\n .item.active { background: var(--p-hover); box-shadow: inset 3px 0 0 var(--p-accent); }\n .item-main { flex: 1; min-width: 0; text-align: left; padding: 10px 14px; border: none; background: transparent; color: inherit; cursor: pointer; font: inherit; }\n .item-main:hover { background: var(--p-hover); }\n .item .row { display: flex; align-items: center; gap: 8px; }\n .badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; flex: none; }\n .badge.create { background: var(--p-success-bg); color: var(--p-success-fg); }\n .badge.edit { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .item .base { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .item .full { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .item .time { color: var(--p-text-caption); font-size: 11px; flex: none; }\n .actions { display: flex; align-items: center; gap: 2px; padding-right: 6px; opacity: 0; }\n .item:hover .actions { opacity: 1; }\n .mini-btn { border: none; background: transparent; color: var(--p-text-tertiary); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; }\n .mini-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .preview { flex: 1; display: flex; flex-direction: column; min-width: 0; }\n /* Tab strip above the preview area (multi-tab, mirrors the in-app overlay) */\n .ptabs { flex: none; display: flex; align-items: stretch; height: 28px; background: var(--p-bg-layer-1); border-bottom: 1px solid var(--p-border-l2); }\n .ptabs-scroll { flex: 1 1 auto; display: flex; align-items: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }\n .ptab { flex: none; display: flex; align-items: center; gap: 6px; max-width: 220px; padding: 0 6px 0 12px; cursor: pointer; border-right: 1px solid var(--p-border-l1); color: var(--p-text-secondary); font-size: 12px; line-height: 28px; user-select: none; }\n .ptab:hover { background: var(--p-hover); color: var(--p-text); }\n .ptab.is-active { background: var(--p-bg); color: var(--p-text); box-shadow: inset 0 -2px 0 var(--p-accent); }\n .ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ptab-close { flex: none; width: 18px; height: 18px; padding: 0; line-height: 1; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: inherit; cursor: pointer; border-radius: 4px; }\n .ptab-close:hover { background: rgba(128, 128, 128, 0.18); }\n .ctx-menu-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; }\n .ctx-menu { position: fixed; z-index: 10000; min-width: 140px; padding: 4px 0; background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); border-radius: 8px; box-shadow: var(--p-shadow); font-size: 12px; }\n .ctx-menu-item { display: block; width: 100%; padding: 6px 14px; border: none; background: transparent; color: var(--p-text); text-align: left; cursor: pointer; font: inherit; }\n .ctx-menu-item:hover:not(:disabled) { background: var(--p-hover); }\n .ctx-menu-item:disabled { color: var(--p-text-caption); cursor: default; }\n .preview .area { flex: 1; min-height: 0; overflow: auto; position: relative; }\n .preview pre { margin: 0; padding: 16px; background: var(--p-code-bg); font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; color: var(--p-code-fg); }\n .preview .hint { padding: 32px; color: var(--p-text-tertiary); text-align: center; }\n .preview .err { padding: 24px; color: var(--p-error); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .preview-img { display: block; max-width: 100%; max-height: 80vh; object-fit: contain; margin: 16px; }\n /* iframe/embed are REPLACED elements: inset-0 keeps their intrinsic\n (small) size, so give them an explicit width/height 100% to fill the area. */\n .preview-iframe { width: 100%; height: 100%; min-height: 400px; border: 0; background: #fff; }\n .preview-pdf { width: 100%; height: 100%; min-height: 480px; border: 0; background: #fff; display: block; }\n .markdown { padding: 16px 20px; line-height: 1.6; word-wrap: break-word; }\n .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { margin: 16px 0 8px; line-height: 1.3; }\n .markdown h1 { font-size: 1.5em; border-bottom: 1px solid var(--p-border-l2); padding-bottom: 6px; }\n .markdown h2 { font-size: 1.3em; border-bottom: 1px solid var(--p-border-l1); padding-bottom: 4px; }\n .markdown code { background: var(--p-code-bg); color: var(--p-code-fg); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; font-size: 0.9em; }\n .markdown pre { background: var(--p-code-bg); padding: 12px 14px; border-radius: 6px; overflow: auto; }\n .markdown pre code { background: transparent; padding: 0; }\n .markdown img { max-width: 100%; }\n .markdown blockquote { border-left: 3px solid var(--p-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--p-text-secondary); }\n .markdown ul, .markdown ol { padding-left: 24px; }\n .markdown a { color: var(--p-accent); }\n .markdown hr { border: none; border-top: 1px solid var(--p-border-l2); margin: 16px 0; }\n .markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 12px 0; }\n .markdown th, .markdown td { border: 1px solid var(--p-border-l2); padding: 4px 10px; text-align: left; }\n .markdown th { background: var(--p-code-bg); font-weight: 600; }\n .diff { border-top: 1px solid var(--p-border-l2); }\n .diff-block { border-bottom: 1px solid var(--p-border-l1); }\n .diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }\n .diff-block.del .diff-label { color: var(--p-error); background: rgba(236,19,19,0.06); }\n .diff-block.add .diff-label { color: var(--p-success-fg); background: rgba(34,197,94,0.08); }\n .diff-pre { margin: 0; padding: 8px 12px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre-wrap; word-break: break-word; }\n .diff-block.del .diff-pre { background: rgba(236,19,19,0.05); }\n .diff-block.add .diff-pre { background: rgba(34,197,94,0.06); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--p-bg-layer-1); border: 1px solid var(--p-border-l2); color: var(--p-text); padding: 6px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .18s; pointer-events: none; box-shadow: var(--p-shadow); z-index: 10; }\n .gtoggle { flex: none; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 6px; border-bottom: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); }\n .gtoggle-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--p-text-tertiary); }\n /* Panel on the left side: preview and panel swap via flex-direction */\n main.side-left { flex-direction: row-reverse; }\n main.side-left .sidebar { border-left: none; border-right: 1px solid var(--p-border-l2); }\n main.side-left .divider { left: auto; right: -4px; }\n .gtoggle-side-icon { display: inline-flex; transition: transform .15s; }\n .gtoggle-side-icon.is-flipped { transform: scaleX(-1); }\n .gtoggle-btn { width: 26px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--p-text-secondary); cursor: pointer; border-radius: 6px; padding: 0; }\n .gtoggle-btn:hover { background: var(--p-hover); color: var(--p-text); }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n /* 文件搜索:默认隐藏,头部搜索按钮切换 */\n .searchbar { flex: none; padding: 6px 8px; border-bottom: 1px solid var(--p-border-l1); }\n .searchbar input { width: 100%; box-sizing: border-box; border: 1px solid var(--p-border-l2); background: var(--p-bg-layer-1); color: var(--p-text); font: inherit; font-size: 12px; border-radius: 6px; padding: 3px 8px; outline: none; }\n .searchbar input:focus { border-color: var(--p-accent); }\n .tree-sub { color: var(--p-text-tertiary); font-size: 11px; margin-left: 6px; }\n .gtoggle-btn.is-active { color: var(--p-accent); }\n .git-badge { font-size: 10px; font-weight: 700; width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-badge-M { background: var(--p-warn-bg); color: var(--p-warn-fg); }\n .git-badge-A { background: var(--p-success-bg); color: var(--p-success-fg); }\n .git-badge-D { background: rgba(236,19,19,0.1); color: var(--p-error); }\n .git-badge-R { background: rgba(65,118,230,0.1); color: var(--p-accent); }\n .git-badge-U { background: var(--p-hover); color: var(--p-text-tertiary); }\n .git-orig { color: var(--p-text-tertiary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .git-adds { color: var(--p-success-fg); }\n .git-dels { color: var(--p-error); }\n .git-err { padding: 14px 12px; color: var(--p-error); word-break: break-all; }\n .gd { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; }\n .gd-line { white-space: pre-wrap; word-break: break-all; padding: 0 12px; }\n .gd-meta { color: var(--p-text-tertiary); background: var(--p-code-bg); padding: 2px 12px; }\n .gd-hunk { color: var(--p-accent); background: rgba(65,118,230,0.08); padding: 2px 12px; }\n .gd-add { color: #1a7f37; background: rgba(34,197,94,0.08); }\n .gd-del { color: #cf222e; background: rgba(236,19,19,0.07); }\n :root[data-ds-dark-theme] .gd-add { color: #69db7c; }\n :root[data-ds-dark-theme] .gd-del { color: #faa2c1; }\n .list.is-hidden { display: none; }\n .tree { flex: 1; min-height: 0; display: none; flex-direction: column; }\n .tree.is-active { display: flex; }\n .tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }\n .tree .empty { padding: 32px 20px; color: var(--p-text-tertiary); text-align: center; }\n .tree-row { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; padding: 0 8px; cursor: pointer; white-space: nowrap; color: var(--p-text); font-size: 14px; border-radius: 8px; }\n .tree-row:hover { background: var(--p-hover); }\n .tree-row.is-selected { background: var(--p-hover); }\n .tree-dir { font-weight: 600; }\n .tree-hidden { opacity: .45; }\n .tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }\n .tree-ref { height: 20px; border: 1px solid var(--p-border-l1); background: var(--p-bg-layer-2); color: var(--p-text-tertiary); font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 999px; flex: none; align-items: center; padding: 0 8px; display: none; }\n .tree-ref:hover { background: var(--p-hover); color: var(--p-text); }\n .tree-row:hover .tree-ref, .tree-row:focus-within .tree-ref { display: inline-flex; }\n .tree-copied { font-size: 11px; color: var(--p-text-tertiary); flex: none; }\n .tree-loading { color: var(--p-text-tertiary); cursor: default; font-size: 12px; }\n .tree-error { color: var(--p-error); cursor: default; font-size: 12px; }\n .tree-retry {\n display: block; margin: 10px auto 0; padding: 4px 16px; font-size: 12px; cursor: pointer;\n border: 1px solid var(--p-border-l2); border-radius: 6px; background: var(--p-bg);\n color: var(--p-text);\n }\n .tree-retry:hover { filter: brightness(1.1); }\n /* Code preview (syntax-highlighted): gutter + code, no banner chrome */\n .codeview { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n .codeview-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--p-code-bg); }\n .codeview-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--p-text-caption); background: var(--p-code-bg); border-right: 1px solid var(--p-border-l1); position: sticky; left: 0; user-select: none; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; }\n .codeview-pre { flex: 1; margin: 0; padding: 12px; background: var(--p-code-bg); font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, \"Cascadia Mono\", \"Liberation Mono\", monospace; white-space: pre; }\n .codeview-pre code { font: inherit; }\n .tok-comment { color: #868e96; }\n .tok-string { color: #2f9e44; }\n .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }\n .tok-keyword, .tok-important, .tok-atrule { color: #d6336c; }\n .tok-function, .tok-decorator { color: #6741d9; }\n .tok-class, .tok-builtin, .tok-tag, .tok-key { color: #1971c2; }\n .tok-property { color: #495057; }\n :root[data-ds-dark-theme] .tok-comment { color: #adb5bd; }\n :root[data-ds-dark-theme] .tok-string { color: #69db7c; }\n :root[data-ds-dark-theme] .tok-number, :root[data-ds-dark-theme] .tok-bool, :root[data-ds-dark-theme] .tok-variable, :root[data-ds-dark-theme] .tok-hex, :root[data-ds-dark-theme] .tok-attr { color: #ffa94d; }\n :root[data-ds-dark-theme] .tok-keyword, :root[data-ds-dark-theme] .tok-important, :root[data-ds-dark-theme] .tok-atrule { color: #faa2c1; }\n :root[data-ds-dark-theme] .tok-function, :root[data-ds-dark-theme] .tok-decorator { color: #b197fc; }\n :root[data-ds-dark-theme] .tok-class, :root[data-ds-dark-theme] .tok-builtin, :root[data-ds-dark-theme] .tok-tag, :root[data-ds-dark-theme] .tok-key { color: #74c0fc; }\n :root[data-ds-dark-theme] .tok-property { color: #ced4da; }\n</style>\n</head>\n<body>\n <main>\n <div class=\"preview\">\n <div class=\"ptabs\" id=\"ptabs\"></div>\n <div class=\"area\" id=\"previewArea\"></div>\n </div>\n <div class=\"sidebar\">\n <div class=\"divider\" id=\"divider\"></div>\n <div class=\"gtoggle\">\n <button class=\"gtoggle-btn\" id=\"sideBtn\" type=\"button\" title=\"将文件面板移到左侧\"></button>\n <span class=\"gtoggle-status\" id=\"status\">connecting…</span>\n <button class=\"gtoggle-btn\" id=\"searchBtn\" type=\"button\" title=\"搜索文件\"></button>\n <button class=\"gtoggle-btn\" id=\"refreshBtn\" type=\"button\" title=\"刷新\"></button>\n <button class=\"gtoggle-btn\" id=\"viewBtn\" type=\"button\" title=\"查看 Git 变更(未提交)\"></button>\n </div>\n <div class=\"list is-hidden\" id=\"list\"></div>\n <div class=\"tree is-active\" id=\"tree\">\n <div class=\"searchbar\" id=\"searchBar\" style=\"display: none;\">\n <input id=\"searchInput\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\n <div class=\"tree-body\" id=\"treeBody\"></div>\n </div>\n </div>\n </main>\n <div class=\"toast\" id=\"toast\"></div>\n <script>\n", "\n // i18n 文案函数别名:页面脚本多处用局部变量 t(toast 元素 / 当前标签),\n // 统一经 tr 取文案避免遮蔽。\n var tr = t;\n var DATA_URL = '/flyout-sidebar/data';\n var CONTENT_URL = '/flyout-sidebar/content';\n var MEDIA_URL = '/flyout-sidebar/media';\n var LISTDIR_URL = '/flyout-sidebar/listdir';\n var GITSTATUS_URL = '/flyout-sidebar/gitstatus';\n var GITDIFF_URL = '/flyout-sidebar/gitdiff';\n var SEARCH_URL = '/flyout-sidebar/search';\n var _sm = /[?&]sessionId=([^&]+)/.exec(location.search);\n var _urlSessionId = _sm ? decodeURIComponent(_sm[1]) : '';\n var SESSION_KEY = 'dsh-flyout-sidebar:session';\n function currentSessionId() {\n try {\n var v = localStorage.getItem(SESSION_KEY);\n if (v) return v;\n } catch (e) {}\n return _urlSessionId;\n }\n function listdirUrl(path) {\n var q = [];\n var sid = currentSessionId();\n if (sid) q.push('sessionId=' + encodeURIComponent(sid));\n if (path) q.push('path=' + encodeURIComponent(path));\n return LISTDIR_URL + (q.length ? '?' + q.join('&') : '');\n }\n // 内容/媒体读取也带 sessionId:host 按会话解析工作区根(可能与沙箱根不同)\n function sessionQuery() {\n var sid = currentSessionId();\n return sid ? '&sessionId=' + encodeURIComponent(sid) : '';\n }\n var gitFiles = null;\n var gitError = null;\n var gitSig = null; // 上次渲染的变更签名;轮询数据未变时跳过重渲染,避免冲掉刷新动画\n var treeRoot = null;\n var treeChildren = {};\n var treeExpanded = {};\n var treeError = null;\n var currentView = 'tree';\n\n var FOLDER_CLOSE_D = 'M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z';\n var FOLDER_OPEN_D1 = 'M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z';\n var FOLDER_OPEN_D2 = 'M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z';\n var CODE_D = 'M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z';\n var REFRESH_D = 'M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z';\n\n function svgIcon(paths, size) {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', size || 14);\n svg.setAttribute('height', size || 14);\n svg.setAttribute('viewBox', '0 0 16 16');\n svg.setAttribute('fill', 'none');\n (paths || []).forEach(function (spec) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', spec.d);\n if (spec.transform) p.setAttribute('transform', spec.transform);\n if (spec.opacity) p.setAttribute('opacity', spec.opacity);\n if (spec.fillRule) p.setAttribute('fill-rule', spec.fillRule);\n if (spec.clipRule) p.setAttribute('clip-rule', spec.clipRule);\n p.setAttribute('fill', 'currentColor');\n svg.appendChild(p);\n });\n return svg;\n }\n function folderClosedIcon() { return svgIcon([{ d: FOLDER_CLOSE_D, transform: 'translate(1.5 2.429)' }]); }\n function folderOpenIcon() { return svgIcon([{ d: FOLDER_OPEN_D1 }, { d: FOLDER_OPEN_D2, opacity: '0.2' }]); }\n function fileCodeIcon() { return svgIcon([{ d: CODE_D, fillRule: 'evenodd', clipRule: 'evenodd' }]); }\n function refreshIcon() { return svgIcon([{ d: REFRESH_D }]); }\n // 放大镜(描边圆 + 柄),与 git 分支图标同风格\n function searchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('cx', '7'); circle.setAttribute('cy', '7'); circle.setAttribute('r', '4.4');\n circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '1.4');\n svg.appendChild(circle);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.4'); line.setAttribute('y1', '10.4');\n line.setAttribute('x2', '13.5'); line.setAttribute('y2', '13.5');\n line.setAttribute('stroke', 'currentColor'); line.setAttribute('stroke-width', '1.4');\n line.setAttribute('stroke-linecap', 'round');\n svg.appendChild(line);\n return svg;\n }\n // Git-branch glyph drawn with strokes (matches the sidebar's toggle icon).\n function gitBranchIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var spec = [\n 'M4.5 4.6v6.8',\n 'M11.5 4.7v1.1c0 1.9-1.6 3.1-3.6 3.1-1.9 0-3.4 1.2-3.4 1.2',\n ];\n spec.forEach(function (d) {\n var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n p.setAttribute('d', d);\n p.setAttribute('stroke', 'currentColor');\n p.setAttribute('stroke-width', '1.4');\n p.setAttribute('stroke-linecap', 'round');\n p.setAttribute('fill', 'none');\n svg.appendChild(p);\n });\n [ [4.5, 3], [4.5, 13], [11.5, 3] ].forEach(function (c) {\n var o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n o.setAttribute('cx', String(c[0])); o.setAttribute('cy', String(c[1]));\n o.setAttribute('r', '1.7');\n o.setAttribute('stroke', 'currentColor');\n o.setAttribute('stroke-width', '1.4');\n o.setAttribute('fill', 'none');\n svg.appendChild(o);\n });\n return svg;\n }\n\n function el(tag, className, text) {\n var n = document.createElement(tag);\n if (className) n.className = className;\n if (text != null) n.textContent = text;\n return n;\n }\n function toast(msg) {\n var t = document.getElementById('toast');\n t.textContent = msg;\n t.style.opacity = '1';\n clearTimeout(t._timer);\n t._timer = setTimeout(function () { t.style.opacity = '0'; }, 1600);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text;\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(ta);\n }\n function copyText(text, msg) {\n var done = function () { toast(msg); };\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });\n } else { fallbackCopy(text); done(); }\n }\n\n function errNode(msg) {\n return el('div', 'err', msg || 'read failed');\n }\n // gitLabel / gitTitle / basename 由 shared/gitui.js 内联提供\n function renderGit() {\n var list = document.getElementById('list');\n list.textContent = '';\n if (currentView !== 'git') return;\n if (gitError) {\n list.appendChild(el('div', 'git-err', gitError));\n return;\n }\n if (gitFiles == null) {\n list.appendChild(el('div', 'empty', tr('loadingChanges')));\n return;\n }\n if (!gitFiles.length) {\n list.appendChild(el('div', 'empty', tr('noChanges')));\n return;\n }\n gitFiles.forEach(function (e) {\n var item = el('div', 'item');\n var at = activeTab();\n if (at && at.git && at.path === e.path) item.className += ' active';\n var main = el('button', 'item-main');\n main.title = gitTitle(e);\n var row = el('div', 'row');\n row.appendChild(el('span', 'git-badge git-badge-' + gitLabel(e), gitLabel(e)));\n row.appendChild(el('span', 'base', basename(e.path)));\n if (typeof e.adds === 'number' && (e.adds > 0 || (e.dels || 0) > 0)) {\n var stats = el('span', 'git-stats');\n stats.appendChild(el('span', 'git-adds', '+' + e.adds));\n stats.appendChild(el('span', 'git-dels', '\\u2212' + (e.dels || 0)));\n row.appendChild(stats);\n }\n if (e.origPath) row.appendChild(el('span', 'git-orig', '\\u2190 ' + basename(e.origPath)));\n main.appendChild(row);\n main.appendChild(el('div', 'full', e.path));\n main.addEventListener('click', function () { openGitDiff(e.path); });\n item.appendChild(main);\n var actions = el('div', 'actions');\n var cp = el('button', 'mini-btn', '⧉');\n cp.title = tr('copyPath');\n cp.addEventListener('click', function (ev) { ev.stopPropagation(); copyText(e.path, tr('copiedPath')); });\n var qt = el('button', 'mini-btn', '@');\n qt.title = tr('refFlyoutTitle');\n qt.addEventListener('click', function (ev) { ev.stopPropagation(); copyText('@' + e.path, tr('copied')); });\n actions.appendChild(cp);\n actions.appendChild(qt);\n item.appendChild(actions);\n list.appendChild(item);\n });\n }\n // Unified git diff renderer: meta/hunk/+/- rows with colors.\n // 行分类逻辑由 shared/gitui.js 的 gitDiffLineClass 提供。\n function gitDiffNode(text) {\n var wrap = el('div', 'gd');\n if (!text) {\n wrap.appendChild(el('div', 'hint', tr('noChangesHead')));\n return wrap;\n }\n var lines = String(text).replace(/\\n$/, '').split('\\n');\n lines.forEach(function (line) {\n wrap.appendChild(el('div', gitDiffLineClass(line), line));\n });\n return wrap;\n }\n // ── Multi-tab preview (mirrors the in-app overlay) ────────────────────\n // tabs: [{ key, path, git, type, loading, ok, error, content, truncated, diff }]\n // 'p:' keys are content previews, 'g:' keys are git diffs.\n var tabs = [];\n var activeKey = null;\n function findTab(key) {\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) return tabs[i];\n return null;\n }\n function activeTab() { return findTab(activeKey); }\n function renderTabs() {\n var wrap = document.getElementById('ptabs');\n wrap.textContent = '';\n var scroll = el('div', 'ptabs-scroll');\n tabs.forEach(function (t) {\n var tab = el('div', 'ptab' + (t.key === activeKey ? ' is-active' : ''));\n tab.title = (t.git ? tr('diffTabPrefix') : '') + t.path;\n tab.appendChild(el('span', 'ptab-name', basename(t.path)));\n var x = el('button', 'ptab-close', '×');\n x.type = 'button';\n x.title = tr('closeTab');\n x.addEventListener('click', function (ev) { ev.stopPropagation(); closeTab(t.key); });\n tab.appendChild(x);\n tab.addEventListener('click', function () { setActiveTab(t.key); });\n tab.addEventListener('contextmenu', function (ev) { showTabContextMenu(ev, t.key); });\n scroll.appendChild(tab);\n });\n wrap.appendChild(scroll);\n }\n function refreshTreeSelection() { if (treeRoot && currentView === 'tree') renderTree(); }\n function setActiveTab(key) {\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeTab(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n tabs.splice(idx, 1);\n if (activeKey === key) activeKey = tabs.length ? tabs[Math.min(idx, tabs.length - 1)].key : null;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeOthers(key) {\n var t = findTab(key);\n if (!t) return;\n tabs = [t];\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeRight(key) {\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n if (idx < 0) return;\n var removedActive = false;\n for (var j = tabs.length - 1; j > idx; j -= 1) {\n if (tabs[j].key === activeKey) removedActive = true;\n tabs.splice(j, 1);\n }\n if (removedActive) activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n }\n function closeContextMenu() {\n var m = document.getElementById('ctxMenu');\n if (m) m.remove();\n }\n function showTabContextMenu(ev, key) {\n ev.preventDefault();\n closeContextMenu();\n var idx = -1;\n for (var i = 0; i < tabs.length; i += 1) if (tabs[i].key === key) { idx = i; break; }\n var menu = el('div', 'ctx-menu');\n menu.id = 'ctxMenu';\n menu.style.left = ev.clientX + 'px';\n menu.style.top = ev.clientY + 'px';\n function addItem(label, fn, disabled) {\n var b = el('button', 'ctx-menu-item', label);\n b.type = 'button';\n if (disabled) { b.disabled = true; b.className += ' is-disabled'; }\n b.addEventListener('click', function () { closeContextMenu(); fn(); });\n menu.appendChild(b);\n }\n addItem(tr('closeTab'), function () { closeTab(key); }, false);\n addItem(tr('closeOthers'), function () { closeOthers(key); }, tabs.length <= 1);\n addItem(tr('closeRight'), function () { closeRight(key); }, idx >= tabs.length - 1);\n var backdrop = el('div', 'ctx-menu-backdrop');\n backdrop.addEventListener('click', closeContextMenu);\n backdrop.addEventListener('contextmenu', function (e) { e.preventDefault(); closeContextMenu(); });\n document.body.appendChild(backdrop);\n document.body.appendChild(menu);\n }\n function patchTab(key, patch) {\n var t = findTab(key);\n if (!t) return;\n for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) t[k] = patch[k];\n renderTabs();\n renderActive();\n }\n function codeViewNode(content, path) {\n var view = el('div', 'codeview');\n var scroll = el('div', 'codeview-scroll');\n var gutter = el('pre', 'codeview-gutter');\n gutter.setAttribute('aria-hidden', 'true');\n var lines = String(content).replace(/\\n$/, '').split('\\n');\n var gt = '';\n for (var gi = 0; gi < lines.length; gi += 1) gt += (gi + 1) + (gi < lines.length - 1 ? '\\n' : '');\n gutter.textContent = gt;\n var pre = el('pre', 'codeview-pre');\n var code = el('code');\n code.innerHTML = highlightCode(content, fileExt(path));\n pre.appendChild(code);\n scroll.appendChild(gutter);\n scroll.appendChild(pre);\n view.appendChild(scroll);\n return view;\n }\n function renderActive() {\n var area = document.getElementById('previewArea');\n area.textContent = '';\n var t = activeTab();\n if (!t) {\n area.appendChild(el('div', 'hint', currentView === 'git' ? tr('hintClickGit') : tr('hintClickTree')));\n return;\n }\n if (t.loading) { area.appendChild(el('div', 'hint', tr('loading'))); return; }\n if (t.ok === false) { area.appendChild(errNode(t.error || tr('readFailed'))); return; }\n if (t.git) { area.appendChild(gitDiffNode(t.diff || '')); return; }\n var type = t.type || 'text';\n if (type === 'image') {\n var img = el('img', 'preview-img');\n img.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + sessionQuery();\n img.alt = t.path;\n img.addEventListener('error', function () { area.textContent = ''; area.appendChild(errNode(tr('imageLoadFailed'))); });\n area.appendChild(img);\n return;\n }\n if (type === 'pdf') {\n // Standalone tab: use the browser's NATIVE PDF viewer (with its own\n // toolbar). #zoom=page-width fits the page to the box width.\n var pdf = el('embed', 'preview-pdf');\n pdf.src = MEDIA_URL + '?path=' + encodeURIComponent(t.path) + sessionQuery() + '#zoom=page-width';\n pdf.type = 'application/pdf';\n area.appendChild(pdf);\n return;\n }\n if (type === 'html') {\n var frame = el('iframe', 'preview-iframe');\n frame.setAttribute('sandbox', 'allow-scripts');\n frame.setAttribute('srcdoc', t.content || '');\n area.appendChild(frame);\n return;\n }\n if (type === 'markdown') {\n var md = el('div', 'markdown');\n md.innerHTML = mdToHtml(t.content || '');\n area.appendChild(md);\n return;\n }\n if (t.truncated) area.appendChild(el('div', 'diff-label', tr('truncated')));\n area.appendChild(codeViewNode(t.content || '', t.path));\n }\n function openFileTab(path) {\n var key = 'p:' + path;\n var type = extType(path);\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: false, type: type }; tabs.push(t); }\n t.type = type;\n var needFetch = type !== 'image' && type !== 'pdf';\n t.loading = needFetch;\n activeKey = key;\n renderTabs();\n renderActive();\n refreshTreeSelection();\n if (!needFetch) return;\n fetch(CONTENT_URL + '?path=' + encodeURIComponent(path) + sessionQuery()).then(function (r) { return r.json(); }).then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || tr('readFailed') }); return; }\n patchTab(key, { loading: false, ok: true, content: data.content, truncated: data.truncated });\n }).catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n function openGitDiff(path) {\n var key = 'g:' + path;\n var t = findTab(key);\n if (!t) { t = { key: key, path: path, git: true }; tabs.push(t); }\n t.loading = true;\n activeKey = key;\n renderTabs();\n renderActive();\n fetch(GITDIFF_URL + '?path=' + encodeURIComponent(path) + '&sessionId=' + encodeURIComponent(currentSessionId() || ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (data) {\n if (!data || data.ok !== true) { patchTab(key, { loading: false, ok: false, error: (data && data.error) || tr('readFailed') }); return; }\n patchTab(key, { loading: false, ok: true, diff: data.diff });\n })\n .catch(function (e) {\n patchTab(key, { loading: false, ok: false, error: String(e && e.message ? e.message : e) });\n });\n }\n\n // ── View toggle: file tree ⇄ git changed files ───────────────────────\n function setView(view) {\n currentView = view;\n var btn = document.getElementById('viewBtn');\n btn.classList.toggle('is-active', view === 'git');\n btn.title = view === 'tree' ? tr('viewGit') : tr('backToFiles');\n btn.textContent = '';\n btn.appendChild(view === 'tree' ? gitBranchIcon() : folderClosedIcon());\n var rb = document.getElementById('refreshBtn');\n if (rb) rb.title = view === 'tree' ? tr('refreshTree') : tr('refreshChanges');\n document.getElementById('list').classList.toggle('is-hidden', view !== 'git');\n document.getElementById('tree').classList.toggle('is-active', view === 'tree');\n if (view === 'tree' && !treeRoot) loadTreeRoot();\n if (view === 'git') { loadGit(); }\n }\n\n function loadTreeRoot(retries, delay) {\n treeRoot = null;\n treeError = null;\n treeChildren = {};\n treeExpanded = {};\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n bodyEl.appendChild(el('div', 'tree-loading', tr('loadingTree')));\n // A freshly switched-to workspace may not be resolvable on the host yet\n // (its session is still loading/persisting); retry with exponential\n // backoff (~9s total) so the tree self-corrects instead of sitting on\n // an error/empty state.\n var left = typeof retries === 'number' ? retries : 5;\n delay = typeof delay === 'number' ? delay : 400;\n fetch(listdirUrl(), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n if (res && res.ok) {\n treeRoot = { path: res.path, entries: res.entries };\n treeError = null;\n } else if (left > 0) {\n setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay);\n return;\n } else {\n // 明确错误态 + 重试按钮:不再停在「加载文件树…」,treeRoot 保持\n // null,点重试或切换视图都会重新拉取。\n treeError = (res && res.error) || tr('treeLoadFailed');\n renderTreeError();\n return;\n }\n renderTree();\n staggerList(document.getElementById('treeBody'), '.tree-row');\n }).catch(function () {\n if (left > 0) { setTimeout(function () { loadTreeRoot(left - 1, delay * 2); }, delay); return; }\n treeError = tr('treeLoadFailed');\n renderTreeError();\n });\n }\n\n // 树根加载失败:错误文案 + 重试按钮(treeRoot 仍为 null,随时可重新获取)\n function renderTreeError() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n var err = el('div', 'tree-error', treeError || tr('treeLoadFailed'));\n bodyEl.appendChild(err);\n var btn = el('button', 'tree-retry', tr('retry'));\n btn.type = 'button';\n btn.addEventListener('click', function () { loadTreeRoot(); });\n bodyEl.appendChild(btn);\n }\n\n // ── 文件搜索(文件树视图内):搜索框默认隐藏,头部搜索按钮切换 ──────\n var searchOpen = false;\n var searchQuery = '';\n var searchState = null; // { loading } | { error } | { entries }\n var searchTimer = null;\n\n // 搜索结果:平铺的匹配文件行(点击打开预览标签),目录前缀弱化显示\n function renderSearchResults() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (searchState && searchState.loading) {\n bodyEl.appendChild(el('div', 'tree-loading', tr('searching')));\n return;\n }\n if (searchState && searchState.error) {\n bodyEl.appendChild(el('div', 'tree-error', searchState.error));\n return;\n }\n var entries = (searchState && searchState.entries) || [];\n if (!entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('noResults')));\n return;\n }\n entries.forEach(function (p) {\n var row = el('div', 'tree-row');\n row.title = p;\n row.appendChild(fileCodeIcon());\n var name = el('span', 'tree-name', basename(p));\n var slash = p.lastIndexOf('/');\n if (slash >= 0) name.appendChild(el('span', 'tree-sub', p.slice(0, slash)));\n row.appendChild(name);\n row.addEventListener('click', function () { openFileTab(p); });\n bodyEl.appendChild(row);\n });\n }\n\n function runSearch() {\n var q = searchQuery.trim();\n if (!q) { searchState = null; renderTree(); return; }\n searchState = { loading: true };\n renderTree();\n var sid = currentSessionId();\n fetch(SEARCH_URL + '?q=' + encodeURIComponent(q) + (sid ? '&sessionId=' + encodeURIComponent(sid) : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); })\n .then(function (res) {\n if (q !== searchQuery.trim()) return; // 已被更新的查询取代\n searchState = res && res.ok ? { entries: res.entries || [] } : { error: (res && res.error) || tr('searchFailed') };\n renderTree();\n })\n .catch(function () {\n if (q !== searchQuery.trim()) return;\n searchState = { error: tr('searchFailed') };\n renderTree();\n });\n }\n\n // 搜索激活时树体显示匹配结果,否则显示目录树\n function renderTree() {\n if (searchOpen && searchQuery.trim()) { renderSearchResults(); return; }\n renderTreeBase();\n }\n\n function renderTreeBase() {\n var bodyEl = document.getElementById('treeBody');\n bodyEl.textContent = '';\n if (!treeRoot) {\n // 根加载失败后 renderTree 的后续调用不得抹掉错误态;重试按钮随时可重新拉取\n if (treeError) renderTreeError();\n return;\n }\n if (!treeRoot.entries || !treeRoot.entries.length) {\n bodyEl.appendChild(el('div', 'empty', tr('emptyDir')));\n return;\n }\n treeRoot.entries.forEach(function (entry) {\n bodyEl.appendChild(renderTreeNode(entry, 0));\n });\n }\n\n function copyRef(path) {\n copyText('@' + path, tr('copied'));\n }\n\n function renderTreeNode(entry, depth) {\n var wrap = el('div');\n var at = activeTab();\n var isSelected = !!(at && !at.git && at.path === entry.path);\n var row = el('div', 'tree-row' + (entry.isDir ? ' tree-dir' : '') + (entry.hidden ? ' tree-hidden' : '') + (isSelected ? ' is-selected' : ''));\n row.style.paddingLeft = (8 + depth * 20) + 'px';\n row.title = entry.path;\n\n row.appendChild(entry.isDir ? (treeExpanded[entry.path] ? folderOpenIcon() : folderClosedIcon()) : fileCodeIcon());\n row.appendChild(el('span', 'tree-name', entry.name));\n\n var refBtn = el('button', 'tree-ref', tr('refBtn'));\n refBtn.type = 'button';\n refBtn.title = tr('refFlyoutTitle');\n refBtn.addEventListener('click', function (ev) { ev.stopPropagation(); copyRef(entry.path); });\n row.appendChild(refBtn);\n\n if (entry.isDir) {\n row.addEventListener('click', function () { toggleTree(entry.path); });\n } else {\n row.addEventListener('click', function () { openFileTab(entry.path); });\n }\n wrap.appendChild(row);\n\n if (entry.isDir && treeExpanded[entry.path]) {\n var node = treeChildren[entry.path];\n var childPad = 8 + (depth + 1) * 20 + 20;\n if (node && node.loading) {\n var lr = el('div', 'tree-row tree-loading', tr('loading'));\n lr.style.paddingLeft = childPad + 'px';\n wrap.appendChild(lr);\n } else if (node && node.error) {\n var er = el('div', 'tree-row tree-error', node.error);\n er.style.paddingLeft = childPad + 'px';\n wrap.appendChild(er);\n } else if (node && node.entries) {\n node.entries.forEach(function (c) { wrap.appendChild(renderTreeNode(c, depth + 1)); });\n }\n }\n return wrap;\n }\n\n function toggleTree(path) {\n if (treeExpanded[path]) {\n treeExpanded[path] = false;\n renderTree();\n return;\n }\n treeExpanded[path] = true;\n if (!treeChildren[path]) {\n treeChildren[path] = { loading: true };\n renderTree();\n fetch(listdirUrl(path), { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (res) {\n treeChildren[path] = res && res.ok ? { entries: res.entries } : { error: (res && res.error) || tr('readFailed') };\n renderTree();\n }).catch(function () {\n treeChildren[path] = { error: tr('readFailed') };\n renderTree();\n });\n } else {\n renderTree();\n }\n }\n\n document.getElementById('viewBtn').addEventListener('click', function () {\n setView(currentView === 'tree' ? 'git' : 'tree');\n });\n var refreshBtn = document.getElementById('refreshBtn');\n if (refreshBtn) {\n refreshBtn.appendChild(refreshIcon());\n refreshBtn.addEventListener('click', function () {\n // 搜索激活时刷新 = 重跑搜索;否则重取目录树 / 变更列表\n if (currentView === 'tree') {\n if (searchOpen && searchQuery.trim()) runSearch();\n else loadTreeRoot();\n }\n else loadGit(true);\n });\n }\n var searchBtn = document.getElementById('searchBtn');\n if (searchBtn) {\n searchBtn.appendChild(searchIcon());\n searchBtn.title = tr('searchToggle');\n searchBtn.addEventListener('click', function () {\n searchOpen = !searchOpen;\n searchBtn.classList.toggle('is-active', searchOpen);\n var bar = document.getElementById('searchBar');\n bar.style.display = searchOpen ? 'block' : 'none';\n var input = document.getElementById('searchInput');\n if (searchOpen) {\n input.focus();\n } else {\n input.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n }\n });\n }\n var searchInput = document.getElementById('searchInput');\n if (searchInput) {\n searchInput.addEventListener('input', function () {\n searchQuery = searchInput.value;\n if (searchTimer) clearTimeout(searchTimer);\n searchTimer = setTimeout(runSearch, 250);\n });\n searchInput.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Escape') return;\n ev.stopPropagation();\n if (searchQuery.trim()) {\n searchInput.value = '';\n searchQuery = '';\n searchState = null;\n renderTree();\n } else if (searchBtn) {\n searchBtn.click(); // 收起搜索框\n }\n });\n }\n // Poll git status for the changed-files view (and the live/offline badge).\n // force=1 绕过 host 的 stale-while-revalidate 缓存,由刷新按钮使用。\n // 刷新后给列表行加交错浮现动画(从上往下);selector 区分 git 列表与文件树。\n function staggerList(list, selector) {\n var items = list.querySelectorAll(selector || '.item');\n for (var i = 0; i < items.length; i++) {\n var it = items[i];\n it.classList.remove('flash-in');\n void it.offsetWidth; // 重置动画,使连续刷新也能重放\n it.classList.add('flash-in');\n it.style.animationDelay = (Math.min(i, 12) * 45) + 'ms';\n }\n }\n function loadGit(force) {\n var list = document.getElementById('list');\n // 强制刷新时列表短暂变暗,响应返回后恢复,给出「刷新过了」的可见反馈。\n if (force && list) list.classList.add('is-refreshing');\n fetch(GITSTATUS_URL + '?sessionId=' + encodeURIComponent(currentSessionId() || '') + (force ? '&force=1' : ''), { cache: 'no-store' })\n .then(function (r) { return r.json(); }).then(function (data) {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n var ok = data && data.ok === true;\n if (ok) {\n st.textContent = 'live';\n st.style.color = themeColors.success;\n } else {\n st.textContent = 'git error';\n st.style.color = themeColors.error;\n }\n // 数据签名未变时跳过重渲染:2s 轮询不会重建行元素、冲掉浮现动画。\n var nextFiles = ok ? (Array.isArray(data.entries) ? data.entries : []) : [];\n var nextError = ok ? null : ((data && data.error) || tr('gitStatusFailed'));\n var sig = JSON.stringify([nextError, nextFiles]);\n if (!force && sig === gitSig) return;\n gitSig = sig;\n gitFiles = nextFiles;\n gitError = nextError;\n renderGit();\n if (force && list) staggerList(list);\n }).catch(function () {\n if (list) list.classList.remove('is-refreshing');\n var st = document.getElementById('status');\n st.textContent = 'offline';\n st.style.color = themeColors.error;\n });\n }\n document.getElementById('divider').title = tr('resizePanel');\n setView('tree');\n renderTabs();\n renderActive();\n // ── Panel side: file panel on the left (default) or the right ─────────\n var PANEL_SIDE_KEY = 'dsh-flyout-sidebar:panelLeft';\n var panelLeft = true;\n try { var _savedSide = localStorage.getItem(PANEL_SIDE_KEY); if (_savedSide === '0') panelLeft = false; } catch (e) {}\n function panelSideIcon() {\n var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', 16); svg.setAttribute('height', 16);\n svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('fill', 'none');\n var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');\n rect.setAttribute('x', '1.5'); rect.setAttribute('y', '1.5');\n rect.setAttribute('width', '13'); rect.setAttribute('height', '13');\n rect.setAttribute('rx', '2.8');\n rect.setAttribute('stroke', 'currentColor');\n rect.setAttribute('stroke-width', '1.5');\n rect.setAttribute('fill', 'none');\n svg.appendChild(rect);\n var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', '10.2'); line.setAttribute('y1', '2.6');\n line.setAttribute('x2', '10.2'); line.setAttribute('y2', '13.4');\n line.setAttribute('stroke', 'currentColor');\n line.setAttribute('stroke-width', '1.5');\n svg.appendChild(line);\n return svg;\n }\n function applyPanelSide() {\n var main = document.querySelector('main');\n if (main) main.classList.toggle('side-left', panelLeft);\n var b = document.getElementById('sideBtn');\n if (b) {\n b.title = panelLeft ? tr('movePanelRight') : tr('movePanelLeft');\n var icon = b.querySelector('.gtoggle-side-icon');\n if (icon) icon.classList.toggle('is-flipped', panelLeft);\n }\n }\n var sideBtn = document.getElementById('sideBtn');\n if (sideBtn) {\n var sideIconWrap = el('span', 'gtoggle-side-icon');\n sideIconWrap.appendChild(panelSideIcon());\n sideBtn.appendChild(sideIconWrap);\n sideBtn.addEventListener('click', function () {\n panelLeft = !panelLeft;\n applyPanelSide();\n try { localStorage.setItem(PANEL_SIDE_KEY, panelLeft ? '1' : '0'); } catch (e) {}\n });\n }\n applyPanelSide();\n // ── Panel width: draggable divider; default = minimum ─────────────────\n var PANEL_W_KEY = 'dsh-flyout-sidebar:panelw';\n var PANEL_MIN = 240;\n var PANEL_MAX_RATIO = 0.6;\n var panelW = PANEL_MIN;\n try {\n var _pw = parseInt(localStorage.getItem(PANEL_W_KEY), 10);\n if (Number.isFinite(_pw) && _pw >= PANEL_MIN) panelW = _pw;\n } catch (e) {}\n function applyPanelW() {\n var sb = document.querySelector('.sidebar');\n if (sb) sb.style.width = panelW + 'px';\n }\n applyPanelW();\n document.getElementById('divider').addEventListener('mousedown', function (ev) {\n ev.preventDefault();\n var divider = document.getElementById('divider');\n divider.classList.add('dragging');\n document.body.classList.add('panel-dragging');\n var maxW = Math.max(PANEL_MIN, Math.round(window.innerWidth * PANEL_MAX_RATIO));\n var onMove = function (e) {\n var w = panelLeft ? e.clientX : window.innerWidth - e.clientX;\n panelW = Math.max(PANEL_MIN, Math.min(w, maxW));\n applyPanelW();\n };\n var onUp = function () {\n divider.classList.remove('dragging');\n document.body.classList.remove('panel-dragging');\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n try { localStorage.setItem(PANEL_W_KEY, String(panelW)); } catch (e) {}\n };\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n setInterval(function () { if (!document.hidden && currentView === 'git') loadGit(); }, 2000);\n // Follow the app's light/dark theme live: the main tab writes the theme to\n // localStorage (THEME_KEY) whenever DSH's theme changes.\n var THEME_KEY = 'dsh-flyout-sidebar:theme';\n // 主题切换时缓存一次状态色,2s 轮询不再反复 getComputedStyle\n var themeColors = { success: '#34c55e', error: '#ef4444' };\n function readThemeColors() {\n try {\n var cs = getComputedStyle(document.documentElement);\n themeColors.success = cs.getPropertyValue('--p-success-fg').trim() || themeColors.success;\n themeColors.error = cs.getPropertyValue('--p-error').trim() || themeColors.error;\n } catch (e) {}\n }\n function applyTheme() {\n var v = null;\n try { v = localStorage.getItem(THEME_KEY); } catch (e) {}\n if (v !== 'dark' && v !== 'light') {\n var m = /[?&]scheme=([^&]+)/.exec(location.search);\n if (m) v = m[1];\n }\n if (v === 'dark') document.documentElement.setAttribute('data-ds-dark-theme', '');\n else document.documentElement.removeAttribute('data-ds-dark-theme');\n readThemeColors();\n }\n readThemeColors();\n // Follow the active session in real time: the main tab publishes the\n // current session id to localStorage (SESSION_KEY) only when it actually\n // changes, so the storage event alone is enough — no polling.\n var _lastTreeSession = currentSessionId();\n function watchSession() {\n var sid = currentSessionId();\n if (sid !== _lastTreeSession) {\n _lastTreeSession = sid;\n // Preview tabs hold the previous project's files — close them all so\n // the new workspace starts clean.\n tabs = [];\n activeKey = null;\n renderTabs();\n renderActive();\n loadTreeRoot();\n if (currentView === 'git') loadGit();\n }\n }\n window.addEventListener('storage', function (e) {\n if (e.key === SESSION_KEY) watchSession();\n if (e.key === THEME_KEY) applyTheme();\n });\n // Background tabs throttle setInterval, so a tab left in the background can\n // show stale data for up to a minute. Refresh immediately whenever the\n // user returns to (or focuses on) this tab.\n document.addEventListener('visibilitychange', function () {\n if (!document.hidden && currentView === 'git') loadGit();\n });\n window.addEventListener('focus', function () { if (currentView === 'git') loadGit(); });\n <\/script>\n</body>\n</html>"])), sharedScript);
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.5",
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",