dsh-flyout-sidebar 0.1.2 → 0.1.3

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
@@ -23,7 +23,9 @@
23
23
  ctx = context;
24
24
  }
25
25
  async function getJson(url) {
26
- return (await fetch(url)).json();
26
+ const res = await fetch(url);
27
+ if (!res.ok) throw new Error("HTTP " + res.status + " " + url);
28
+ return res.json();
27
29
  }
28
30
  /** 组装查询串:空值参数不下发(与旧版 URL 形态一致) */
29
31
  function qs(...pairs) {
@@ -38,11 +40,17 @@
38
40
  gitDiff(path, sessionId) {
39
41
  return getJson("/flyout-sidebar/gitdiff" + qs(["path", path], ["sessionId", sessionId]));
40
42
  },
41
- readArtifact(path) {
42
- return getJson("/flyout-sidebar/content" + qs(["path", path]));
43
+ readArtifact(path, sessionId) {
44
+ return getJson("/flyout-sidebar/content" + qs(["path", path], ["sessionId", sessionId]));
43
45
  },
44
46
  listDir(path, sessionId) {
45
47
  return getJson("/flyout-sidebar/listdir" + qs(["path", path], ["sessionId", sessionId]));
48
+ },
49
+ searchFiles(query, sessionId) {
50
+ return getJson("/flyout-sidebar/search" + qs(["q", query], ["sessionId", sessionId]));
51
+ },
52
+ openInEditor(path, sessionId) {
53
+ return getJson("/flyout-sidebar/open" + qs(["path", path], ["sessionId", sessionId]));
46
54
  }
47
55
  };
48
56
  //#endregion
@@ -121,7 +129,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
121
129
  .artifacts-ptab-close:hover { background: var(--dsw-alias-interactive-bg-hover-accent, rgba(0, 0, 0, 0.08)); }
122
130
  .artifacts-preview-hide {
123
131
  flex: none; width: 32px; display: inline-flex; align-items: center; justify-content: center;
124
- border: none; border-left: 1px solid var(--dsw-alias-border-l1); background: transparent;
132
+ border: none; border-right: 1px solid var(--dsw-alias-border-l1); background: transparent;
125
133
  color: var(--dsw-alias-label-secondary); cursor: pointer; padding: 0;
126
134
  }
127
135
  .artifacts-preview-hide:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
@@ -156,6 +164,7 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
156
164
  color: var(--dsw-alias-label-secondary); cursor: pointer;
157
165
  }
158
166
  .artifacts-toggle:hover { color: var(--dsw-alias-label-primary); }
167
+ .artifacts-toggle.is-active { color: var(--dsw-alias-state-business-primary); }
159
168
  .artifacts-link { color: var(--dsw-alias-state-business-primary); text-decoration: none; padding: 4px; border-radius: 6px; display: inline-flex; align-items: center; }
160
169
  .artifacts-link:hover { background: var(--dsw-alias-interactive-bg-hover); }
161
170
  .artifacts-iconbtn {
@@ -263,6 +272,11 @@ body[data-ds-dark-theme] .gd-add { color: #69db7c; }
263
272
  body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
264
273
  .artifacts-tree { display: flex; flex-direction: column; height: 100%; min-height: 0; }
265
274
  .artifacts-tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }
275
+ .artifacts-searchbar { flex: none; padding: 8px 10px 4px; }
276
+ .artifacts-search-input { box-sizing: border-box; width: 100%; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 8px; padding: 5px 10px; outline: none; }
277
+ .artifacts-search-input:focus { border-color: var(--dsw-alias-button-primary-fill); }
278
+ .artifacts-search-input::placeholder { color: var(--dsw-alias-label-tertiary); }
279
+ .artifacts-search-dir { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-left: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
280
  .artifacts-tree-row { box-sizing: border-box; width: 100%; height: 34px; font: var(--dsw-font-s-14); color: var(--dsw-alias-label-primary); text-align: left; cursor: pointer; white-space: nowrap; background: transparent; border: none; border-radius: 8px; align-items: center; gap: 6px; padding: 0 8px; display: flex; animation: artifacts-row-in .15s var(--ds-ease-in-out, ease); }
267
281
  .artifacts-tree-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
268
282
  .artifacts-tree-dir { font: var(--dsw-font-s-strong-14); }
@@ -275,6 +289,14 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
275
289
  .artifacts-tree-copied { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary); flex: none; }
276
290
  .artifacts-tree-loading { cursor: default; color: var(--dsw-alias-label-tertiary); font-size: 12px; }
277
291
  .artifacts-tree-error { cursor: default; color: var(--dsw-alias-state-error-primary); font-size: 12px; }
292
+ .artifacts-tree-fail { padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
293
+ .artifacts-tree-fail .artifacts-tree-error { text-align: center; word-break: break-all; }
294
+ .artifacts-retry {
295
+ padding: 4px 16px; font-size: 12px; cursor: pointer; border-radius: 6px;
296
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-base);
297
+ color: var(--dsw-alias-label-primary);
298
+ }
299
+ .artifacts-retry:hover { background: var(--dsw-alias-interactive-bg-hover); }
278
300
  @keyframes artifacts-row-in { 0% { opacity: 0 } }
279
301
  .artifacts-settings { display: flex; flex-direction: column; gap: 14px; width: 100%; height: 100%; min-height: 0; overflow-y: auto; padding-bottom: 24px; }
280
302
  .artifacts-setintro { color: var(--dsw-alias-label-tertiary); margin: 0; padding: 0 2px; font-size: 13px; line-height: 20px; }
@@ -294,13 +316,22 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
294
316
  .artifacts-switch input:focus-visible + .artifacts-switch-track { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
295
317
  .artifacts-setcontrol { flex: none; align-items: center; gap: 6px; display: flex; }
296
318
  .artifacts-widthinput { width: 76px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 6px; padding: 4px 8px; }
319
+ .artifacts-langselect { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 6px; padding: 4px 8px; cursor: pointer; }
297
320
  .artifacts-suffix { color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 22px; }
298
321
  .artifacts-code { display: flex; flex-direction: column; height: 100%; min-height: 0; }
299
- .artifacts-code-scroll { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: flex-start; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); }
300
- .artifacts-code-gutter { flex: none; min-width: 2.2em; margin: 0; padding: 12px 6px 12px 8px; text-align: right; color: var(--dsw-alias-label-tertiary); border-right: 1px solid var(--dsw-alias-border-l1); position: sticky; left: 0; user-select: none; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); white-space: pre; }
301
- .artifacts-code-pre { flex: 1; margin: 0; padding: 12px; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); color: var(--shiki-foreground, var(--dsw-alias-label-primary)); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); white-space: pre; }
322
+ /* 代码视图:逐行渲染(行 = 行号 + 代码),软换行时行号仍对齐 */
323
+ .artifacts-code-scroll { flex: 1; min-height: 0; overflow: auto; padding: 12px 0; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); }
324
+ .artifacts-code-line { display: flex; align-items: flex-start; min-width: fit-content; }
325
+ .artifacts-code-gutter { flex: none; min-width: 2.2em; padding: 0 6px 0 8px; text-align: right; color: var(--dsw-alias-label-tertiary); border-right: 1px solid var(--dsw-alias-border-l1); position: sticky; left: 0; user-select: none; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
326
+ .artifacts-code-pre { flex: 1 0 auto; margin: 0; padding: 0 12px; color: var(--shiki-foreground, var(--dsw-alias-label-primary)); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); white-space: pre; }
302
327
  .artifacts-code-pre code { font: inherit; color: inherit; }
303
- .artifacts-code-line { display: block; }
328
+ .artifacts-code-wrap .artifacts-code-pre { white-space: pre-wrap; word-break: break-all; }
329
+ .artifacts-imgview { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: grab; user-select: none; }
330
+ .artifacts-imgview.is-dragging { cursor: grabbing; }
331
+ .artifacts-imgview .artifacts-img { flex: none; margin: 16px; transform-origin: center; will-change: transform; }
332
+ .artifacts-git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
333
+ .artifacts-git-adds { color: var(--p-success-fg, #1a7f37); }
334
+ .artifacts-git-dels { color: var(--p-error, #cf222e); }
304
335
  .tok-comment { color: #868e96; }
305
336
  .tok-string { color: #2f9e44; }
306
337
  .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }
@@ -385,11 +416,339 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
385
416
  return m ? (m[1] || "").toLowerCase() : "";
386
417
  }
387
418
  //#endregion
388
- //#region src/client/store.ts
389
- const basename = (p) => {
390
- const parts = String(p).split("/");
391
- return parts[parts.length - 1] || p;
419
+ //#region src/shared/i18n.js
420
+ /**
421
+ * 共享:中英双语 UI 文案。
422
+ *
423
+ * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注
424
+ * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:
425
+ * 1. tsdown 打包进 client 侧(浏览器 React bundle);
426
+ * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar
427
+ * 的经典 <script>(见 src/host/page.ts)。
428
+ *
429
+ * 语言选择:localStorage `dsh-flyout-sidebar:lang`('zh' | 'en',主面板的
430
+ * 设置项写入、弹出页读取),未设置时按浏览器语言自动判定。`t(key)` 取当前
431
+ * 语言文案,缺失时回退另一语言、再回退 key 本身。
432
+ */
433
+ /** @type {Record<string, string>} */
434
+ const ZH = {
435
+ loading: "加载中…",
436
+ loadingTree: "加载文件树…",
437
+ loadingChanges: "加载变更列表…",
438
+ loadingPdf: "加载 PDF…",
439
+ searching: "搜索中…",
440
+ emptyDir: "(空目录)",
441
+ noChanges: "没有未提交的变更",
442
+ noChangesHead: "没有未提交的变更(相对于 HEAD)",
443
+ noResults: "没有匹配的文件",
444
+ readFailed: "读取失败",
445
+ retry: "重试",
446
+ gitStatusFailed: "git status 失败",
447
+ imageLoadFailed: "图片加载失败",
448
+ pdfLoadFailed: "pdf.js 加载失败",
449
+ searchFailed: "搜索失败",
450
+ treeLoadFailed: "加载失败",
451
+ truncated: "(已截断的预览)",
452
+ copied: "已复制",
453
+ copiedPath: "已复制路径",
454
+ copiedRef: "已复制 @引用(未能写入输入框)",
455
+ insertedInput: "已插入输入框",
456
+ refBtn: "@引用",
457
+ refTitle: "引用到输入框(失败则复制 @path)",
458
+ refFlyoutTitle: "复制 @path 引用",
459
+ copyPath: "复制路径",
460
+ refInput: "@引用到输入框",
461
+ collapsePanel: "收起侧边栏",
462
+ panelAria: "文件面板",
463
+ flyoutTitle: "弹出式侧边栏",
464
+ flyoutOpen: "弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)",
465
+ resizeHandle: "拖动调整宽度",
466
+ resizePanel: "拖动调整面板宽度",
467
+ refreshTree: "刷新文件树",
468
+ refreshChanges: "刷新变更列表",
469
+ refresh: "刷新",
470
+ viewGit: "查看 Git 变更(未提交)",
471
+ backToFiles: "返回文件列表",
472
+ hidePreview: "隐藏预览(标签页保留)",
473
+ closeTab: "关闭标签页",
474
+ previewRegion: "文件预览",
475
+ diffTabPrefix: "[diff] ",
476
+ searchPlaceholder: "搜索文件…",
477
+ searchToggle: "搜索文件",
478
+ hintClickGit: "点击右侧变更文件查看 diff",
479
+ hintClickTree: "点击右侧文件查看内容",
480
+ movePanelLeft: "将文件面板移到左侧",
481
+ movePanelRight: "将文件面板移到右侧",
482
+ statusU: "未跟踪",
483
+ statusA: "新增",
484
+ statusM: "修改",
485
+ statusD: "删除",
486
+ statusR: "重命名",
487
+ statusC: "复制",
488
+ statusT: "类型变更",
489
+ staged: "(已暂存)",
490
+ unstaged: "(未暂存)",
491
+ diffDeleted: "- 删除",
492
+ diffAdded: "+ 新增",
493
+ zoomOut: "缩小",
494
+ zoomIn: "放大",
495
+ prevPage: "上一页",
496
+ nextPage: "下一页",
497
+ settingsIntro: "管理「Flyout Sidebar」的显示与行为。",
498
+ setDefaultOpen: "默认展开",
499
+ setDefaultOpenDesc: "页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。",
500
+ setAutoRefresh: "自动刷新",
501
+ setAutoRefreshDesc: "开启后侧边栏展开时将即时同步并更新产物列表",
502
+ setFileTree: "文件树",
503
+ setFileTreeDesc: "在侧边栏显示「文件树」标签页,浏览工作区目录。",
504
+ setMinWidth: "最短面板宽度",
505
+ setMinWidthDesc: "面板的最小宽度(占窗口宽度的百分比,20–60);更宽可通过拖动面板左边缘调整。",
506
+ setCodeWrap: "代码换行",
507
+ setCodeWrapDesc: "代码预览长行软换行;关闭则横向滚动。",
508
+ wordWrap: "自动换行",
509
+ openInEditor: "在系统编辑器打开",
510
+ openedInEditor: "已在编辑器打开",
511
+ openFailed: "打开失败",
512
+ setLang: "界面语言",
513
+ setLangDesc: "侧边栏与独立弹出页的显示语言;独立弹出页需刷新后生效。",
514
+ langAuto: "跟随浏览器",
515
+ langZh: "中文",
516
+ langEn: "English"
517
+ };
518
+ /** @type {Record<string, string>} */
519
+ const EN = {
520
+ loading: "Loading…",
521
+ loadingTree: "Loading file tree…",
522
+ loadingChanges: "Loading change list…",
523
+ loadingPdf: "Loading PDF…",
524
+ searching: "Searching…",
525
+ emptyDir: "(empty directory)",
526
+ noChanges: "No uncommitted changes",
527
+ noChangesHead: "No uncommitted changes (relative to HEAD)",
528
+ noResults: "No matching files",
529
+ readFailed: "Failed to read",
530
+ retry: "Retry",
531
+ gitStatusFailed: "git status failed",
532
+ imageLoadFailed: "Failed to load image",
533
+ pdfLoadFailed: "Failed to load pdf.js",
534
+ searchFailed: "Search failed",
535
+ treeLoadFailed: "Failed to load",
536
+ truncated: "(truncated preview)",
537
+ copied: "Copied",
538
+ copiedPath: "Path copied",
539
+ copiedRef: "Copied @reference (could not write to input box)",
540
+ insertedInput: "Inserted into input box",
541
+ refBtn: "@ref",
542
+ refTitle: "Quote into input box (falls back to copying @path)",
543
+ refFlyoutTitle: "Copy @path reference",
544
+ copyPath: "Copy path",
545
+ refInput: "@reference into input box",
546
+ collapsePanel: "Collapse sidebar",
547
+ panelAria: "File panel",
548
+ flyoutTitle: "Flyout sidebar",
549
+ flyoutOpen: "Flyout sidebar — open in a new tab (drag to another display)",
550
+ resizeHandle: "Drag to resize",
551
+ resizePanel: "Drag to resize panel",
552
+ refreshTree: "Refresh file tree",
553
+ refreshChanges: "Refresh change list",
554
+ refresh: "Refresh",
555
+ viewGit: "View Git changes (uncommitted)",
556
+ backToFiles: "Back to file list",
557
+ hidePreview: "Hide preview (tabs are kept)",
558
+ closeTab: "Close tab",
559
+ previewRegion: "File preview",
560
+ diffTabPrefix: "[diff] ",
561
+ searchPlaceholder: "Search files…",
562
+ searchToggle: "Search files",
563
+ hintClickGit: "Click a changed file to view its diff",
564
+ hintClickTree: "Click a file to view its content",
565
+ movePanelLeft: "Move file panel to the left",
566
+ movePanelRight: "Move file panel to the right",
567
+ statusU: "Untracked",
568
+ statusA: "Added",
569
+ statusM: "Modified",
570
+ statusD: "Deleted",
571
+ statusR: "Renamed",
572
+ statusC: "Copied",
573
+ statusT: "Type change",
574
+ staged: "(staged)",
575
+ unstaged: "(unstaged)",
576
+ diffDeleted: "- Deleted",
577
+ diffAdded: "+ Added",
578
+ zoomOut: "Zoom out",
579
+ zoomIn: "Zoom in",
580
+ prevPage: "Previous page",
581
+ nextPage: "Next page",
582
+ settingsIntro: "Manage the display and behavior of \"Flyout Sidebar\".",
583
+ setDefaultOpen: "Open by default",
584
+ setDefaultOpenDesc: "Expand the sidebar on page load; when off it stays collapsed until the corner icon is clicked.",
585
+ setAutoRefresh: "Auto refresh",
586
+ setAutoRefreshDesc: "Keep the artifact list in sync while the sidebar is open",
587
+ setFileTree: "File tree",
588
+ setFileTreeDesc: "Show the \"File tree\" tab in the sidebar to browse the workspace directory.",
589
+ setMinWidth: "Minimum panel width",
590
+ setMinWidthDesc: "Minimum width of the panel (percentage of window width, 20–60); drag the panel edge to go wider.",
591
+ setCodeWrap: "Code wrap",
592
+ setCodeWrapDesc: "Soft-wrap long lines in code previews; when off they scroll horizontally.",
593
+ wordWrap: "Word wrap",
594
+ openInEditor: "Open in system editor",
595
+ openedInEditor: "Opened in editor",
596
+ openFailed: "Failed to open",
597
+ setLang: "Interface language",
598
+ setLangDesc: "Display language for the sidebar and the standalone flyout page (flyout requires a reload).",
599
+ langAuto: "Auto (browser)",
600
+ langZh: "中文",
601
+ langEn: "English"
602
+ };
603
+ /** @type {Record<string, Record<string, string>>} */
604
+ const DICTS = {
605
+ zh: ZH,
606
+ en: EN
392
607
  };
608
+ const LANG_KEY = "dsh-flyout-sidebar:lang";
609
+ /** @type {'zh' | 'en' | null} 已显式选择的语言(null = 跟随浏览器自动判定) */
610
+ let explicitLang = null;
611
+ /** @type {Array<(lang: string) => void>} */
612
+ let listeners = [];
613
+ function readStoredLang() {
614
+ try {
615
+ var v = localStorage.getItem(LANG_KEY);
616
+ return v === "zh" || v === "en" ? v : null;
617
+ } catch (e) {
618
+ return null;
619
+ }
620
+ }
621
+ /** 浏览器语言自动判定:非 zh 开头一律英文 */
622
+ function autoLang() {
623
+ try {
624
+ var langs = navigator.languages || (navigator.language ? [navigator.language] : []);
625
+ for (var i = 0; i < langs.length; i++) {
626
+ var l = String(langs[i] || "").toLowerCase();
627
+ if (l.indexOf("zh") === 0) return "zh";
628
+ if (l.indexOf("en") === 0) return "en";
629
+ }
630
+ } catch (e) {}
631
+ return "en";
632
+ }
633
+ function currentLang() {
634
+ if (explicitLang === null) explicitLang = readStoredLang();
635
+ return explicitLang || autoLang();
636
+ }
637
+ /**
638
+ * @param {string} key 文案键
639
+ * @returns {string} 当前语言文案;两级回退(另一语言 → key)
640
+ */
641
+ function t(key) {
642
+ var lang = currentLang();
643
+ var dict = DICTS[lang] || EN;
644
+ if (dict[key] != null) return dict[key];
645
+ var other = DICTS[lang === "zh" ? "en" : "zh"];
646
+ if (!other) return key;
647
+ return other[key] != null ? other[key] : key;
648
+ }
649
+ /** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */
650
+ function getLang() {
651
+ return currentLang();
652
+ }
653
+ /** @returns {'zh' | 'en' | null} 用户显式选择的语言(null = 跟随浏览器) */
654
+ function getExplicitLang() {
655
+ if (explicitLang === null) explicitLang = readStoredLang();
656
+ return explicitLang;
657
+ }
658
+ /**
659
+ * 显式设置语言并持久化(null = 恢复跟随浏览器)。
660
+ * @param {'zh' | 'en' | null} lang
661
+ */
662
+ function setLang(lang) {
663
+ explicitLang = lang;
664
+ try {
665
+ if (lang === null) localStorage.removeItem(LANG_KEY);
666
+ else localStorage.setItem(LANG_KEY, lang);
667
+ } catch (e) {}
668
+ var next = listeners.slice();
669
+ for (var i = 0; i < next.length; i++) {
670
+ var fn = next[i];
671
+ if (!fn) continue;
672
+ try {
673
+ fn(currentLang());
674
+ } catch (e) {}
675
+ }
676
+ }
677
+ /** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */
678
+ function subscribeLang(fn) {
679
+ listeners.push(fn);
680
+ return function() {
681
+ listeners = listeners.filter(function(f) {
682
+ return f !== fn;
683
+ });
684
+ };
685
+ }
686
+ //#endregion
687
+ //#region src/shared/gitui.js
688
+ /**
689
+ * 共享:git 变更列表 / diff 的纯展示逻辑。
690
+ *
691
+ * client 侧 React 组件与独立弹出页(page.ts 内联脚本)共用,避免双端各自
692
+ * 实现后细节漂移。与 ext/highlight/markdown 一样遵守「可移植源码」约束:
693
+ * 只能使用 JSDoc 标注类型、依赖仅限本目录(i18n 的 t 在内联场景由同作用域
694
+ * 函数声明提升提供)。
695
+ */
696
+ /** @param {string} p @returns {string} */
697
+ function basename(p) {
698
+ var parts = String(p).split("/");
699
+ return parts[parts.length - 1] || p;
700
+ }
701
+ /**
702
+ * 变更文件的状态徽章字母:未跟踪为 U,否则取工作树状态(y),再退到暂存
703
+ * 状态(x),空缺按 M。
704
+ * @param {{ x: string, y: string }} e @returns {string}
705
+ */
706
+ function gitLabel(e) {
707
+ if (e.x === "?" || e.y === "?") return "U";
708
+ return (e.y !== " " ? e.y : e.x) || "M";
709
+ }
710
+ /**
711
+ * 变更文件行 hover 提示:状态名 + 暂存/未暂存。
712
+ * @param {{ x: string, y: string }} e @returns {string}
713
+ */
714
+ function gitTitle(e) {
715
+ var label = gitLabel(e);
716
+ /** @type {Record<string, string>} */
717
+ var map = {
718
+ U: t("statusU"),
719
+ A: t("statusA"),
720
+ M: t("statusM"),
721
+ D: t("statusD"),
722
+ R: t("statusR"),
723
+ C: t("statusC"),
724
+ T: t("statusT")
725
+ };
726
+ var staged = e.x !== " " && e.x !== "?";
727
+ return (map[label] || label) + (staged ? t("staged") : t("unstaged"));
728
+ }
729
+ /**
730
+ * unified diff 单行的样式类:hunk / 增 / 删 / 元信息(diff --git、index、
731
+ * rename 等),其余为普通行。
732
+ * @param {string} line @returns {string}
733
+ */
734
+ function gitDiffLineClass(line) {
735
+ var cls = "gd-line";
736
+ if (line.indexOf("@@") === 0) cls += " gd-hunk";
737
+ else if (line.charAt(0) === "+" && line.indexOf("+++") !== 0) cls += " gd-add";
738
+ else if (line.charAt(0) === "-" && line.indexOf("---") !== 0) cls += " gd-del";
739
+ else if (line.indexOf("diff ") === 0 || line.indexOf("index ") === 0 || line.indexOf("--- ") === 0 || line.indexOf("+++ ") === 0 || line.indexOf("new file") === 0 || line.indexOf("deleted file") === 0 || line.indexOf("old mode") === 0 || line.indexOf("new mode") === 0 || line.indexOf("rename ") === 0 || line.indexOf("similarity ") === 0 || line.indexOf("copy ") === 0 || line.indexOf("Binary files") === 0 || line.charAt(0) === "\\") cls += " gd-meta";
740
+ return cls;
741
+ }
742
+ //#endregion
743
+ //#region src/client/store.ts
744
+ /**
745
+ * Client 侧:共享状态与工具函数。
746
+ *
747
+ * - store / settingsStore:面板开关与功能设置(localStorage 持久化),
748
+ * 触发按钮与面板等组件通过 useOpen / useSettings 订阅。
749
+ * - currentSessionId / quoteToComposer:读取客户端会话库、把 @path 引用
750
+ * 写入会话输入框。
751
+ */
393
752
  /** 当前会话 id(读自客户端会话库;文件树把它传给 host 以定位工作区) */
394
753
  function currentSessionId() {
395
754
  try {
@@ -475,7 +834,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
475
834
  autoRefresh: true,
476
835
  minPanelWidth: 20,
477
836
  showFileTree: true,
478
- defaultOpen: true
837
+ defaultOpen: true,
838
+ codeWrap: false
479
839
  };
480
840
  function loadSettings() {
481
841
  try {
@@ -518,7 +878,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
518
878
  };
519
879
  store.open = !!settingsStore.get().defaultOpen;
520
880
  const slideState = {
521
- visible: store.open,
881
+ visible: true,
522
882
  slidOut: !store.open
523
883
  };
524
884
  const slideListeners = [];
@@ -537,22 +897,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
537
897
  fn(next);
538
898
  } catch {}
539
899
  };
540
- let slideTimer = null;
541
900
  store.subscribe((open) => {
542
- if (open) {
543
- if (slideTimer) {
544
- clearTimeout(slideTimer);
545
- slideTimer = null;
546
- }
547
- setSlide({ visible: true });
548
- requestAnimationFrame(() => requestAnimationFrame(() => setSlide({ slidOut: false })));
549
- } else {
550
- setSlide({ slidOut: true });
551
- slideTimer = setTimeout(() => {
552
- slideTimer = null;
553
- setSlide({ visible: false });
554
- }, 240);
555
- }
901
+ setSlide({ slidOut: !open });
556
902
  });
557
903
  const useSlide = () => {
558
904
  const [s, setS] = React.useState({ ...slideState });
@@ -585,6 +931,22 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
585
931
  }, []);
586
932
  return sessionId;
587
933
  }
934
+ /**
935
+ * 界面语言:订阅 i18n 的语言变更,语言切换时强制订阅组件重渲染(组件内的
936
+ * t() 调用随之取到新语言文案)。返回值用于设置区判断下拉框选项。
937
+ */
938
+ function useLang() {
939
+ const [, force] = React.useReducer((n) => n + 1, 0);
940
+ React.useEffect(() => {
941
+ const unsubscribe = subscribeLang(() => force());
942
+ return () => unsubscribe();
943
+ }, []);
944
+ return getLang();
945
+ }
946
+ /** 当前设置语言:'zh' / 'en'(显式)或 null(跟随浏览器自动判定) */
947
+ const getLanguageSetting = getExplicitLang;
948
+ /** 设置界面语言并持久化;null = 恢复跟随浏览器 */
949
+ const setLanguage = setLang;
588
950
  //#endregion
589
951
  //#region src/shared/highlight.js
590
952
  /**
@@ -841,6 +1203,67 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
841
1203
  const fn = HL_ENGINES[hlLangOf(hint)];
842
1204
  return fn ? fn(String(src)) : escHtml(src);
843
1205
  }
1206
+ /**
1207
+ * 把 highlightCode 输出的 HTML 按源码行拆分成逐行 HTML 数组。跨行 token
1208
+ * (块注释、Python 三引号串等)在行边界处闭合、下一行重新打开,保证逐行
1209
+ * 渲染时着色与整块渲染一致。逐行渲染使行号与代码天然对齐(含软换行模式),
1210
+ * 也是行级查找高亮的基础。
1211
+ * @param {string} html highlightCode 的输出
1212
+ * @returns {string[]}
1213
+ */
1214
+ function splitHlLines(html) {
1215
+ var lines = [];
1216
+ var cur = "";
1217
+ /** @type {string[]} */
1218
+ var stack = [];
1219
+ var i = 0;
1220
+ var openAll = function() {
1221
+ var s = "";
1222
+ for (var k = 0; k < stack.length; k++) s += "<span class=\"" + stack[k] + "\">";
1223
+ return s;
1224
+ };
1225
+ var closeAll = function() {
1226
+ var s = "";
1227
+ for (var k = 0; k < stack.length; k++) s += "</span>";
1228
+ return s;
1229
+ };
1230
+ while (i < html.length) {
1231
+ if (html.indexOf("<span class=\"", i) === i) {
1232
+ var end = html.indexOf(">", i);
1233
+ if (end < 0) {
1234
+ cur += html.slice(i);
1235
+ break;
1236
+ }
1237
+ cur += html.slice(i, end + 1);
1238
+ stack.push(html.slice(i + 13, end - 1));
1239
+ i = end + 1;
1240
+ continue;
1241
+ }
1242
+ if (html.indexOf("</span>", i) === i) {
1243
+ cur += "</span>";
1244
+ stack.pop();
1245
+ i += 7;
1246
+ continue;
1247
+ }
1248
+ var nl = html.indexOf("\n", i);
1249
+ var nextTag = html.indexOf("<", i);
1250
+ if (nextTag >= 0 && (nl < 0 || nextTag < nl)) {
1251
+ cur += html.slice(i, nextTag);
1252
+ i = nextTag;
1253
+ continue;
1254
+ }
1255
+ if (nl < 0) {
1256
+ cur += html.slice(i);
1257
+ break;
1258
+ }
1259
+ cur += html.slice(i, nl);
1260
+ lines.push(cur + closeAll());
1261
+ cur = openAll();
1262
+ i = nl + 1;
1263
+ }
1264
+ lines.push(cur + closeAll());
1265
+ return lines;
1266
+ }
844
1267
  //#endregion
845
1268
  //#region src/shared/markdown.js
846
1269
  /**
@@ -852,15 +1275,34 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
852
1275
  */
853
1276
  /** @param {string} s @returns {string} */
854
1277
  function mdEscape(s) {
855
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1278
+ return String(s).replace(/[\u0000\u0001]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1279
+ }
1280
+ /**
1281
+ * URL 白名单:只放行 http(s)、站内相对路径、页内锚点与 data:image/*,
1282
+ * 其余(javascript:、vbscript: 及实体编码变体等)一律替换为 '#',防止
1283
+ * 预览渲染出来的链接/图片在点击时执行脚本。
1284
+ * @param {string} u @returns {string}
1285
+ */
1286
+ function mdSafeUrl(u) {
1287
+ var s = String(u || "").replace(/[\s\u0000-\u001f]/g, "");
1288
+ if (/^https?:\/\//i.test(s)) return s;
1289
+ if (/^\/(?!\/)/.test(s) || /^\.{1,2}\//.test(s) || s.charAt(0) === "#") return s;
1290
+ if (/^data:image\/(?:png|gif|jpeg|webp|bmp|avif);/i.test(s)) return s;
1291
+ return "#";
856
1292
  }
857
1293
  /** @param {string} s @returns {string} */
858
1294
  function mdInline(s) {
859
- s = s.replace(/`([^`]+)`/g, (m, c) => "<code>" + c + "</code>");
860
- s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, "<img alt=\"$1\" src=\"$2\">");
861
- s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>");
1295
+ /** @type {string[]} */
1296
+ const codes = [];
1297
+ s = s.replace(/`([^`]+)`/g, (m, c) => {
1298
+ codes.push("<code>" + c + "</code>");
1299
+ return "\0" + (codes.length - 1) + "\0";
1300
+ });
1301
+ s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, u) => "<img alt=\"" + alt + "\" src=\"" + mdSafeUrl(u) + "\">");
1302
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, text, u) => "<a href=\"" + mdSafeUrl(u) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + text + "</a>");
862
1303
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
863
1304
  s = s.replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
1305
+ s = s.replace(/\u0000(\d+)\u0000/g, (m, i) => codes[Number(i)] ?? "");
864
1306
  return s;
865
1307
  }
866
1308
  /** @param {string} src @returns {string} HTML */
@@ -943,15 +1385,23 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
943
1385
  * 自定义 pdf.js 渲染器、Markdown / 图片 / HTML 沙箱、多类型分发。
944
1386
  */
945
1387
  function renderDiff(diff) {
946
- return /* @__PURE__ */ h("div", { className: "artifacts-diff" }, diff && diff.before != null && diff.before !== "" ? /* @__PURE__ */ h("div", { className: "artifacts-diff-block artifacts-diff-del" }, /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, "- 删除"), /* @__PURE__ */ h("pre", { className: "artifacts-diff-pre" }, diff.before)) : null, /* @__PURE__ */ h("div", { className: "artifacts-diff-block artifacts-diff-add" }, /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, "+ 新增"), /* @__PURE__ */ h("pre", { className: "artifacts-diff-pre" }, diff && diff.after != null ? diff.after : "")));
1388
+ return /* @__PURE__ */ h("div", { className: "artifacts-diff" }, diff && diff.before != null && diff.before !== "" ? /* @__PURE__ */ h("div", { className: "artifacts-diff-block artifacts-diff-del" }, /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, t("diffDeleted")), /* @__PURE__ */ h("pre", { className: "artifacts-diff-pre" }, diff.before)) : null, /* @__PURE__ */ h("div", { className: "artifacts-diff-block artifacts-diff-add" }, /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, t("diffAdded")), /* @__PURE__ */ h("pre", { className: "artifacts-diff-pre" }, diff && diff.after != null ? diff.after : "")));
947
1389
  }
948
- /** 代码预览:行号栏 + 语法高亮代码(共享高亮器,语言取自扩展名) */
949
- function CodeView({ code, path }) {
950
- const src = String(code || "");
951
- return /* @__PURE__ */ h("div", { className: "artifacts-code" }, /* @__PURE__ */ h("div", { className: "artifacts-code-scroll" }, /* @__PURE__ */ h("pre", {
1390
+ /** 代码预览:逐行渲染(行号 + 语法高亮同行内对齐,软换行也不串行)。
1391
+ * 文内查找直接用浏览器原生 Ctrl/Cmd+F(真实 DOM 文本可搜)。 */
1392
+ function CodeView({ code, path, wrap }) {
1393
+ const src = String(code || "").replace(/\n$/, "");
1394
+ const hlLines = React.useMemo(() => splitHlLines(highlightCode(src, fileExt(path || ""))), [src, path]);
1395
+ return /* @__PURE__ */ h("div", { className: "artifacts-code" + (wrap ? " artifacts-code-wrap" : "") }, /* @__PURE__ */ h("div", { className: "artifacts-code-scroll" }, hlLines.map((line, i) => /* @__PURE__ */ h("div", {
1396
+ key: i,
1397
+ className: "artifacts-code-line"
1398
+ }, /* @__PURE__ */ h("div", {
952
1399
  className: "artifacts-code-gutter",
953
1400
  "aria-hidden": "true"
954
- }, src.replace(/\n$/, "").split("\n").map((_, i) => String(i + 1)).join("\n")), /* @__PURE__ */ h("pre", { className: "artifacts-code-pre" }, /* @__PURE__ */ h("code", { dangerouslySetInnerHTML: { __html: highlightCode(src, fileExt(path || "")) } }))));
1401
+ }, i + 1), /* @__PURE__ */ h("div", {
1402
+ className: "artifacts-code-pre",
1403
+ dangerouslySetInnerHTML: { __html: line || "&nbsp;" }
1404
+ })))));
955
1405
  }
956
1406
  let pdfjsPromise = null;
957
1407
  function loadPdfjs() {
@@ -969,7 +1419,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
969
1419
  s.async = true;
970
1420
  s.onload = () => {
971
1421
  try {
972
- if (!window.pdfjsLib) throw new Error("pdf.js 加载失败");
1422
+ if (!window.pdfjsLib) throw new Error(t("pdfLoadFailed"));
973
1423
  window.pdfjsLib.GlobalWorkerOptions.workerSrc = "/flyout-sidebar/pdfjs/pdf.worker.min.js";
974
1424
  resolve(window.pdfjsLib);
975
1425
  } catch (e) {
@@ -978,7 +1428,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
978
1428
  };
979
1429
  s.onerror = () => {
980
1430
  pdfjsPromise = null;
981
- reject(/* @__PURE__ */ new Error("pdf.js 加载失败"));
1431
+ reject(new Error(t("pdfLoadFailed")));
982
1432
  };
983
1433
  document.head.appendChild(s);
984
1434
  });
@@ -1096,7 +1546,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1096
1546
  const clampPage = (n) => Math.max(1, Math.min(pageCount || 1, n));
1097
1547
  const goPage = (n) => setPageNo(clampPage(n));
1098
1548
  const zoomBy = (f) => setZoom((z) => Math.max(.25, Math.min(4, Math.round(z * f * 100) / 100)));
1099
- if (phase === "loading") return /* @__PURE__ */ h("div", { className: "artifacts-pdfview" }, /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "加载 PDF…"));
1549
+ if (phase === "loading") return /* @__PURE__ */ h("div", { className: "artifacts-pdfview" }, /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("loadingPdf")));
1100
1550
  if (phase === "error") return /* @__PURE__ */ h("embed", {
1101
1551
  className: "artifacts-pdf",
1102
1552
  src: "/flyout-sidebar/media?path=" + encodeURIComponent(path),
@@ -1107,25 +1557,25 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1107
1557
  return /* @__PURE__ */ h("div", { className: "artifacts-pdfview" }, /* @__PURE__ */ h("div", { className: "artifacts-pdfview-bar" }, /* @__PURE__ */ h("button", {
1108
1558
  type: "button",
1109
1559
  className: "artifacts-pdfview-btn",
1110
- title: "缩小",
1560
+ title: t("zoomOut"),
1111
1561
  disabled,
1112
1562
  onClick: () => zoomBy(.8)
1113
1563
  }, "−"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-zoom" }, Math.round(zoom * 100) + "%"), /* @__PURE__ */ h("button", {
1114
1564
  type: "button",
1115
1565
  className: "artifacts-pdfview-btn",
1116
- title: "放大",
1566
+ title: t("zoomIn"),
1117
1567
  disabled,
1118
1568
  onClick: () => zoomBy(1.25)
1119
1569
  }, "+"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-spacer" }), /* @__PURE__ */ h("button", {
1120
1570
  type: "button",
1121
1571
  className: "artifacts-pdfview-btn",
1122
- title: "上一页",
1572
+ title: t("prevPage"),
1123
1573
  disabled: disabled || pageNo <= 1,
1124
1574
  onClick: () => goPage(pageNo - 1)
1125
1575
  }, "‹"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-page" }, pageNo + " / " + pageCount), /* @__PURE__ */ h("button", {
1126
1576
  type: "button",
1127
1577
  className: "artifacts-pdfview-btn",
1128
- title: "下一页",
1578
+ title: t("nextPage"),
1129
1579
  disabled: disabled || pageNo >= pageCount,
1130
1580
  onClick: () => goPage(pageNo + 1)
1131
1581
  }, "›")), /* @__PURE__ */ h("div", {
@@ -1136,33 +1586,93 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1136
1586
  className: "artifacts-pdfview-canvas"
1137
1587
  })));
1138
1588
  }
1139
- /** 统一 git diff 渲染:meta/hunk/+/- 行着色,等宽可滚动 */
1589
+ /** 图片预览:滚轮缩放 + 拖拽平移 + 双击复位 */
1590
+ function ImageView({ path }) {
1591
+ const [scale, setScale] = React.useState(1);
1592
+ const [offset, setOffset] = React.useState({
1593
+ x: 0,
1594
+ y: 0
1595
+ });
1596
+ const [dragging, setDragging] = React.useState(false);
1597
+ const dragRef = React.useRef(null);
1598
+ const wrapRef = React.useRef(null);
1599
+ const src = "/flyout-sidebar/media?path=" + encodeURIComponent(path);
1600
+ React.useEffect(() => {
1601
+ const wrap = wrapRef.current;
1602
+ if (!wrap) return;
1603
+ const onWheel = (ev) => {
1604
+ ev.preventDefault();
1605
+ const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
1606
+ setScale((sc) => Math.max(.1, Math.min(12, sc * factor)));
1607
+ };
1608
+ wrap.addEventListener("wheel", onWheel, { passive: false });
1609
+ return () => wrap.removeEventListener("wheel", onWheel);
1610
+ }, []);
1611
+ React.useEffect(() => {
1612
+ if (!dragging) return;
1613
+ const onMove = (ev) => {
1614
+ const start = dragRef.current;
1615
+ if (!start) return;
1616
+ setOffset({
1617
+ x: start.ox + (ev.clientX - start.x),
1618
+ y: start.oy + (ev.clientY - start.y)
1619
+ });
1620
+ };
1621
+ const onUp = () => {
1622
+ setDragging(false);
1623
+ dragRef.current = null;
1624
+ };
1625
+ document.addEventListener("mousemove", onMove);
1626
+ document.addEventListener("mouseup", onUp);
1627
+ return () => {
1628
+ document.removeEventListener("mousemove", onMove);
1629
+ document.removeEventListener("mouseup", onUp);
1630
+ };
1631
+ }, [dragging]);
1632
+ return /* @__PURE__ */ h("div", {
1633
+ ref: wrapRef,
1634
+ className: "artifacts-imgview" + (dragging ? " is-dragging" : ""),
1635
+ onMouseDown: (ev) => {
1636
+ ev.preventDefault();
1637
+ dragRef.current = {
1638
+ x: ev.clientX,
1639
+ y: ev.clientY,
1640
+ ox: offset.x,
1641
+ oy: offset.y
1642
+ };
1643
+ setDragging(true);
1644
+ },
1645
+ onDoubleClick: () => {
1646
+ setScale(1);
1647
+ setOffset({
1648
+ x: 0,
1649
+ y: 0
1650
+ });
1651
+ }
1652
+ }, /* @__PURE__ */ h("img", {
1653
+ className: "artifacts-img",
1654
+ draggable: false,
1655
+ src,
1656
+ alt: path,
1657
+ style: { transform: "translate(" + offset.x + "px," + offset.y + "px) scale(" + scale + ")" }
1658
+ }));
1659
+ }
1660
+ /** 统一 git diff 渲染:meta/hunk/+/- 行着色,等宽可滚动;行分类与独立弹出页共用 */
1140
1661
  function GitDiffView({ diff }) {
1141
1662
  const text = String(diff || "");
1142
- if (!text) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "没有未提交的变更(相对于 HEAD)");
1143
- return /* @__PURE__ */ h("div", { className: "artifacts-gitdiff" }, text.replace(/\n$/, "").split("\n").map((line, i) => {
1144
- let cls = "gd-line";
1145
- if (line.startsWith("@@")) cls += " gd-hunk";
1146
- else if (line.startsWith("+") && !line.startsWith("+++")) cls += " gd-add";
1147
- else if (line.startsWith("-") && !line.startsWith("---")) cls += " gd-del";
1148
- else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("new file") || line.startsWith("deleted file") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("rename ") || line.startsWith("similarity ") || line.startsWith("copy ") || line.startsWith("Binary files") || line.startsWith("\\")) cls += " gd-meta";
1149
- return /* @__PURE__ */ h("div", {
1150
- key: i,
1151
- className: cls
1152
- }, line);
1153
- }));
1663
+ if (!text) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("noChangesHead"));
1664
+ return /* @__PURE__ */ h("div", { className: "artifacts-gitdiff" }, text.replace(/\n$/, "").split("\n").map((line, i) => /* @__PURE__ */ h("div", {
1665
+ key: i,
1666
+ className: gitDiffLineClass(line)
1667
+ }, line)));
1154
1668
  }
1155
- function renderPreview(p) {
1156
- if (p.loading) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "加载中…");
1157
- if (p.ok === false) return /* @__PURE__ */ h("div", { className: "artifacts-error" }, p.error || "读取失败");
1669
+ function renderPreview(p, codeWrap = false) {
1670
+ if (p.loading) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("loading"));
1671
+ if (p.ok === false) return /* @__PURE__ */ h("div", { className: "artifacts-error" }, p.error || t("readFailed"));
1158
1672
  if (p.git) return /* @__PURE__ */ h("div", { className: "artifacts-preview-body" }, /* @__PURE__ */ h(GitDiffView, { diff: p.diff }));
1159
1673
  const type = p.type || extType(p.path);
1160
1674
  let view;
1161
- if (type === "image") view = /* @__PURE__ */ h("img", {
1162
- className: "artifacts-img",
1163
- src: "/flyout-sidebar/media?path=" + encodeURIComponent(p.path || ""),
1164
- alt: p.path || ""
1165
- });
1675
+ if (type === "image") view = /* @__PURE__ */ h(ImageView, { path: p.path || "" });
1166
1676
  else if (type === "html") view = /* @__PURE__ */ h("iframe", {
1167
1677
  className: "artifacts-iframe",
1168
1678
  sandbox: "allow-scripts",
@@ -1176,8 +1686,9 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1176
1686
  });
1177
1687
  else view = /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h(CodeView, {
1178
1688
  code: p.content,
1179
- path: p.path
1180
- }), p.truncated ? /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, "(truncated preview)") : null);
1689
+ path: p.path,
1690
+ wrap: codeWrap
1691
+ }), p.truncated ? /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, t("truncated")) : null);
1181
1692
  return /* @__PURE__ */ h("div", { className: "artifacts-preview-body" }, p.diff && typeof p.diff === "object" ? renderDiff(p.diff) : null, view);
1182
1693
  }
1183
1694
  //#endregion
@@ -1335,43 +1846,139 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1335
1846
  strokeLinecap: "round",
1336
1847
  fill: "none"
1337
1848
  }));
1849
+ /** 搜索图标:放大镜(文件搜索开关) */
1850
+ const SearchIcon = ({ size }) => /* @__PURE__ */ h("svg", {
1851
+ width: size,
1852
+ height: size,
1853
+ viewBox: "0 0 16 16",
1854
+ fill: "none",
1855
+ "aria-hidden": "true"
1856
+ }, /* @__PURE__ */ h("circle", {
1857
+ cx: "7",
1858
+ cy: "7",
1859
+ r: "4.4",
1860
+ stroke: "currentColor",
1861
+ strokeWidth: 1.4
1862
+ }), /* @__PURE__ */ h("path", {
1863
+ d: "m10.4 10.4 3.1 3.1",
1864
+ stroke: "currentColor",
1865
+ strokeWidth: 1.4,
1866
+ strokeLinecap: "round"
1867
+ }));
1338
1868
  //#endregion
1339
1869
  //#region src/client/components.tsx
1340
1870
  /**
1341
1871
  * Client 侧:UI 组件 —— 文件树、多标签预览侧边面板、角落触发按钮、设置区。
1342
1872
  */
1343
- function FileTree({ onOpen, selectedPath, refreshToken }) {
1873
+ function FileTree({ onOpen, selectedPath, refreshToken, searchOpen = false, onCloseSearch }) {
1344
1874
  const [root, setRoot] = React.useState(null);
1345
1875
  const [children, setChildren] = React.useState({});
1346
1876
  const [expanded, setExpanded] = React.useState({});
1347
1877
  const [copiedPath, setCopiedPath] = React.useState(null);
1348
1878
  const [copiedLabel, setCopiedLabel] = React.useState("");
1879
+ const [query, setQuery] = React.useState("");
1880
+ const [search, setSearch] = React.useState(null);
1349
1881
  const copyTimer = React.useRef(null);
1350
1882
  const rootTimer = React.useRef(null);
1351
1883
  const sessionId = useSessionId();
1352
- const loadRoot = () => {
1353
- setChildren({});
1354
- setExpanded({});
1355
- setRoot(null);
1884
+ React.useEffect(() => {
1885
+ if (!searchOpen) setQuery("");
1886
+ }, [searchOpen]);
1887
+ React.useEffect(() => {
1888
+ const q = query.trim();
1889
+ if (!q) {
1890
+ setSearch(null);
1891
+ return;
1892
+ }
1893
+ let alive = true;
1894
+ setSearch({ loading: true });
1895
+ const timer = setTimeout(() => {
1896
+ host.searchFiles(q, currentSessionId()).then((res) => {
1897
+ if (alive) setSearch(res && res.ok ? { entries: res.entries || [] } : { error: res && res.error || t("searchFailed") });
1898
+ }).catch(() => {
1899
+ if (alive) setSearch({ error: t("searchFailed") });
1900
+ });
1901
+ }, 250);
1902
+ return () => {
1903
+ alive = false;
1904
+ clearTimeout(timer);
1905
+ };
1906
+ }, [
1907
+ query,
1908
+ sessionId,
1909
+ refreshToken
1910
+ ]);
1911
+ const fetchDir = (path) => {
1912
+ const sid = currentSessionId();
1913
+ setChildren((prev) => ({
1914
+ ...prev,
1915
+ [path]: { loading: true }
1916
+ }));
1917
+ host.listDir(path, sid).then((res) => {
1918
+ if (currentSessionId() !== sid) return;
1919
+ setChildren((prev) => ({
1920
+ ...prev,
1921
+ [path]: res && res.ok ? { entries: res.entries || [] } : { error: res && res.error || t("readFailed") }
1922
+ }));
1923
+ }).catch(() => {
1924
+ if (currentSessionId() !== sid) return;
1925
+ setChildren((prev) => ({
1926
+ ...prev,
1927
+ [path]: { error: t("readFailed") }
1928
+ }));
1929
+ });
1930
+ };
1931
+ const [rootError, setRootError] = React.useState(null);
1932
+ const loadRoot = (keepState = false) => {
1933
+ if (!keepState) {
1934
+ setChildren({});
1935
+ setExpanded({});
1936
+ setRoot(null);
1937
+ }
1938
+ setRootError(null);
1939
+ if (keepState) {
1940
+ for (const p of Object.keys(expanded)) if (expanded[p]) fetchDir(p);
1941
+ }
1356
1942
  if (rootTimer.current) clearTimeout(rootTimer.current);
1357
- const attempt = (tries) => {
1943
+ const delays = [
1944
+ 400,
1945
+ 800,
1946
+ 1600,
1947
+ 3200,
1948
+ 3200
1949
+ ];
1950
+ const sid = currentSessionId();
1951
+ const attempt = (i) => {
1358
1952
  host.listDir("", currentSessionId()).then((res) => {
1953
+ if (currentSessionId() !== sid) return;
1359
1954
  if (res && res.ok) setRoot({
1360
1955
  path: res.path || "",
1361
1956
  entries: res.entries || []
1362
1957
  });
1363
- else if (tries > 0) rootTimer.current = setTimeout(() => attempt(tries - 1), 400);
1958
+ else if (i < delays.length) rootTimer.current = setTimeout(() => attempt(i + 1), delays[i]);
1959
+ else if (!keepState) setRootError(res && res.error || t("treeLoadFailed"));
1364
1960
  }).catch(() => {
1365
- if (tries > 0) rootTimer.current = setTimeout(() => attempt(tries - 1), 400);
1961
+ if (currentSessionId() !== sid) return;
1962
+ if (i < delays.length) rootTimer.current = setTimeout(() => attempt(i + 1), delays[i]);
1963
+ else if (!keepState) setRootError(t("treeLoadFailed"));
1366
1964
  });
1367
1965
  };
1368
- attempt(3);
1966
+ attempt(0);
1369
1967
  };
1968
+ const firstTreeRender = React.useRef(true);
1370
1969
  React.useEffect(() => {
1371
1970
  loadRoot();
1372
- }, [sessionId, refreshToken]);
1971
+ }, [sessionId]);
1972
+ React.useEffect(() => {
1973
+ if (firstTreeRender.current) {
1974
+ firstTreeRender.current = false;
1975
+ return;
1976
+ }
1977
+ loadRoot(root != null);
1978
+ }, [refreshToken]);
1373
1979
  React.useEffect(() => () => {
1374
1980
  if (rootTimer.current) clearTimeout(rootTimer.current);
1981
+ if (copyTimer.current) clearTimeout(copyTimer.current);
1375
1982
  }, []);
1376
1983
  const toggle = (path) => {
1377
1984
  const nextExpanded = {
@@ -1379,27 +1986,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1379
1986
  [path]: !expanded[path]
1380
1987
  };
1381
1988
  setExpanded(nextExpanded);
1382
- if (nextExpanded[path] && !children[path]) {
1383
- setChildren({
1384
- ...children,
1385
- [path]: { loading: true }
1386
- });
1387
- host.listDir(path, currentSessionId()).then((res) => {
1388
- setChildren((prev) => ({
1389
- ...prev,
1390
- [path]: res && res.ok ? { entries: res.entries || [] } : { error: res && res.error || "读取失败" }
1391
- }));
1392
- }).catch(() => {
1393
- setChildren((prev) => ({
1394
- ...prev,
1395
- [path]: { error: "读取失败" }
1396
- }));
1397
- });
1398
- }
1989
+ if (nextExpanded[path] && !children[path]) fetchDir(path);
1399
1990
  };
1400
1991
  const copyRef = (path) => {
1401
1992
  const text = "@" + path;
1402
- let label = "已复制";
1993
+ let label = t("copied");
1403
1994
  const done = () => {
1404
1995
  setCopiedPath(path);
1405
1996
  setCopiedLabel(label);
@@ -1410,7 +2001,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1410
2001
  }, 1600);
1411
2002
  };
1412
2003
  if (quoteToComposer(path)) {
1413
- label = "已插入输入框";
2004
+ label = t("insertedInput");
1414
2005
  done();
1415
2006
  return;
1416
2007
  }
@@ -1423,15 +2014,15 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1423
2014
  done();
1424
2015
  }
1425
2016
  };
1426
- const rowActions = (entry) => copiedPath === entry.path ? /* @__PURE__ */ h("span", { className: "artifacts-tree-copied" }, copiedLabel || "已复制") : /* @__PURE__ */ h("button", {
2017
+ const rowActions = (entry) => copiedPath === entry.path ? /* @__PURE__ */ h("span", { className: "artifacts-tree-copied" }, copiedLabel || t("copied")) : /* @__PURE__ */ h("button", {
1427
2018
  type: "button",
1428
2019
  className: "artifacts-tree-ref",
1429
- title: "引用到输入框(失败则复制 @path)",
2020
+ title: t("refTitle"),
1430
2021
  onClick: (e) => {
1431
2022
  e.stopPropagation();
1432
2023
  copyRef(entry.path);
1433
2024
  }
1434
- }, "@引用");
2025
+ }, t("refBtn"));
1435
2026
  const renderNode = (entry, depth, flashDelay) => {
1436
2027
  const pad = { paddingLeft: 6 + depth * 20 };
1437
2028
  const isSelected = selectedPath === entry.path;
@@ -1459,7 +2050,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1459
2050
  }, isExpanded ? /* @__PURE__ */ h(FolderOpenIcon, { size: 14 }) : /* @__PURE__ */ h(FolderClosedIcon, { size: 14 }), /* @__PURE__ */ h("span", { className: "artifacts-tree-name" }, entry.name), rowActions(entry)), isExpanded ? node && node.loading ? /* @__PURE__ */ h("div", {
1460
2051
  className: "artifacts-tree-row artifacts-tree-loading",
1461
2052
  style: { paddingLeft: 6 + (depth + 1) * 20 + 20 }
1462
- }, "加载中…") : node && node.error ? /* @__PURE__ */ h("div", {
2053
+ }, t("loading")) : node && node.error ? /* @__PURE__ */ h("div", {
1463
2054
  className: "artifacts-tree-row artifacts-tree-error",
1464
2055
  style: { paddingLeft: 6 + (depth + 1) * 20 + 20 }
1465
2056
  }, node.error) : node && node.entries ? node.entries.map((c) => renderNode(c, depth + 1)) : null : null);
@@ -1484,15 +2075,64 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1484
2075
  title: entry.path
1485
2076
  }, /* @__PURE__ */ h(FileCodeIcon, { size: 14 }), /* @__PURE__ */ h("span", { className: "artifacts-tree-name" }, entry.name), rowActions(entry));
1486
2077
  };
1487
- return /* @__PURE__ */ h("div", { className: "artifacts-tree" }, /* @__PURE__ */ h("div", { className: "artifacts-tree-body" }, !root ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "加载文件树…") : !root.entries || !root.entries.length ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, "(空目录)") : root.entries.map((e, i) => renderNode(e, 0, Math.min(i, 12) * 45))));
2078
+ const renderSearchRow = (path, idx) => {
2079
+ const entry = {
2080
+ name: basename(path),
2081
+ path,
2082
+ isDir: false,
2083
+ hidden: false
2084
+ };
2085
+ const slash = path.lastIndexOf("/");
2086
+ const dir = slash >= 0 ? path.slice(0, slash) : "";
2087
+ return /* @__PURE__ */ h("div", {
2088
+ key: path,
2089
+ role: "button",
2090
+ tabIndex: 0,
2091
+ className: "artifacts-tree-row artifacts-flash-in" + (selectedPath === path ? " is-selected" : ""),
2092
+ style: { animationDelay: Math.min(idx, 12) * 30 + "ms" },
2093
+ onClick: () => {
2094
+ if (onOpen) onOpen(path);
2095
+ },
2096
+ onKeyDown: (ev) => {
2097
+ if (ev.key === "Enter" || ev.key === " ") {
2098
+ ev.preventDefault();
2099
+ if (onOpen) onOpen(path);
2100
+ }
2101
+ },
2102
+ title: path
2103
+ }, /* @__PURE__ */ h(FileCodeIcon, { size: 14 }), /* @__PURE__ */ h("span", { className: "artifacts-tree-name" }, entry.name, dir ? /* @__PURE__ */ h("span", { className: "artifacts-search-dir" }, dir) : null), rowActions(entry));
2104
+ };
2105
+ return /* @__PURE__ */ h("div", { className: "artifacts-tree" }, searchOpen ? /* @__PURE__ */ h("div", { className: "artifacts-searchbar" }, /* @__PURE__ */ h("input", {
2106
+ type: "text",
2107
+ className: "artifacts-search-input",
2108
+ placeholder: t("searchPlaceholder"),
2109
+ value: query,
2110
+ autoFocus: true,
2111
+ onChange: (e) => setQuery(e.currentTarget.value),
2112
+ onKeyDown: (ev) => {
2113
+ if (ev.key === "Escape") {
2114
+ if (onCloseSearch) onCloseSearch();
2115
+ else setQuery("");
2116
+ }
2117
+ }
2118
+ })) : null, /* @__PURE__ */ h("div", {
2119
+ className: "artifacts-tree-body",
2120
+ key: refreshToken
2121
+ }, query.trim() ? search && search.loading ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("searching")) : search && search.error ? /* @__PURE__ */ h("div", { className: "artifacts-tree-error artifacts-git-error" }, search.error) : search && search.entries && search.entries.length ? search.entries.map((p, i) => renderSearchRow(p, i)) : /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("noResults")) : !root ? rootError ? /* @__PURE__ */ h("div", { className: "artifacts-tree-fail" }, /* @__PURE__ */ h("div", { className: "artifacts-tree-error artifacts-git-error" }, rootError), /* @__PURE__ */ h("button", {
2122
+ type: "button",
2123
+ className: "artifacts-retry",
2124
+ onClick: () => loadRoot(false)
2125
+ }, t("retry"))) : /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("loadingTree")) : !root.entries || !root.entries.length ? /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("emptyDir")) : root.entries.map((e, i) => renderNode(e, 0, Math.min(i, 12) * 45))));
1488
2126
  }
1489
- function ArtifactsPanel() {
1490
- const open = useOpen();
1491
- const settings = useSettings();
2127
+ const TABS_KEY = "dsh-flyout-sidebar:tabs";
2128
+ /**
2129
+ * 多标签预览状态('p:' 前缀 = 内容预览,'g:' 前缀 = git diff):打开/关闭/
2130
+ * 会话切换清空/持久化与恢复集中在此 hook,面板组件只消费结果。
2131
+ */
2132
+ function usePreviewTabs(sessionId) {
1492
2133
  const [tabs, setTabs] = React.useState([]);
1493
2134
  const [activeKey, setActiveKey] = React.useState(null);
1494
2135
  const [previewHidden, setPreviewHidden] = React.useState(false);
1495
- const sessionId = useSessionId();
1496
2136
  const firstSession = React.useRef(true);
1497
2137
  React.useEffect(() => {
1498
2138
  if (firstSession.current) {
@@ -1502,18 +2142,219 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1502
2142
  setTabs([]);
1503
2143
  setActiveKey(null);
1504
2144
  setPreviewHidden(false);
2145
+ try {
2146
+ sessionStorage.removeItem(TABS_KEY);
2147
+ } catch {}
2148
+ }, [sessionId]);
2149
+ const restoredSid = React.useRef(null);
2150
+ React.useEffect(() => {
2151
+ if (restoredSid.current !== sessionId) return;
2152
+ try {
2153
+ if (!tabs.length) sessionStorage.removeItem(TABS_KEY);
2154
+ else sessionStorage.setItem(TABS_KEY, JSON.stringify({
2155
+ sid: sessionId,
2156
+ tabs: tabs.map((tb) => ({
2157
+ key: tb.key,
2158
+ path: tb.path,
2159
+ git: tb.git
2160
+ })),
2161
+ activeKey
2162
+ }));
2163
+ } catch {}
2164
+ }, [
2165
+ tabs,
2166
+ activeKey,
2167
+ sessionId
2168
+ ]);
2169
+ const patchTab = (key, patch) => setTabs((prev) => prev.map((tb) => tb.key === key ? {
2170
+ ...tb,
2171
+ ...patch
2172
+ } : tb));
2173
+ const reqTokens = React.useRef({});
2174
+ const openTab = (key, path, git, initial) => {
2175
+ setPreviewHidden(false);
2176
+ reqTokens.current[key] = (reqTokens.current[key] || 0) + 1;
2177
+ setTabs((prev) => {
2178
+ const i = prev.findIndex((tb) => tb.key === key);
2179
+ if (i >= 0) {
2180
+ const next = prev.slice();
2181
+ const cur = next[i];
2182
+ if (cur) next[i] = {
2183
+ ...cur,
2184
+ ...initial,
2185
+ ok: void 0,
2186
+ error: void 0
2187
+ };
2188
+ return next;
2189
+ }
2190
+ return prev.concat([{
2191
+ key,
2192
+ path,
2193
+ git,
2194
+ ...initial
2195
+ }]);
2196
+ });
2197
+ setActiveKey(key);
2198
+ };
2199
+ const closeTab = (key) => {
2200
+ setTabs((prev) => {
2201
+ prev.findIndex((tb) => tb.key === key);
2202
+ return prev.filter((tb) => tb.key !== key);
2203
+ });
2204
+ setActiveKey((cur) => {
2205
+ if (cur !== key) return cur;
2206
+ const remaining = tabsRef.current.filter((tb) => tb.key !== key);
2207
+ if (!remaining.length) return null;
2208
+ const closedIdx = tabsRef.current.findIndex((tb) => tb.key === key);
2209
+ return remaining[Math.min(Math.max(closedIdx, 0), remaining.length - 1)]?.key ?? null;
2210
+ });
2211
+ };
2212
+ const tabsRef = React.useRef([]);
2213
+ React.useEffect(() => {
2214
+ tabsRef.current = tabs;
2215
+ }, [tabs]);
2216
+ const openFile = (path) => {
2217
+ const key = "p:" + path;
2218
+ const type = extType(path);
2219
+ const initial = {
2220
+ loading: false,
2221
+ type,
2222
+ diff: null
2223
+ };
2224
+ if (type !== "image" && type !== "pdf") initial.loading = true;
2225
+ openTab(key, path, false, initial);
2226
+ if (type === "image" || type === "pdf") return;
2227
+ const myReq = reqTokens.current[key];
2228
+ host.readArtifact(path, sessionId).then((res) => {
2229
+ if (reqTokens.current[key] !== myReq) return;
2230
+ patchTab(key, {
2231
+ loading: false,
2232
+ ...res
2233
+ });
2234
+ }).catch((e) => {
2235
+ if (reqTokens.current[key] !== myReq) return;
2236
+ patchTab(key, {
2237
+ loading: false,
2238
+ ok: false,
2239
+ error: String(e instanceof Error && e.message ? e.message : e)
2240
+ });
2241
+ });
2242
+ };
2243
+ const openGitDiff = (path) => {
2244
+ const key = "g:" + path;
2245
+ openTab(key, path, true, { loading: true });
2246
+ const myReq = reqTokens.current[key];
2247
+ host.gitDiff(path, sessionId).then((res) => {
2248
+ if (reqTokens.current[key] !== myReq) return;
2249
+ patchTab(key, {
2250
+ loading: false,
2251
+ ...res
2252
+ });
2253
+ }).catch((e) => {
2254
+ if (reqTokens.current[key] !== myReq) return;
2255
+ patchTab(key, {
2256
+ loading: false,
2257
+ ok: false,
2258
+ error: String(e instanceof Error && e.message ? e.message : e)
2259
+ });
2260
+ });
2261
+ };
2262
+ React.useEffect(() => {
2263
+ if (restoredSid.current === sessionId) return;
2264
+ restoredSid.current = sessionId;
2265
+ if (sessionId == null) return;
2266
+ try {
2267
+ const raw = sessionStorage.getItem(TABS_KEY);
2268
+ if (!raw) return;
2269
+ const saved = JSON.parse(raw);
2270
+ const sid = saved.sid;
2271
+ const list = saved.tabs;
2272
+ if (sid !== sessionId || !Array.isArray(list)) return;
2273
+ for (const st of list) {
2274
+ if (typeof st?.key !== "string" || typeof st?.path !== "string") continue;
2275
+ if (st.git) openGitDiff(st.path);
2276
+ else openFile(st.path);
2277
+ }
2278
+ const savedActive = saved.activeKey;
2279
+ if (typeof savedActive === "string") setActiveKey(savedActive);
2280
+ } catch {}
1505
2281
  }, [sessionId]);
2282
+ return {
2283
+ tabs,
2284
+ activeKey,
2285
+ setActiveKey,
2286
+ previewHidden,
2287
+ setPreviewHidden,
2288
+ openFile,
2289
+ openGitDiff,
2290
+ closeTab
2291
+ };
2292
+ }
2293
+ /** git 变更列表(文件面板的「变更」视图) */
2294
+ function GitChangesList({ files, error, activeDiffPath, flashKey, onOpen, onCopyPath, onQuote }) {
2295
+ const rows = [];
2296
+ if (error) rows.push(/* @__PURE__ */ h("div", {
2297
+ key: "err",
2298
+ className: "artifacts-tree-error artifacts-git-error"
2299
+ }, error));
2300
+ else if (files == null) rows.push(/* @__PURE__ */ h("div", {
2301
+ key: "load",
2302
+ className: "artifacts-empty"
2303
+ }, t("loadingChanges")));
2304
+ else if (!files.length) rows.push(/* @__PURE__ */ h("div", {
2305
+ key: "empty",
2306
+ className: "artifacts-empty"
2307
+ }, t("noChanges")));
2308
+ (files || []).forEach((e, idx) => {
2309
+ const label = gitLabel(e);
2310
+ const isActive = activeDiffPath === e.path;
2311
+ const flashPrefix = flashKey > 0 ? flashKey + ":" : "";
2312
+ rows.push(/* @__PURE__ */ h("div", {
2313
+ key: flashPrefix + e.path,
2314
+ className: "artifacts-item" + (isActive ? " is-active" : "") + (flashPrefix ? " artifacts-flash-in" : ""),
2315
+ style: flashPrefix ? { animationDelay: Math.min(idx, 12) * 45 + "ms" } : void 0
2316
+ }, /* @__PURE__ */ h("button", {
2317
+ type: "button",
2318
+ className: "artifacts-item-main",
2319
+ title: gitTitle(e),
2320
+ onClick: () => onOpen(e.path)
2321
+ }, /* @__PURE__ */ h("div", { className: "artifacts-item-row" }, /* @__PURE__ */ h("span", { className: "artifacts-git-badge artifacts-git-badge-" + label }, label), /* @__PURE__ */ h("span", { className: "artifacts-item-base" }, basename(e.path)), typeof e.adds === "number" && (e.adds > 0 || (e.dels ?? 0) > 0) ? /* @__PURE__ */ h("span", { className: "artifacts-git-stats" }, /* @__PURE__ */ h("span", { className: "artifacts-git-adds" }, "+", e.adds), /* @__PURE__ */ h("span", { className: "artifacts-git-dels" }, "−", e.dels ?? 0)) : null, e.origPath ? /* @__PURE__ */ h("span", { className: "artifacts-git-orig" }, "← ", basename(e.origPath)) : null), /* @__PURE__ */ h("div", { className: "artifacts-item-full" }, e.path)), /* @__PURE__ */ h("div", { className: "artifacts-item-actions" }, /* @__PURE__ */ h("button", {
2322
+ type: "button",
2323
+ className: "artifacts-minibtn",
2324
+ title: t("copyPath"),
2325
+ onClick: () => onCopyPath(e.path)
2326
+ }, "⧉"), /* @__PURE__ */ h("button", {
2327
+ type: "button",
2328
+ className: "artifacts-minibtn",
2329
+ title: t("refInput"),
2330
+ onClick: () => onQuote(e.path)
2331
+ }, "@"))));
2332
+ });
2333
+ return /* @__PURE__ */ h(Fragment, null, rows);
2334
+ }
2335
+ function ArtifactsPanel() {
2336
+ const open = useOpen();
2337
+ const settings = useSettings();
2338
+ useLang();
2339
+ const sessionId = useSessionId();
2340
+ const { tabs, activeKey, setActiveKey, previewHidden, setPreviewHidden, openFile, openGitDiff, closeTab } = usePreviewTabs(sessionId);
2341
+ const activeTab = tabs.find((t) => t.key === activeKey) || null;
1506
2342
  const [notice, setNotice] = React.useState("");
2343
+ const [winW, setWinW] = React.useState(() => typeof window !== "undefined" ? window.innerWidth : 1400);
1507
2344
  const [gitFiles, setGitFiles] = React.useState(null);
1508
2345
  const [gitError, setGitError] = React.useState(null);
1509
- const [panelWidth, setPanelWidth] = React.useState(null);
2346
+ const [panelFrac, setPanelFrac] = React.useState(null);
1510
2347
  const [resizing, setResizing] = React.useState(false);
1511
2348
  const [activeView, setActiveView] = React.useState(() => settings.showFileTree ? "tree" : "git");
1512
2349
  const [treeRefresh, setTreeRefresh] = React.useState(0);
2350
+ const [searchOpen, setSearchOpen] = React.useState(false);
1513
2351
  const [gitRefresh, setGitRefresh] = React.useState(0);
1514
2352
  const [gitRefreshing, setGitRefreshing] = React.useState(false);
1515
2353
  const [gitFlash, setGitFlash] = React.useState(0);
1516
2354
  const noticeTimer = React.useRef(null);
2355
+ React.useEffect(() => () => {
2356
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
2357
+ }, []);
1517
2358
  const gitForceRef = React.useRef(false);
1518
2359
  React.useEffect(() => {
1519
2360
  if (!open || activeView !== "git") return;
@@ -1532,7 +2373,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1532
2373
  setGitError(null);
1533
2374
  } else {
1534
2375
  setGitFiles([]);
1535
- setGitError(res && res.error || "git status 失败");
2376
+ setGitError(res && res.error || t("gitStatusFailed"));
1536
2377
  }
1537
2378
  }).catch((e) => {
1538
2379
  if (force) setGitRefreshing(false);
@@ -1595,8 +2436,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1595
2436
  if (document.body) obs.observe(document.body, opts);
1596
2437
  return () => obs.disconnect();
1597
2438
  }, []);
1598
- const minWidthPx = Math.max(80, Math.round(window.innerWidth * (settings.minPanelWidth || 0) / 100));
1599
- const widthPx = panelWidth != null ? Math.max(panelWidth, minWidthPx) : minWidthPx;
2439
+ React.useEffect(() => {
2440
+ const onResize = () => setWinW(window.innerWidth);
2441
+ window.addEventListener("resize", onResize);
2442
+ return () => window.removeEventListener("resize", onResize);
2443
+ }, []);
2444
+ const rightOffset = (() => {
2445
+ const n = parseFloat(document.documentElement.style.getPropertyValue("--dsh-sidebar-width"));
2446
+ return Number.isFinite(n) ? n : 0;
2447
+ })();
2448
+ const avail = Math.max(120, winW - rightOffset);
2449
+ const minWidthPx = Math.max(80, Math.round(winW * (settings.minPanelWidth || 0) / 100));
2450
+ const widthPx = panelFrac != null ? Math.max(minWidthPx, Math.round(panelFrac * avail)) : minWidthPx;
1600
2451
  React.useEffect(() => {
1601
2452
  const root = document.documentElement;
1602
2453
  root.style.setProperty("--dsh-flyout-sidebar-width", open ? widthPx + "px" : "0px");
@@ -1611,26 +2462,36 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1611
2462
  document.body.removeAttribute("data-dsh-flyout-dragging");
1612
2463
  };
1613
2464
  }, [resizing]);
2465
+ const resizeHandlers = React.useRef(null);
2466
+ React.useEffect(() => () => {
2467
+ const h = resizeHandlers.current;
2468
+ if (h) {
2469
+ document.removeEventListener("mousemove", h.onMove);
2470
+ document.removeEventListener("mouseup", h.onUp);
2471
+ }
2472
+ }, []);
1614
2473
  const { visible, slidOut } = useSlide();
1615
2474
  if (!visible) return null;
1616
2475
  const flyoutHref = "/flyout-sidebar" + (sessionId ? "?sessionId=" + encodeURIComponent(sessionId) : "");
1617
2476
  const startResize = (e) => {
1618
2477
  e.preventDefault();
1619
2478
  setResizing(true);
1620
- const rightOffset = (() => {
1621
- const v = document.documentElement.style.getPropertyValue("--dsh-sidebar-width");
1622
- const n = parseFloat(v);
1623
- return Number.isFinite(n) ? n : 0;
1624
- })();
2479
+ const availAtStart = avail;
1625
2480
  const onMove = (ev) => {
1626
2481
  const w = window.innerWidth - ev.clientX - rightOffset;
1627
- setPanelWidth(Math.max(minWidthPx, Math.min(w, window.innerWidth - rightOffset - 24)));
2482
+ const frac = Math.max(minWidthPx / availAtStart, Math.min(w / availAtStart, (availAtStart - 24) / availAtStart));
2483
+ setPanelFrac(frac);
1628
2484
  };
1629
2485
  const onUp = () => {
1630
2486
  setResizing(false);
2487
+ resizeHandlers.current = null;
1631
2488
  document.removeEventListener("mousemove", onMove);
1632
2489
  document.removeEventListener("mouseup", onUp);
1633
2490
  };
2491
+ resizeHandlers.current = {
2492
+ onMove,
2493
+ onUp
2494
+ };
1634
2495
  document.addEventListener("mousemove", onMove);
1635
2496
  document.addEventListener("mouseup", onUp);
1636
2497
  };
@@ -1652,162 +2513,53 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1652
2513
  };
1653
2514
  const quotePath = (path) => {
1654
2515
  if (quoteToComposer(path)) {
1655
- flash("已插入输入框");
2516
+ flash(t("insertedInput"));
1656
2517
  return;
1657
2518
  }
1658
- copyText("@" + path, "已复制 @引用(未能写入输入框)");
1659
- };
1660
- const patchTab = (key, patch) => setTabs((prev) => prev.map((t) => t.key === key ? {
1661
- ...t,
1662
- ...patch
1663
- } : t));
1664
- const openTab = (key, path, git, initial) => {
1665
- setPreviewHidden(false);
1666
- setTabs((prev) => {
1667
- const i = prev.findIndex((t) => t.key === key);
1668
- if (i >= 0) {
1669
- const next = prev.slice();
1670
- const cur = next[i];
1671
- if (cur) next[i] = {
1672
- ...cur,
1673
- ...initial
1674
- };
1675
- return next;
1676
- }
1677
- return prev.concat([{
1678
- key,
1679
- path,
1680
- git,
1681
- ...initial
1682
- }]);
1683
- });
1684
- setActiveKey(key);
1685
- };
1686
- const closeTab = (key) => {
1687
- const idx = tabs.findIndex((t) => t.key === key);
1688
- const next = tabs.filter((t) => t.key !== key);
1689
- setTabs(next);
1690
- if (activeKey === key) setActiveKey(next.length ? next[Math.min(idx, next.length - 1)]?.key ?? null : null);
2519
+ copyText("@" + path, t("copiedRef"));
1691
2520
  };
1692
- const activeTab = tabs.find((t) => t.key === activeKey) || null;
1693
- const openFile = (path) => {
1694
- const key = "p:" + path;
1695
- const type = extType(path);
1696
- const initial = {
1697
- loading: false,
1698
- type,
1699
- diff: null
1700
- };
1701
- if (type !== "image" && type !== "pdf") initial.loading = true;
1702
- openTab(key, path, false, initial);
1703
- if (type === "image" || type === "pdf") return;
1704
- host.readArtifact(path).then((res) => {
1705
- patchTab(key, {
1706
- loading: false,
1707
- ...res
1708
- });
1709
- }).catch((e) => {
1710
- patchTab(key, {
1711
- loading: false,
1712
- ok: false,
1713
- error: String(e instanceof Error && e.message ? e.message : e)
1714
- });
1715
- });
1716
- };
1717
- const openGitDiff = (path) => {
1718
- const key = "g:" + path;
1719
- openTab(key, path, true, { loading: true });
1720
- host.gitDiff(path, currentSessionId()).then((res) => {
1721
- patchTab(key, {
1722
- loading: false,
1723
- ...res
1724
- });
1725
- }).catch((e) => {
1726
- patchTab(key, {
1727
- loading: false,
1728
- ok: false,
1729
- error: String(e instanceof Error && e.message ? e.message : e)
1730
- });
1731
- });
1732
- };
1733
- const gitLabel = (e) => {
1734
- if (e.x === "?" || e.y === "?") return "U";
1735
- return (e.y !== " " ? e.y : e.x) || "M";
1736
- };
1737
- const gitTitle = (e) => {
1738
- const label = gitLabel(e);
1739
- const map = {
1740
- U: "未跟踪",
1741
- A: "新增",
1742
- M: "修改",
1743
- D: "删除",
1744
- R: "重命名",
1745
- C: "复制"
2521
+ React.useEffect(() => {
2522
+ if (!open) return;
2523
+ const onKey = (ev) => {
2524
+ if (ev.key !== "Escape") return;
2525
+ const target = ev.target;
2526
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
2527
+ if (tabs.length && !previewHidden) {
2528
+ if (activeKey) closeTab(activeKey);
2529
+ else setPreviewHidden(true);
2530
+ } else store.setOpen(false);
1746
2531
  };
1747
- const staged = e.x !== " " && e.x !== "?";
1748
- return (map[label] || label) + (staged ? "(已暂存)" : "(未暂存)");
1749
- };
1750
- const gitListRows = [];
1751
- if (gitError) gitListRows.push(/* @__PURE__ */ h("div", {
1752
- key: "err",
1753
- className: "artifacts-tree-error artifacts-git-error"
1754
- }, gitError));
1755
- else if (gitFiles == null) gitListRows.push(/* @__PURE__ */ h("div", {
1756
- key: "load",
1757
- className: "artifacts-empty"
1758
- }, "加载变更列表…"));
1759
- else if (!gitFiles.length) gitListRows.push(/* @__PURE__ */ h("div", {
1760
- key: "empty",
1761
- className: "artifacts-empty"
1762
- }, "没有未提交的变更"));
1763
- (gitFiles || []).forEach((e, idx) => {
1764
- const label = gitLabel(e);
1765
- const isActive = !!(activeTab && activeTab.git && activeTab.path === e.path);
1766
- const flashKey = gitFlash > 0 ? gitFlash + ":" : "";
1767
- gitListRows.push(/* @__PURE__ */ h("div", {
1768
- key: flashKey + e.path,
1769
- className: "artifacts-item" + (isActive ? " is-active" : "") + (flashKey ? " artifacts-flash-in" : ""),
1770
- style: flashKey ? { animationDelay: Math.min(idx, 12) * 45 + "ms" } : void 0
1771
- }, /* @__PURE__ */ h("button", {
1772
- type: "button",
1773
- className: "artifacts-item-main",
1774
- title: gitTitle(e),
1775
- onClick: () => openGitDiff(e.path)
1776
- }, /* @__PURE__ */ h("div", { className: "artifacts-item-row" }, /* @__PURE__ */ h("span", { className: "artifacts-git-badge artifacts-git-badge-" + label }, label), /* @__PURE__ */ h("span", { className: "artifacts-item-base" }, basename(e.path)), e.origPath ? /* @__PURE__ */ h("span", { className: "artifacts-git-orig" }, "← ", basename(e.origPath)) : null), /* @__PURE__ */ h("div", { className: "artifacts-item-full" }, e.path)), /* @__PURE__ */ h("div", { className: "artifacts-item-actions" }, /* @__PURE__ */ h("button", {
1777
- type: "button",
1778
- className: "artifacts-minibtn",
1779
- title: "复制路径",
1780
- onClick: () => copyText(e.path, "已复制路径")
1781
- }, "⧉"), /* @__PURE__ */ h("button", {
1782
- type: "button",
1783
- className: "artifacts-minibtn",
1784
- title: "@引用到输入框",
1785
- onClick: () => quotePath(e.path)
1786
- }, "@"))));
1787
- });
2532
+ document.addEventListener("keydown", onKey);
2533
+ return () => document.removeEventListener("keydown", onKey);
2534
+ }, [
2535
+ open,
2536
+ tabs,
2537
+ previewHidden,
2538
+ activeKey
2539
+ ]);
1788
2540
  const previewOverlay = tabs.length && !previewHidden ? /* @__PURE__ */ h("div", {
1789
2541
  className: "artifacts-preview-overlay" + (slidOut ? " artifacts-slid-out" : ""),
1790
2542
  role: "region",
1791
- "aria-label": "文件预览"
1792
- }, /* @__PURE__ */ h("div", { className: "artifacts-preview-overlay-tabs" }, /* @__PURE__ */ h("div", { className: "artifacts-ptabs-scroll" }, tabs.map((t) => /* @__PURE__ */ h("div", {
1793
- key: t.key,
1794
- className: "artifacts-ptab" + (t.key === activeKey ? " is-active" : ""),
1795
- title: (t.git ? "[diff] " : "") + (t.path || ""),
1796
- onClick: () => setActiveKey(t.key)
1797
- }, /* @__PURE__ */ h("span", { className: "artifacts-ptab-name" }, basename(t.path || "")), /* @__PURE__ */ h("button", {
2543
+ "aria-label": t("previewRegion")
2544
+ }, /* @__PURE__ */ h("div", { className: "artifacts-preview-overlay-tabs" }, /* @__PURE__ */ h("div", { className: "artifacts-ptabs-scroll" }, tabs.map((tab) => /* @__PURE__ */ h("div", {
2545
+ key: tab.key,
2546
+ className: "artifacts-ptab" + (tab.key === activeKey ? " is-active" : ""),
2547
+ title: (tab.git ? t("diffTabPrefix") : "") + (tab.path || ""),
2548
+ onClick: () => setActiveKey(tab.key)
2549
+ }, /* @__PURE__ */ h("span", { className: "artifacts-ptab-name" }, basename(tab.path || "")), /* @__PURE__ */ h("button", {
1798
2550
  type: "button",
1799
2551
  className: "artifacts-ptab-close",
1800
- title: "关闭标签页",
2552
+ title: t("closeTab"),
1801
2553
  onClick: (e) => {
1802
2554
  e.stopPropagation();
1803
- closeTab(t.key);
2555
+ closeTab(tab.key);
1804
2556
  }
1805
2557
  }, "×")))), /* @__PURE__ */ h("button", {
1806
2558
  type: "button",
1807
2559
  className: "artifacts-preview-hide",
1808
- title: "隐藏预览(标签页保留)",
2560
+ title: t("hidePreview"),
1809
2561
  onClick: () => setPreviewHidden(true)
1810
- }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab) : null) : null;
2562
+ }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null) : null;
1811
2563
  return /* @__PURE__ */ h(Fragment, null, previewOverlay, /* @__PURE__ */ h("div", {
1812
2564
  className: "artifacts-panel" + (slidOut ? " artifacts-slid-out" : "") + (resizing ? " artifacts-resizing" : ""),
1813
2565
  style: { width: widthPx },
@@ -1815,23 +2567,29 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1815
2567
  "aria-label": "Artifacts"
1816
2568
  }, /* @__PURE__ */ h("div", {
1817
2569
  className: "artifacts-resize",
1818
- title: "拖动调整宽度",
2570
+ title: t("resizeHandle"),
1819
2571
  onMouseDown: startResize
1820
2572
  }), /* @__PURE__ */ h("div", { className: "artifacts-head" }, /* @__PURE__ */ h("div", { className: "artifacts-head-left" }, /* @__PURE__ */ h("button", {
1821
2573
  type: "button",
1822
2574
  className: "artifacts-toggle",
1823
- title: "收起侧边栏",
2575
+ title: t("collapsePanel"),
1824
2576
  onClick: () => store.setOpen(false)
1825
2577
  }, /* @__PURE__ */ h(PanelIcon, { size: 16 })), /* @__PURE__ */ h("a", {
1826
2578
  className: "artifacts-link",
1827
2579
  href: flyoutHref,
1828
2580
  target: "_blank",
1829
2581
  rel: "noreferrer noopener",
1830
- title: "弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)"
1831
- }, /* @__PURE__ */ h(FlyoutIcon, { size: 16 }))), /* @__PURE__ */ h("span", { className: "artifacts-spacer" }), notice ? /* @__PURE__ */ h("span", { className: "artifacts-notice" }, notice) : null, /* @__PURE__ */ h("button", {
2582
+ title: t("flyoutOpen")
2583
+ }, /* @__PURE__ */ h(FlyoutIcon, { size: 16 }))), /* @__PURE__ */ h("span", { className: "artifacts-spacer" }), notice ? /* @__PURE__ */ h("span", { className: "artifacts-notice" }, notice) : null, activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h("button", {
2584
+ type: "button",
2585
+ className: "artifacts-toggle artifacts-search-toggle" + (searchOpen ? " is-active" : ""),
2586
+ title: t("searchToggle"),
2587
+ "aria-pressed": searchOpen,
2588
+ onClick: () => setSearchOpen((v) => !v)
2589
+ }, /* @__PURE__ */ h(SearchIcon, { size: 16 })) : null, /* @__PURE__ */ h("button", {
1832
2590
  type: "button",
1833
2591
  className: "artifacts-toggle",
1834
- title: activeView === "tree" ? "刷新文件树" : "刷新变更列表",
2592
+ title: activeView === "tree" ? t("refreshTree") : t("refreshChanges"),
1835
2593
  onClick: () => {
1836
2594
  if (activeView === "tree") setTreeRefresh((n) => n + 1);
1837
2595
  else {
@@ -1843,24 +2601,35 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1843
2601
  }, /* @__PURE__ */ h(RefreshIcon, { size: 16 })), settings.showFileTree ? /* @__PURE__ */ h("button", {
1844
2602
  type: "button",
1845
2603
  className: "artifacts-iconbtn artifacts-viewbtn" + (activeView === "git" ? " is-active" : ""),
1846
- title: activeView === "tree" ? "查看 Git 变更(未提交)" : "返回文件列表",
2604
+ title: activeView === "tree" ? t("viewGit") : t("backToFiles"),
1847
2605
  "aria-pressed": activeView === "git",
1848
2606
  onClick: () => setActiveView(activeView === "tree" ? "git" : "tree")
1849
2607
  }, activeView === "tree" ? /* @__PURE__ */ h(GitBranchIcon, { size: 16 }) : /* @__PURE__ */ h(FolderClosedIcon, { size: 16 })) : null), /* @__PURE__ */ h("div", { className: "artifacts-main" }, /* @__PURE__ */ h("div", {
1850
2608
  className: "artifacts-body" + (activeView === "git" && gitRefreshing ? " artifacts-refreshing" : ""),
1851
2609
  style: { flex: "1 1 auto" }
1852
- }, activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h(FileTree, {
2610
+ }, open ? activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h(FileTree, {
1853
2611
  onOpen: openFile,
1854
2612
  selectedPath: activeTab && !activeTab.git ? activeTab.path : null,
1855
- refreshToken: treeRefresh
1856
- }) : gitListRows))));
2613
+ refreshToken: treeRefresh,
2614
+ searchOpen,
2615
+ onCloseSearch: () => setSearchOpen(false)
2616
+ }) : /* @__PURE__ */ h(GitChangesList, {
2617
+ files: gitFiles,
2618
+ error: gitError,
2619
+ activeDiffPath: activeTab && activeTab.git ? activeTab.path : null,
2620
+ flashKey: gitFlash,
2621
+ onOpen: openGitDiff,
2622
+ onCopyPath: (p) => copyText(p, t("copiedPath")),
2623
+ onQuote: quotePath
2624
+ }) : null))));
1857
2625
  }
1858
2626
  function CornerButton() {
1859
2627
  const open = useOpen();
2628
+ useLang();
1860
2629
  return /* @__PURE__ */ h("button", {
1861
2630
  type: "button",
1862
2631
  className: "artifacts-corner-btn" + (open ? " artifacts-slid-out" : ""),
1863
- title: "弹出式侧边栏",
2632
+ title: t("flyoutTitle"),
1864
2633
  "aria-expanded": open,
1865
2634
  onClick: () => store.toggle()
1866
2635
  }, /* @__PURE__ */ h(PanelIcon, { size: 18 }));
@@ -1878,23 +2647,29 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1878
2647
  }
1879
2648
  function SettingsSection() {
1880
2649
  const settings = useSettings();
2650
+ useLang();
1881
2651
  const set = (key, value) => settingsStore.set(key, value);
1882
- return /* @__PURE__ */ h("div", { className: "artifacts-settings" }, /* @__PURE__ */ h("p", { className: "artifacts-setintro" }, "管理「Flyout Sidebar」的显示与行为。"), /* @__PURE__ */ h("div", { className: "artifacts-setgroup" }, /* @__PURE__ */ h(SettingsToggle, {
1883
- label: "默认展开",
1884
- desc: "页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。",
2652
+ return /* @__PURE__ */ h("div", { className: "artifacts-settings" }, /* @__PURE__ */ h("p", { className: "artifacts-setintro" }, t("settingsIntro")), /* @__PURE__ */ h("div", { className: "artifacts-setgroup" }, /* @__PURE__ */ h(SettingsToggle, {
2653
+ label: t("setDefaultOpen"),
2654
+ desc: t("setDefaultOpenDesc"),
1885
2655
  value: settings.defaultOpen,
1886
2656
  onToggle: (v) => set("defaultOpen", v)
1887
2657
  }), /* @__PURE__ */ h(SettingsToggle, {
1888
- label: "自动刷新",
1889
- desc: "开启后侧边栏展开时将即时同步并更新产物列表",
2658
+ label: t("setAutoRefresh"),
2659
+ desc: t("setAutoRefreshDesc"),
1890
2660
  value: settings.autoRefresh,
1891
2661
  onToggle: (v) => set("autoRefresh", v)
1892
2662
  }), /* @__PURE__ */ h(SettingsToggle, {
1893
- label: "文件树",
1894
- desc: "在侧边栏显示「文件树」标签页,浏览工作区目录。",
2663
+ label: t("setFileTree"),
2664
+ desc: t("setFileTreeDesc"),
1895
2665
  value: settings.showFileTree,
1896
2666
  onToggle: (v) => set("showFileTree", v)
1897
- }), /* @__PURE__ */ h("div", { className: "artifacts-setrow" }, /* @__PURE__ */ h("div", { className: "artifacts-settext" }, /* @__PURE__ */ h("div", { className: "artifacts-settitle" }, "最短面板宽度"), /* @__PURE__ */ h("div", { className: "artifacts-setdesc" }, "面板的最小宽度(占窗口宽度的百分比,20–60);更宽可通过拖动面板左边缘调整。")), /* @__PURE__ */ h("div", { className: "artifacts-setcontrol" }, /* @__PURE__ */ h("input", {
2667
+ }), /* @__PURE__ */ h(SettingsToggle, {
2668
+ label: t("setCodeWrap"),
2669
+ desc: t("setCodeWrapDesc"),
2670
+ value: settings.codeWrap,
2671
+ onToggle: (v) => set("codeWrap", v)
2672
+ }), /* @__PURE__ */ h("div", { className: "artifacts-setrow" }, /* @__PURE__ */ h("div", { className: "artifacts-settext" }, /* @__PURE__ */ h("div", { className: "artifacts-settitle" }, t("setMinWidth")), /* @__PURE__ */ h("div", { className: "artifacts-setdesc" }, t("setMinWidthDesc"))), /* @__PURE__ */ h("div", { className: "artifacts-setcontrol" }, /* @__PURE__ */ h("input", {
1898
2673
  type: "number",
1899
2674
  className: "artifacts-widthinput",
1900
2675
  min: 20,
@@ -1904,8 +2679,19 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1904
2679
  const n = parseInt(e.currentTarget.value, 10);
1905
2680
  if (Number.isNaN(n)) return;
1906
2681
  set("minPanelWidth", Math.max(20, Math.min(60, n)));
2682
+ },
2683
+ onBlur: (e) => {
2684
+ const n = parseInt(e.currentTarget.value, 10);
2685
+ set("minPanelWidth", Number.isNaN(n) ? 20 : Math.max(20, Math.min(60, n)));
2686
+ }
2687
+ }), /* @__PURE__ */ h("span", { className: "artifacts-suffix" }, "%"))), /* @__PURE__ */ h("div", { className: "artifacts-setrow" }, /* @__PURE__ */ h("div", { className: "artifacts-settext" }, /* @__PURE__ */ h("div", { className: "artifacts-settitle" }, t("setLang")), /* @__PURE__ */ h("div", { className: "artifacts-setdesc" }, t("setLangDesc"))), /* @__PURE__ */ h("div", { className: "artifacts-setcontrol" }, /* @__PURE__ */ h("select", {
2688
+ className: "artifacts-langselect",
2689
+ value: getLanguageSetting() || "",
2690
+ onChange: (e) => {
2691
+ const v = e.currentTarget.value;
2692
+ setLanguage(v === "zh" || v === "en" ? v : null);
1907
2693
  }
1908
- }), /* @__PURE__ */ h("span", { className: "artifacts-suffix" }, "%")))));
2694
+ }, /* @__PURE__ */ h("option", { value: "" }, t("langAuto")), /* @__PURE__ */ h("option", { value: "zh" }, t("langZh")), /* @__PURE__ */ h("option", { value: "en" }, t("langEn")))))));
1909
2695
  }
1910
2696
  //#endregion
1911
2697
  //#region src/client/index.tsx