dsh-flyout-sidebar 0.1.2 → 0.1.4

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 {
@@ -231,6 +240,9 @@ body[data-dsh-flyout-dragging] .artifacts-preview-overlay { transition: none; }
231
240
  .artifacts-markdown blockquote { border-left: 3px solid var(--dsw-alias-border-l2); margin: 8px 0; padding: 2px 12px; color: var(--dsw-alias-label-secondary); }
232
241
  .artifacts-markdown ul, .artifacts-markdown ol { padding-left: 24px; }
233
242
  .artifacts-markdown a { color: var(--dsw-alias-state-business-primary); }
243
+ .artifacts-markdown table { display: block; overflow-x: auto; border-collapse: collapse; margin: 10px 0; }
244
+ .artifacts-markdown th, .artifacts-markdown td { border: 1px solid var(--dsw-alias-border-l2); padding: 4px 10px; text-align: left; }
245
+ .artifacts-markdown th { background: var(--dsw-alias-bg-layer-1); font-weight: 600; }
234
246
  .artifacts-diff { border-top: 1px solid var(--dsw-alias-border-l2); }
235
247
  .artifacts-diff-block { border-bottom: 1px solid var(--dsw-alias-border-l1); }
236
248
  .artifacts-diff-label { font-size: 11px; padding: 4px 12px; font-weight: 600; }
@@ -263,6 +275,11 @@ body[data-ds-dark-theme] .gd-add { color: #69db7c; }
263
275
  body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
264
276
  .artifacts-tree { display: flex; flex-direction: column; height: 100%; min-height: 0; }
265
277
  .artifacts-tree-body { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 6px 8px; }
278
+ .artifacts-searchbar { flex: none; padding: 8px 10px 4px; }
279
+ .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; }
280
+ .artifacts-search-input:focus { border-color: var(--dsw-alias-button-primary-fill); }
281
+ .artifacts-search-input::placeholder { color: var(--dsw-alias-label-tertiary); }
282
+ .artifacts-search-dir { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-left: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
283
  .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
284
  .artifacts-tree-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
268
285
  .artifacts-tree-dir { font: var(--dsw-font-s-strong-14); }
@@ -275,6 +292,14 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
275
292
  .artifacts-tree-copied { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary); flex: none; }
276
293
  .artifacts-tree-loading { cursor: default; color: var(--dsw-alias-label-tertiary); font-size: 12px; }
277
294
  .artifacts-tree-error { cursor: default; color: var(--dsw-alias-state-error-primary); font-size: 12px; }
295
+ .artifacts-tree-fail { padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
296
+ .artifacts-tree-fail .artifacts-tree-error { text-align: center; word-break: break-all; }
297
+ .artifacts-retry {
298
+ padding: 4px 16px; font-size: 12px; cursor: pointer; border-radius: 6px;
299
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-base);
300
+ color: var(--dsw-alias-label-primary);
301
+ }
302
+ .artifacts-retry:hover { background: var(--dsw-alias-interactive-bg-hover); }
278
303
  @keyframes artifacts-row-in { 0% { opacity: 0 } }
279
304
  .artifacts-settings { display: flex; flex-direction: column; gap: 14px; width: 100%; height: 100%; min-height: 0; overflow-y: auto; padding-bottom: 24px; }
280
305
  .artifacts-setintro { color: var(--dsw-alias-label-tertiary); margin: 0; padding: 0 2px; font-size: 13px; line-height: 20px; }
@@ -294,13 +319,22 @@ body[data-ds-dark-theme] .gd-del { color: #faa2c1; }
294
319
  .artifacts-switch input:focus-visible + .artifacts-switch-track { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
295
320
  .artifacts-setcontrol { flex: none; align-items: center; gap: 6px; display: flex; }
296
321
  .artifacts-widthinput { width: 76px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; border-radius: 6px; padding: 4px 8px; }
322
+ .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
323
  .artifacts-suffix { color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 22px; }
298
324
  .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; }
325
+ /* 代码视图:逐行渲染(行 = 行号 + 代码),软换行时行号仍对齐 */
326
+ .artifacts-code-scroll { flex: 1; min-height: 0; overflow: auto; padding: 12px 0; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); }
327
+ .artifacts-code-line { display: flex; align-items: flex-start; min-width: fit-content; }
328
+ .artifacts-code-gutter { flex: none; min-width: 2.2em; padding: 0 6px 0 8px; text-align: right; color: var(--dsw-alias-label-tertiary); border-right: 1px solid var(--dsw-alias-border-l1); position: sticky; left: 0; user-select: none; background: var(--shiki-background, var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1))); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
329
+ .artifacts-code-pre { flex: 1 0 auto; margin: 0; padding: 0 12px; color: var(--shiki-foreground, var(--dsw-alias-label-primary)); font: 12px/1.6 var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); white-space: pre; }
302
330
  .artifacts-code-pre code { font: inherit; color: inherit; }
303
- .artifacts-code-line { display: block; }
331
+ .artifacts-code-wrap .artifacts-code-pre { white-space: pre-wrap; word-break: break-all; }
332
+ .artifacts-imgview { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: grab; user-select: none; }
333
+ .artifacts-imgview.is-dragging { cursor: grabbing; }
334
+ .artifacts-imgview .artifacts-img { flex: none; margin: 16px; transform-origin: center; will-change: transform; }
335
+ .artifacts-git-stats { flex: none; display: inline-flex; gap: 4px; font: 11px/16px var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
336
+ .artifacts-git-adds { color: var(--p-success-fg, #1a7f37); }
337
+ .artifacts-git-dels { color: var(--p-error, #cf222e); }
304
338
  .tok-comment { color: #868e96; }
305
339
  .tok-string { color: #2f9e44; }
306
340
  .tok-number, .tok-bool, .tok-variable, .tok-hex, .tok-attr { color: #e8590c; }
@@ -385,11 +419,339 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
385
419
  return m ? (m[1] || "").toLowerCase() : "";
386
420
  }
387
421
  //#endregion
388
- //#region src/client/store.ts
389
- const basename = (p) => {
390
- const parts = String(p).split("/");
391
- return parts[parts.length - 1] || p;
422
+ //#region src/shared/i18n.js
423
+ /**
424
+ * 共享:中英双语 UI 文案。
425
+ *
426
+ * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注
427
+ * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:
428
+ * 1. tsdown 打包进 client 侧(浏览器 React bundle);
429
+ * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar
430
+ * 的经典 <script>(见 src/host/page.ts)。
431
+ *
432
+ * 语言选择:localStorage `dsh-flyout-sidebar:lang`('zh' | 'en',主面板的
433
+ * 设置项写入、弹出页读取),未设置时按浏览器语言自动判定。`t(key)` 取当前
434
+ * 语言文案,缺失时回退另一语言、再回退 key 本身。
435
+ */
436
+ /** @type {Record<string, string>} */
437
+ const ZH = {
438
+ loading: "加载中…",
439
+ loadingTree: "加载文件树…",
440
+ loadingChanges: "加载变更列表…",
441
+ loadingPdf: "加载 PDF…",
442
+ searching: "搜索中…",
443
+ emptyDir: "(空目录)",
444
+ noChanges: "没有未提交的变更",
445
+ noChangesHead: "没有未提交的变更(相对于 HEAD)",
446
+ noResults: "没有匹配的文件",
447
+ readFailed: "读取失败",
448
+ retry: "重试",
449
+ gitStatusFailed: "git status 失败",
450
+ imageLoadFailed: "图片加载失败",
451
+ pdfLoadFailed: "pdf.js 加载失败",
452
+ searchFailed: "搜索失败",
453
+ treeLoadFailed: "加载失败",
454
+ truncated: "(已截断的预览)",
455
+ copied: "已复制",
456
+ copiedPath: "已复制路径",
457
+ copiedRef: "已复制 @引用(未能写入输入框)",
458
+ insertedInput: "已插入输入框",
459
+ refBtn: "@引用",
460
+ refTitle: "引用到输入框(失败则复制 @path)",
461
+ refFlyoutTitle: "复制 @path 引用",
462
+ copyPath: "复制路径",
463
+ refInput: "@引用到输入框",
464
+ collapsePanel: "收起侧边栏",
465
+ panelAria: "文件面板",
466
+ flyoutTitle: "弹出式侧边栏",
467
+ flyoutOpen: "弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)",
468
+ resizeHandle: "拖动调整宽度",
469
+ resizePanel: "拖动调整面板宽度",
470
+ refreshTree: "刷新文件树",
471
+ refreshChanges: "刷新变更列表",
472
+ refresh: "刷新",
473
+ viewGit: "查看 Git 变更(未提交)",
474
+ backToFiles: "返回文件列表",
475
+ hidePreview: "隐藏预览(标签页保留)",
476
+ closeTab: "关闭标签页",
477
+ previewRegion: "文件预览",
478
+ diffTabPrefix: "[diff] ",
479
+ searchPlaceholder: "搜索文件…",
480
+ searchToggle: "搜索文件",
481
+ hintClickGit: "点击右侧变更文件查看 diff",
482
+ hintClickTree: "点击右侧文件查看内容",
483
+ movePanelLeft: "将文件面板移到左侧",
484
+ movePanelRight: "将文件面板移到右侧",
485
+ statusU: "未跟踪",
486
+ statusA: "新增",
487
+ statusM: "修改",
488
+ statusD: "删除",
489
+ statusR: "重命名",
490
+ statusC: "复制",
491
+ statusT: "类型变更",
492
+ staged: "(已暂存)",
493
+ unstaged: "(未暂存)",
494
+ diffDeleted: "- 删除",
495
+ diffAdded: "+ 新增",
496
+ zoomOut: "缩小",
497
+ zoomIn: "放大",
498
+ prevPage: "上一页",
499
+ nextPage: "下一页",
500
+ settingsIntro: "管理「Flyout Sidebar」的显示与行为。",
501
+ setDefaultOpen: "默认展开",
502
+ setDefaultOpenDesc: "页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。",
503
+ setAutoRefresh: "自动刷新",
504
+ setAutoRefreshDesc: "开启后侧边栏展开时将即时同步并更新产物列表",
505
+ setFileTree: "文件树",
506
+ setFileTreeDesc: "在侧边栏显示「文件树」标签页,浏览工作区目录。",
507
+ setMinWidth: "最短面板宽度",
508
+ setMinWidthDesc: "面板的最小宽度(占窗口宽度的百分比,20–60);更宽可通过拖动面板左边缘调整。",
509
+ setCodeWrap: "代码换行",
510
+ setCodeWrapDesc: "代码预览长行软换行;关闭则横向滚动。",
511
+ wordWrap: "自动换行",
512
+ openInEditor: "在系统编辑器打开",
513
+ openedInEditor: "已在编辑器打开",
514
+ openFailed: "打开失败",
515
+ setLang: "界面语言",
516
+ setLangDesc: "侧边栏与独立弹出页的显示语言;独立弹出页需刷新后生效。",
517
+ langAuto: "跟随浏览器",
518
+ langZh: "中文",
519
+ langEn: "English"
520
+ };
521
+ /** @type {Record<string, string>} */
522
+ const EN = {
523
+ loading: "Loading…",
524
+ loadingTree: "Loading file tree…",
525
+ loadingChanges: "Loading change list…",
526
+ loadingPdf: "Loading PDF…",
527
+ searching: "Searching…",
528
+ emptyDir: "(empty directory)",
529
+ noChanges: "No uncommitted changes",
530
+ noChangesHead: "No uncommitted changes (relative to HEAD)",
531
+ noResults: "No matching files",
532
+ readFailed: "Failed to read",
533
+ retry: "Retry",
534
+ gitStatusFailed: "git status failed",
535
+ imageLoadFailed: "Failed to load image",
536
+ pdfLoadFailed: "Failed to load pdf.js",
537
+ searchFailed: "Search failed",
538
+ treeLoadFailed: "Failed to load",
539
+ truncated: "(truncated preview)",
540
+ copied: "Copied",
541
+ copiedPath: "Path copied",
542
+ copiedRef: "Copied @reference (could not write to input box)",
543
+ insertedInput: "Inserted into input box",
544
+ refBtn: "@ref",
545
+ refTitle: "Quote into input box (falls back to copying @path)",
546
+ refFlyoutTitle: "Copy @path reference",
547
+ copyPath: "Copy path",
548
+ refInput: "@reference into input box",
549
+ collapsePanel: "Collapse sidebar",
550
+ panelAria: "File panel",
551
+ flyoutTitle: "Flyout sidebar",
552
+ flyoutOpen: "Flyout sidebar — open in a new tab (drag to another display)",
553
+ resizeHandle: "Drag to resize",
554
+ resizePanel: "Drag to resize panel",
555
+ refreshTree: "Refresh file tree",
556
+ refreshChanges: "Refresh change list",
557
+ refresh: "Refresh",
558
+ viewGit: "View Git changes (uncommitted)",
559
+ backToFiles: "Back to file list",
560
+ hidePreview: "Hide preview (tabs are kept)",
561
+ closeTab: "Close tab",
562
+ previewRegion: "File preview",
563
+ diffTabPrefix: "[diff] ",
564
+ searchPlaceholder: "Search files…",
565
+ searchToggle: "Search files",
566
+ hintClickGit: "Click a changed file to view its diff",
567
+ hintClickTree: "Click a file to view its content",
568
+ movePanelLeft: "Move file panel to the left",
569
+ movePanelRight: "Move file panel to the right",
570
+ statusU: "Untracked",
571
+ statusA: "Added",
572
+ statusM: "Modified",
573
+ statusD: "Deleted",
574
+ statusR: "Renamed",
575
+ statusC: "Copied",
576
+ statusT: "Type change",
577
+ staged: "(staged)",
578
+ unstaged: "(unstaged)",
579
+ diffDeleted: "- Deleted",
580
+ diffAdded: "+ Added",
581
+ zoomOut: "Zoom out",
582
+ zoomIn: "Zoom in",
583
+ prevPage: "Previous page",
584
+ nextPage: "Next page",
585
+ settingsIntro: "Manage the display and behavior of \"Flyout Sidebar\".",
586
+ setDefaultOpen: "Open by default",
587
+ setDefaultOpenDesc: "Expand the sidebar on page load; when off it stays collapsed until the corner icon is clicked.",
588
+ setAutoRefresh: "Auto refresh",
589
+ setAutoRefreshDesc: "Keep the artifact list in sync while the sidebar is open",
590
+ setFileTree: "File tree",
591
+ setFileTreeDesc: "Show the \"File tree\" tab in the sidebar to browse the workspace directory.",
592
+ setMinWidth: "Minimum panel width",
593
+ setMinWidthDesc: "Minimum width of the panel (percentage of window width, 20–60); drag the panel edge to go wider.",
594
+ setCodeWrap: "Code wrap",
595
+ setCodeWrapDesc: "Soft-wrap long lines in code previews; when off they scroll horizontally.",
596
+ wordWrap: "Word wrap",
597
+ openInEditor: "Open in system editor",
598
+ openedInEditor: "Opened in editor",
599
+ openFailed: "Failed to open",
600
+ setLang: "Interface language",
601
+ setLangDesc: "Display language for the sidebar and the standalone flyout page (flyout requires a reload).",
602
+ langAuto: "Auto (browser)",
603
+ langZh: "中文",
604
+ langEn: "English"
392
605
  };
606
+ /** @type {Record<string, Record<string, string>>} */
607
+ const DICTS = {
608
+ zh: ZH,
609
+ en: EN
610
+ };
611
+ const LANG_KEY = "dsh-flyout-sidebar:lang";
612
+ /** @type {'zh' | 'en' | null} 已显式选择的语言(null = 跟随浏览器自动判定) */
613
+ let explicitLang = null;
614
+ /** @type {Array<(lang: string) => void>} */
615
+ let listeners = [];
616
+ function readStoredLang() {
617
+ try {
618
+ var v = localStorage.getItem(LANG_KEY);
619
+ return v === "zh" || v === "en" ? v : null;
620
+ } catch (e) {
621
+ return null;
622
+ }
623
+ }
624
+ /** 浏览器语言自动判定:非 zh 开头一律英文 */
625
+ function autoLang() {
626
+ try {
627
+ var langs = navigator.languages || (navigator.language ? [navigator.language] : []);
628
+ for (var i = 0; i < langs.length; i++) {
629
+ var l = String(langs[i] || "").toLowerCase();
630
+ if (l.indexOf("zh") === 0) return "zh";
631
+ if (l.indexOf("en") === 0) return "en";
632
+ }
633
+ } catch (e) {}
634
+ return "en";
635
+ }
636
+ function currentLang() {
637
+ if (explicitLang === null) explicitLang = readStoredLang();
638
+ return explicitLang || autoLang();
639
+ }
640
+ /**
641
+ * @param {string} key 文案键
642
+ * @returns {string} 当前语言文案;两级回退(另一语言 → key)
643
+ */
644
+ function t(key) {
645
+ var lang = currentLang();
646
+ var dict = DICTS[lang] || EN;
647
+ if (dict[key] != null) return dict[key];
648
+ var other = DICTS[lang === "zh" ? "en" : "zh"];
649
+ if (!other) return key;
650
+ return other[key] != null ? other[key] : key;
651
+ }
652
+ /** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */
653
+ function getLang() {
654
+ return currentLang();
655
+ }
656
+ /** @returns {'zh' | 'en' | null} 用户显式选择的语言(null = 跟随浏览器) */
657
+ function getExplicitLang() {
658
+ if (explicitLang === null) explicitLang = readStoredLang();
659
+ return explicitLang;
660
+ }
661
+ /**
662
+ * 显式设置语言并持久化(null = 恢复跟随浏览器)。
663
+ * @param {'zh' | 'en' | null} lang
664
+ */
665
+ function setLang(lang) {
666
+ explicitLang = lang;
667
+ try {
668
+ if (lang === null) localStorage.removeItem(LANG_KEY);
669
+ else localStorage.setItem(LANG_KEY, lang);
670
+ } catch (e) {}
671
+ var next = listeners.slice();
672
+ for (var i = 0; i < next.length; i++) {
673
+ var fn = next[i];
674
+ if (!fn) continue;
675
+ try {
676
+ fn(currentLang());
677
+ } catch (e) {}
678
+ }
679
+ }
680
+ /** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */
681
+ function subscribeLang(fn) {
682
+ listeners.push(fn);
683
+ return function() {
684
+ listeners = listeners.filter(function(f) {
685
+ return f !== fn;
686
+ });
687
+ };
688
+ }
689
+ //#endregion
690
+ //#region src/shared/gitui.js
691
+ /**
692
+ * 共享:git 变更列表 / diff 的纯展示逻辑。
693
+ *
694
+ * client 侧 React 组件与独立弹出页(page.ts 内联脚本)共用,避免双端各自
695
+ * 实现后细节漂移。与 ext/highlight/markdown 一样遵守「可移植源码」约束:
696
+ * 只能使用 JSDoc 标注类型、依赖仅限本目录(i18n 的 t 在内联场景由同作用域
697
+ * 函数声明提升提供)。
698
+ */
699
+ /** @param {string} p @returns {string} */
700
+ function basename(p) {
701
+ var parts = String(p).split("/");
702
+ return parts[parts.length - 1] || p;
703
+ }
704
+ /**
705
+ * 变更文件的状态徽章字母:未跟踪为 U,否则取工作树状态(y),再退到暂存
706
+ * 状态(x),空缺按 M。
707
+ * @param {{ x: string, y: string }} e @returns {string}
708
+ */
709
+ function gitLabel(e) {
710
+ if (e.x === "?" || e.y === "?") return "U";
711
+ return (e.y !== " " ? e.y : e.x) || "M";
712
+ }
713
+ /**
714
+ * 变更文件行 hover 提示:状态名 + 暂存/未暂存。
715
+ * @param {{ x: string, y: string }} e @returns {string}
716
+ */
717
+ function gitTitle(e) {
718
+ var label = gitLabel(e);
719
+ /** @type {Record<string, string>} */
720
+ var map = {
721
+ U: t("statusU"),
722
+ A: t("statusA"),
723
+ M: t("statusM"),
724
+ D: t("statusD"),
725
+ R: t("statusR"),
726
+ C: t("statusC"),
727
+ T: t("statusT")
728
+ };
729
+ var staged = e.x !== " " && e.x !== "?";
730
+ return (map[label] || label) + (staged ? t("staged") : t("unstaged"));
731
+ }
732
+ /**
733
+ * unified diff 单行的样式类:hunk / 增 / 删 / 元信息(diff --git、index、
734
+ * rename 等),其余为普通行。
735
+ * @param {string} line @returns {string}
736
+ */
737
+ function gitDiffLineClass(line) {
738
+ var cls = "gd-line";
739
+ if (line.indexOf("@@") === 0) cls += " gd-hunk";
740
+ else if (line.charAt(0) === "+" && line.indexOf("+++") !== 0) cls += " gd-add";
741
+ else if (line.charAt(0) === "-" && line.indexOf("---") !== 0) cls += " gd-del";
742
+ 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";
743
+ return cls;
744
+ }
745
+ //#endregion
746
+ //#region src/client/store.ts
747
+ /**
748
+ * Client 侧:共享状态与工具函数。
749
+ *
750
+ * - store / settingsStore:面板开关与功能设置(localStorage 持久化),
751
+ * 触发按钮与面板等组件通过 useOpen / useSettings 订阅。
752
+ * - currentSessionId / quoteToComposer:读取客户端会话库、把 @path 引用
753
+ * 写入会话输入框。
754
+ */
393
755
  /** 当前会话 id(读自客户端会话库;文件树把它传给 host 以定位工作区) */
394
756
  function currentSessionId() {
395
757
  try {
@@ -475,7 +837,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
475
837
  autoRefresh: true,
476
838
  minPanelWidth: 20,
477
839
  showFileTree: true,
478
- defaultOpen: true
840
+ defaultOpen: true,
841
+ codeWrap: false
479
842
  };
480
843
  function loadSettings() {
481
844
  try {
@@ -518,7 +881,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
518
881
  };
519
882
  store.open = !!settingsStore.get().defaultOpen;
520
883
  const slideState = {
521
- visible: store.open,
884
+ visible: true,
522
885
  slidOut: !store.open
523
886
  };
524
887
  const slideListeners = [];
@@ -537,22 +900,8 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
537
900
  fn(next);
538
901
  } catch {}
539
902
  };
540
- let slideTimer = null;
541
903
  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
- }
904
+ setSlide({ slidOut: !open });
556
905
  });
557
906
  const useSlide = () => {
558
907
  const [s, setS] = React.useState({ ...slideState });
@@ -585,6 +934,22 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
585
934
  }, []);
586
935
  return sessionId;
587
936
  }
937
+ /**
938
+ * 界面语言:订阅 i18n 的语言变更,语言切换时强制订阅组件重渲染(组件内的
939
+ * t() 调用随之取到新语言文案)。返回值用于设置区判断下拉框选项。
940
+ */
941
+ function useLang() {
942
+ const [, force] = React.useReducer((n) => n + 1, 0);
943
+ React.useEffect(() => {
944
+ const unsubscribe = subscribeLang(() => force());
945
+ return () => unsubscribe();
946
+ }, []);
947
+ return getLang();
948
+ }
949
+ /** 当前设置语言:'zh' / 'en'(显式)或 null(跟随浏览器自动判定) */
950
+ const getLanguageSetting = getExplicitLang;
951
+ /** 设置界面语言并持久化;null = 恢复跟随浏览器 */
952
+ const setLanguage = setLang;
588
953
  //#endregion
589
954
  //#region src/shared/highlight.js
590
955
  /**
@@ -841,6 +1206,67 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
841
1206
  const fn = HL_ENGINES[hlLangOf(hint)];
842
1207
  return fn ? fn(String(src)) : escHtml(src);
843
1208
  }
1209
+ /**
1210
+ * 把 highlightCode 输出的 HTML 按源码行拆分成逐行 HTML 数组。跨行 token
1211
+ * (块注释、Python 三引号串等)在行边界处闭合、下一行重新打开,保证逐行
1212
+ * 渲染时着色与整块渲染一致。逐行渲染使行号与代码天然对齐(含软换行模式),
1213
+ * 也是行级查找高亮的基础。
1214
+ * @param {string} html highlightCode 的输出
1215
+ * @returns {string[]}
1216
+ */
1217
+ function splitHlLines(html) {
1218
+ var lines = [];
1219
+ var cur = "";
1220
+ /** @type {string[]} */
1221
+ var stack = [];
1222
+ var i = 0;
1223
+ var openAll = function() {
1224
+ var s = "";
1225
+ for (var k = 0; k < stack.length; k++) s += "<span class=\"" + stack[k] + "\">";
1226
+ return s;
1227
+ };
1228
+ var closeAll = function() {
1229
+ var s = "";
1230
+ for (var k = 0; k < stack.length; k++) s += "</span>";
1231
+ return s;
1232
+ };
1233
+ while (i < html.length) {
1234
+ if (html.indexOf("<span class=\"", i) === i) {
1235
+ var end = html.indexOf(">", i);
1236
+ if (end < 0) {
1237
+ cur += html.slice(i);
1238
+ break;
1239
+ }
1240
+ cur += html.slice(i, end + 1);
1241
+ stack.push(html.slice(i + 13, end - 1));
1242
+ i = end + 1;
1243
+ continue;
1244
+ }
1245
+ if (html.indexOf("</span>", i) === i) {
1246
+ cur += "</span>";
1247
+ stack.pop();
1248
+ i += 7;
1249
+ continue;
1250
+ }
1251
+ var nl = html.indexOf("\n", i);
1252
+ var nextTag = html.indexOf("<", i);
1253
+ if (nextTag >= 0 && (nl < 0 || nextTag < nl)) {
1254
+ cur += html.slice(i, nextTag);
1255
+ i = nextTag;
1256
+ continue;
1257
+ }
1258
+ if (nl < 0) {
1259
+ cur += html.slice(i);
1260
+ break;
1261
+ }
1262
+ cur += html.slice(i, nl);
1263
+ lines.push(cur + closeAll());
1264
+ cur = openAll();
1265
+ i = nl + 1;
1266
+ }
1267
+ lines.push(cur + closeAll());
1268
+ return lines;
1269
+ }
844
1270
  //#endregion
845
1271
  //#region src/shared/markdown.js
846
1272
  /**
@@ -852,17 +1278,69 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
852
1278
  */
853
1279
  /** @param {string} s @returns {string} */
854
1280
  function mdEscape(s) {
855
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1281
+ return String(s).replace(/[\u0000\u0001]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1282
+ }
1283
+ /**
1284
+ * URL 白名单:只放行 http(s)、站内相对路径、页内锚点与 data:image/*,
1285
+ * 其余(javascript:、vbscript: 及实体编码变体等)一律替换为 '#',防止
1286
+ * 预览渲染出来的链接/图片在点击时执行脚本。
1287
+ * @param {string} u @returns {string}
1288
+ */
1289
+ function mdSafeUrl(u) {
1290
+ var s = String(u || "").replace(/[\s\u0000-\u001f]/g, "");
1291
+ if (/^https?:\/\//i.test(s)) return s;
1292
+ if (/^\/(?!\/)/.test(s) || /^\.{1,2}\//.test(s) || s.charAt(0) === "#") return s;
1293
+ if (/^data:image\/(?:png|gif|jpeg|webp|bmp|avif);/i.test(s)) return s;
1294
+ return "#";
856
1295
  }
857
1296
  /** @param {string} s @returns {string} */
858
1297
  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>");
1298
+ /** @type {string[]} */
1299
+ const codes = [];
1300
+ s = s.replace(/`([^`]+)`/g, (m, c) => {
1301
+ codes.push("<code>" + c + "</code>");
1302
+ return "\0" + (codes.length - 1) + "\0";
1303
+ });
1304
+ s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, u) => "<img alt=\"" + alt + "\" src=\"" + mdSafeUrl(u) + "\">");
1305
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, text, u) => "<a href=\"" + mdSafeUrl(u) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + text + "</a>");
862
1306
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
863
1307
  s = s.replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
1308
+ s = s.replace(/\u0000(\d+)\u0000/g, (m, i) => codes[Number(i)] ?? "");
864
1309
  return s;
865
1310
  }
1311
+ /**
1312
+ * GFM 管道表格辅助:按未转义的 | 切分单元格(行内 \| 还原为字面量竖线),
1313
+ * 并去掉首尾管道产生的空单元格。
1314
+ * @param {string} line @returns {string[]}
1315
+ */
1316
+ function mdSplitCells(line) {
1317
+ const parts = String(line).replace(/\\\|/g, "").split("|");
1318
+ for (let i = 0; i < parts.length; i += 1) parts[i] = (parts[i] || "").replace(/\u0002/g, "|").trim();
1319
+ return parts;
1320
+ }
1321
+ /** @param {string[]} cells @returns {string[]} */
1322
+ function mdTrimEdgeCells(cells) {
1323
+ if (cells.length && cells[0] === "") cells.shift();
1324
+ if (cells.length && cells[cells.length - 1] === "") cells.pop();
1325
+ return cells;
1326
+ }
1327
+ /** @param {string[]} cells @returns {boolean} 是否为 GFM 分隔行(:--- / :---: / ---: 形态) */
1328
+ function mdIsDelimiterRow(cells) {
1329
+ return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
1330
+ }
1331
+ /**
1332
+ * @param {string} cell @returns {string} 'center' | 'right' | ''(left 与默认一致,不输出)
1333
+ */
1334
+ function mdCellAlign(cell) {
1335
+ if (/^:-+:$/.test(cell)) return "center";
1336
+ if (/^:-+$/.test(cell)) return "left";
1337
+ if (/^-+:$/.test(cell)) return "right";
1338
+ return "";
1339
+ }
1340
+ /** @param {string} a @returns {string} 单元格对齐的内联样式属性 */
1341
+ function mdAlignAttr(a) {
1342
+ return a === "center" || a === "right" ? " style=\"text-align:" + a + "\"" : "";
1343
+ }
866
1344
  /** @param {string} src @returns {string} HTML */
867
1345
  function mdToHtml(src) {
868
1346
  const lines = String(src || "").replace(/\r\n/g, "\n").split("\n");
@@ -931,6 +1409,35 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
931
1409
  i += 1;
932
1410
  continue;
933
1411
  }
1412
+ if (line.indexOf("|") >= 0 && i + 1 < lines.length) {
1413
+ const head = mdTrimEdgeCells(mdSplitCells(line));
1414
+ const delim = mdTrimEdgeCells(mdSplitCells(lines[i + 1]));
1415
+ if (head.length && mdIsDelimiterRow(delim)) {
1416
+ const aligns = delim.map(mdCellAlign);
1417
+ const colCount = head.length;
1418
+ /** @type {string[][]} */
1419
+ const rows = [];
1420
+ i += 2;
1421
+ while (i < lines.length && lines[i].trim() !== "" && lines[i].indexOf("|") >= 0) {
1422
+ const cells = mdTrimEdgeCells(mdSplitCells(lines[i]));
1423
+ /** @type {string[]} */
1424
+ const padded = [];
1425
+ for (let c = 0; c < colCount; c += 1) padded.push(cells[c] || "");
1426
+ rows.push(padded);
1427
+ i += 1;
1428
+ }
1429
+ let table = "<table><thead><tr>";
1430
+ for (let c = 0; c < colCount; c += 1) table += "<th" + mdAlignAttr(aligns[c] || "") + ">" + mdInline(mdEscape(head[c] || "")) + "</th>";
1431
+ table += "</tr></thead><tbody>";
1432
+ for (const cells of rows) {
1433
+ table += "<tr>";
1434
+ for (let c = 0; c < colCount; c += 1) table += "<td" + mdAlignAttr(aligns[c] || "") + ">" + mdInline(mdEscape(cells[c] || "")) + "</td>";
1435
+ table += "</tr>";
1436
+ }
1437
+ out.push(table + "</tbody></table>");
1438
+ continue;
1439
+ }
1440
+ }
934
1441
  out.push("<p>" + mdInline(mdEscape(line)) + "</p>");
935
1442
  i += 1;
936
1443
  }
@@ -943,15 +1450,23 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
943
1450
  * 自定义 pdf.js 渲染器、Markdown / 图片 / HTML 沙箱、多类型分发。
944
1451
  */
945
1452
  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 : "")));
1453
+ 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
1454
  }
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", {
1455
+ /** 代码预览:逐行渲染(行号 + 语法高亮同行内对齐,软换行也不串行)。
1456
+ * 文内查找直接用浏览器原生 Ctrl/Cmd+F(真实 DOM 文本可搜)。 */
1457
+ function CodeView({ code, path, wrap }) {
1458
+ const src = String(code || "").replace(/\n$/, "");
1459
+ const hlLines = React.useMemo(() => splitHlLines(highlightCode(src, fileExt(path || ""))), [src, path]);
1460
+ 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", {
1461
+ key: i,
1462
+ className: "artifacts-code-line"
1463
+ }, /* @__PURE__ */ h("div", {
952
1464
  className: "artifacts-code-gutter",
953
1465
  "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 || "")) } }))));
1466
+ }, i + 1), /* @__PURE__ */ h("div", {
1467
+ className: "artifacts-code-pre",
1468
+ dangerouslySetInnerHTML: { __html: line || "&nbsp;" }
1469
+ })))));
955
1470
  }
956
1471
  let pdfjsPromise = null;
957
1472
  function loadPdfjs() {
@@ -969,7 +1484,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
969
1484
  s.async = true;
970
1485
  s.onload = () => {
971
1486
  try {
972
- if (!window.pdfjsLib) throw new Error("pdf.js 加载失败");
1487
+ if (!window.pdfjsLib) throw new Error(t("pdfLoadFailed"));
973
1488
  window.pdfjsLib.GlobalWorkerOptions.workerSrc = "/flyout-sidebar/pdfjs/pdf.worker.min.js";
974
1489
  resolve(window.pdfjsLib);
975
1490
  } catch (e) {
@@ -978,7 +1493,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
978
1493
  };
979
1494
  s.onerror = () => {
980
1495
  pdfjsPromise = null;
981
- reject(/* @__PURE__ */ new Error("pdf.js 加载失败"));
1496
+ reject(new Error(t("pdfLoadFailed")));
982
1497
  };
983
1498
  document.head.appendChild(s);
984
1499
  });
@@ -1096,7 +1611,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1096
1611
  const clampPage = (n) => Math.max(1, Math.min(pageCount || 1, n));
1097
1612
  const goPage = (n) => setPageNo(clampPage(n));
1098
1613
  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…"));
1614
+ if (phase === "loading") return /* @__PURE__ */ h("div", { className: "artifacts-pdfview" }, /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("loadingPdf")));
1100
1615
  if (phase === "error") return /* @__PURE__ */ h("embed", {
1101
1616
  className: "artifacts-pdf",
1102
1617
  src: "/flyout-sidebar/media?path=" + encodeURIComponent(path),
@@ -1107,25 +1622,25 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1107
1622
  return /* @__PURE__ */ h("div", { className: "artifacts-pdfview" }, /* @__PURE__ */ h("div", { className: "artifacts-pdfview-bar" }, /* @__PURE__ */ h("button", {
1108
1623
  type: "button",
1109
1624
  className: "artifacts-pdfview-btn",
1110
- title: "缩小",
1625
+ title: t("zoomOut"),
1111
1626
  disabled,
1112
1627
  onClick: () => zoomBy(.8)
1113
1628
  }, "−"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-zoom" }, Math.round(zoom * 100) + "%"), /* @__PURE__ */ h("button", {
1114
1629
  type: "button",
1115
1630
  className: "artifacts-pdfview-btn",
1116
- title: "放大",
1631
+ title: t("zoomIn"),
1117
1632
  disabled,
1118
1633
  onClick: () => zoomBy(1.25)
1119
1634
  }, "+"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-spacer" }), /* @__PURE__ */ h("button", {
1120
1635
  type: "button",
1121
1636
  className: "artifacts-pdfview-btn",
1122
- title: "上一页",
1637
+ title: t("prevPage"),
1123
1638
  disabled: disabled || pageNo <= 1,
1124
1639
  onClick: () => goPage(pageNo - 1)
1125
1640
  }, "‹"), /* @__PURE__ */ h("span", { className: "artifacts-pdfview-page" }, pageNo + " / " + pageCount), /* @__PURE__ */ h("button", {
1126
1641
  type: "button",
1127
1642
  className: "artifacts-pdfview-btn",
1128
- title: "下一页",
1643
+ title: t("nextPage"),
1129
1644
  disabled: disabled || pageNo >= pageCount,
1130
1645
  onClick: () => goPage(pageNo + 1)
1131
1646
  }, "›")), /* @__PURE__ */ h("div", {
@@ -1136,33 +1651,93 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1136
1651
  className: "artifacts-pdfview-canvas"
1137
1652
  })));
1138
1653
  }
1139
- /** 统一 git diff 渲染:meta/hunk/+/- 行着色,等宽可滚动 */
1654
+ /** 图片预览:滚轮缩放 + 拖拽平移 + 双击复位 */
1655
+ function ImageView({ path }) {
1656
+ const [scale, setScale] = React.useState(1);
1657
+ const [offset, setOffset] = React.useState({
1658
+ x: 0,
1659
+ y: 0
1660
+ });
1661
+ const [dragging, setDragging] = React.useState(false);
1662
+ const dragRef = React.useRef(null);
1663
+ const wrapRef = React.useRef(null);
1664
+ const src = "/flyout-sidebar/media?path=" + encodeURIComponent(path);
1665
+ React.useEffect(() => {
1666
+ const wrap = wrapRef.current;
1667
+ if (!wrap) return;
1668
+ const onWheel = (ev) => {
1669
+ ev.preventDefault();
1670
+ const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
1671
+ setScale((sc) => Math.max(.1, Math.min(12, sc * factor)));
1672
+ };
1673
+ wrap.addEventListener("wheel", onWheel, { passive: false });
1674
+ return () => wrap.removeEventListener("wheel", onWheel);
1675
+ }, []);
1676
+ React.useEffect(() => {
1677
+ if (!dragging) return;
1678
+ const onMove = (ev) => {
1679
+ const start = dragRef.current;
1680
+ if (!start) return;
1681
+ setOffset({
1682
+ x: start.ox + (ev.clientX - start.x),
1683
+ y: start.oy + (ev.clientY - start.y)
1684
+ });
1685
+ };
1686
+ const onUp = () => {
1687
+ setDragging(false);
1688
+ dragRef.current = null;
1689
+ };
1690
+ document.addEventListener("mousemove", onMove);
1691
+ document.addEventListener("mouseup", onUp);
1692
+ return () => {
1693
+ document.removeEventListener("mousemove", onMove);
1694
+ document.removeEventListener("mouseup", onUp);
1695
+ };
1696
+ }, [dragging]);
1697
+ return /* @__PURE__ */ h("div", {
1698
+ ref: wrapRef,
1699
+ className: "artifacts-imgview" + (dragging ? " is-dragging" : ""),
1700
+ onMouseDown: (ev) => {
1701
+ ev.preventDefault();
1702
+ dragRef.current = {
1703
+ x: ev.clientX,
1704
+ y: ev.clientY,
1705
+ ox: offset.x,
1706
+ oy: offset.y
1707
+ };
1708
+ setDragging(true);
1709
+ },
1710
+ onDoubleClick: () => {
1711
+ setScale(1);
1712
+ setOffset({
1713
+ x: 0,
1714
+ y: 0
1715
+ });
1716
+ }
1717
+ }, /* @__PURE__ */ h("img", {
1718
+ className: "artifacts-img",
1719
+ draggable: false,
1720
+ src,
1721
+ alt: path,
1722
+ style: { transform: "translate(" + offset.x + "px," + offset.y + "px) scale(" + scale + ")" }
1723
+ }));
1724
+ }
1725
+ /** 统一 git diff 渲染:meta/hunk/+/- 行着色,等宽可滚动;行分类与独立弹出页共用 */
1140
1726
  function GitDiffView({ diff }) {
1141
1727
  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
- }));
1728
+ if (!text) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("noChangesHead"));
1729
+ return /* @__PURE__ */ h("div", { className: "artifacts-gitdiff" }, text.replace(/\n$/, "").split("\n").map((line, i) => /* @__PURE__ */ h("div", {
1730
+ key: i,
1731
+ className: gitDiffLineClass(line)
1732
+ }, line)));
1154
1733
  }
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 || "读取失败");
1734
+ function renderPreview(p, codeWrap = false) {
1735
+ if (p.loading) return /* @__PURE__ */ h("div", { className: "artifacts-hint" }, t("loading"));
1736
+ if (p.ok === false) return /* @__PURE__ */ h("div", { className: "artifacts-error" }, p.error || t("readFailed"));
1158
1737
  if (p.git) return /* @__PURE__ */ h("div", { className: "artifacts-preview-body" }, /* @__PURE__ */ h(GitDiffView, { diff: p.diff }));
1159
1738
  const type = p.type || extType(p.path);
1160
1739
  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
- });
1740
+ if (type === "image") view = /* @__PURE__ */ h(ImageView, { path: p.path || "" });
1166
1741
  else if (type === "html") view = /* @__PURE__ */ h("iframe", {
1167
1742
  className: "artifacts-iframe",
1168
1743
  sandbox: "allow-scripts",
@@ -1176,8 +1751,9 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1176
1751
  });
1177
1752
  else view = /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h(CodeView, {
1178
1753
  code: p.content,
1179
- path: p.path
1180
- }), p.truncated ? /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, "(truncated preview)") : null);
1754
+ path: p.path,
1755
+ wrap: codeWrap
1756
+ }), p.truncated ? /* @__PURE__ */ h("div", { className: "artifacts-diff-label" }, t("truncated")) : null);
1181
1757
  return /* @__PURE__ */ h("div", { className: "artifacts-preview-body" }, p.diff && typeof p.diff === "object" ? renderDiff(p.diff) : null, view);
1182
1758
  }
1183
1759
  //#endregion
@@ -1335,43 +1911,139 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1335
1911
  strokeLinecap: "round",
1336
1912
  fill: "none"
1337
1913
  }));
1914
+ /** 搜索图标:放大镜(文件搜索开关) */
1915
+ const SearchIcon = ({ size }) => /* @__PURE__ */ h("svg", {
1916
+ width: size,
1917
+ height: size,
1918
+ viewBox: "0 0 16 16",
1919
+ fill: "none",
1920
+ "aria-hidden": "true"
1921
+ }, /* @__PURE__ */ h("circle", {
1922
+ cx: "7",
1923
+ cy: "7",
1924
+ r: "4.4",
1925
+ stroke: "currentColor",
1926
+ strokeWidth: 1.4
1927
+ }), /* @__PURE__ */ h("path", {
1928
+ d: "m10.4 10.4 3.1 3.1",
1929
+ stroke: "currentColor",
1930
+ strokeWidth: 1.4,
1931
+ strokeLinecap: "round"
1932
+ }));
1338
1933
  //#endregion
1339
1934
  //#region src/client/components.tsx
1340
1935
  /**
1341
1936
  * Client 侧:UI 组件 —— 文件树、多标签预览侧边面板、角落触发按钮、设置区。
1342
1937
  */
1343
- function FileTree({ onOpen, selectedPath, refreshToken }) {
1938
+ function FileTree({ onOpen, selectedPath, refreshToken, searchOpen = false, onCloseSearch }) {
1344
1939
  const [root, setRoot] = React.useState(null);
1345
1940
  const [children, setChildren] = React.useState({});
1346
1941
  const [expanded, setExpanded] = React.useState({});
1347
1942
  const [copiedPath, setCopiedPath] = React.useState(null);
1348
1943
  const [copiedLabel, setCopiedLabel] = React.useState("");
1944
+ const [query, setQuery] = React.useState("");
1945
+ const [search, setSearch] = React.useState(null);
1349
1946
  const copyTimer = React.useRef(null);
1350
1947
  const rootTimer = React.useRef(null);
1351
1948
  const sessionId = useSessionId();
1352
- const loadRoot = () => {
1353
- setChildren({});
1354
- setExpanded({});
1355
- setRoot(null);
1949
+ React.useEffect(() => {
1950
+ if (!searchOpen) setQuery("");
1951
+ }, [searchOpen]);
1952
+ React.useEffect(() => {
1953
+ const q = query.trim();
1954
+ if (!q) {
1955
+ setSearch(null);
1956
+ return;
1957
+ }
1958
+ let alive = true;
1959
+ setSearch({ loading: true });
1960
+ const timer = setTimeout(() => {
1961
+ host.searchFiles(q, currentSessionId()).then((res) => {
1962
+ if (alive) setSearch(res && res.ok ? { entries: res.entries || [] } : { error: res && res.error || t("searchFailed") });
1963
+ }).catch(() => {
1964
+ if (alive) setSearch({ error: t("searchFailed") });
1965
+ });
1966
+ }, 250);
1967
+ return () => {
1968
+ alive = false;
1969
+ clearTimeout(timer);
1970
+ };
1971
+ }, [
1972
+ query,
1973
+ sessionId,
1974
+ refreshToken
1975
+ ]);
1976
+ const fetchDir = (path) => {
1977
+ const sid = currentSessionId();
1978
+ setChildren((prev) => ({
1979
+ ...prev,
1980
+ [path]: { loading: true }
1981
+ }));
1982
+ host.listDir(path, sid).then((res) => {
1983
+ if (currentSessionId() !== sid) return;
1984
+ setChildren((prev) => ({
1985
+ ...prev,
1986
+ [path]: res && res.ok ? { entries: res.entries || [] } : { error: res && res.error || t("readFailed") }
1987
+ }));
1988
+ }).catch(() => {
1989
+ if (currentSessionId() !== sid) return;
1990
+ setChildren((prev) => ({
1991
+ ...prev,
1992
+ [path]: { error: t("readFailed") }
1993
+ }));
1994
+ });
1995
+ };
1996
+ const [rootError, setRootError] = React.useState(null);
1997
+ const loadRoot = (keepState = false) => {
1998
+ if (!keepState) {
1999
+ setChildren({});
2000
+ setExpanded({});
2001
+ setRoot(null);
2002
+ }
2003
+ setRootError(null);
2004
+ if (keepState) {
2005
+ for (const p of Object.keys(expanded)) if (expanded[p]) fetchDir(p);
2006
+ }
1356
2007
  if (rootTimer.current) clearTimeout(rootTimer.current);
1357
- const attempt = (tries) => {
2008
+ const delays = [
2009
+ 400,
2010
+ 800,
2011
+ 1600,
2012
+ 3200,
2013
+ 3200
2014
+ ];
2015
+ const sid = currentSessionId();
2016
+ const attempt = (i) => {
1358
2017
  host.listDir("", currentSessionId()).then((res) => {
2018
+ if (currentSessionId() !== sid) return;
1359
2019
  if (res && res.ok) setRoot({
1360
2020
  path: res.path || "",
1361
2021
  entries: res.entries || []
1362
2022
  });
1363
- else if (tries > 0) rootTimer.current = setTimeout(() => attempt(tries - 1), 400);
2023
+ else if (i < delays.length) rootTimer.current = setTimeout(() => attempt(i + 1), delays[i]);
2024
+ else if (!keepState) setRootError(res && res.error || t("treeLoadFailed"));
1364
2025
  }).catch(() => {
1365
- if (tries > 0) rootTimer.current = setTimeout(() => attempt(tries - 1), 400);
2026
+ if (currentSessionId() !== sid) return;
2027
+ if (i < delays.length) rootTimer.current = setTimeout(() => attempt(i + 1), delays[i]);
2028
+ else if (!keepState) setRootError(t("treeLoadFailed"));
1366
2029
  });
1367
2030
  };
1368
- attempt(3);
2031
+ attempt(0);
1369
2032
  };
2033
+ const firstTreeRender = React.useRef(true);
1370
2034
  React.useEffect(() => {
1371
2035
  loadRoot();
1372
- }, [sessionId, refreshToken]);
2036
+ }, [sessionId]);
2037
+ React.useEffect(() => {
2038
+ if (firstTreeRender.current) {
2039
+ firstTreeRender.current = false;
2040
+ return;
2041
+ }
2042
+ loadRoot(root != null);
2043
+ }, [refreshToken]);
1373
2044
  React.useEffect(() => () => {
1374
2045
  if (rootTimer.current) clearTimeout(rootTimer.current);
2046
+ if (copyTimer.current) clearTimeout(copyTimer.current);
1375
2047
  }, []);
1376
2048
  const toggle = (path) => {
1377
2049
  const nextExpanded = {
@@ -1379,27 +2051,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1379
2051
  [path]: !expanded[path]
1380
2052
  };
1381
2053
  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
- }
2054
+ if (nextExpanded[path] && !children[path]) fetchDir(path);
1399
2055
  };
1400
2056
  const copyRef = (path) => {
1401
2057
  const text = "@" + path;
1402
- let label = "已复制";
2058
+ let label = t("copied");
1403
2059
  const done = () => {
1404
2060
  setCopiedPath(path);
1405
2061
  setCopiedLabel(label);
@@ -1410,7 +2066,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1410
2066
  }, 1600);
1411
2067
  };
1412
2068
  if (quoteToComposer(path)) {
1413
- label = "已插入输入框";
2069
+ label = t("insertedInput");
1414
2070
  done();
1415
2071
  return;
1416
2072
  }
@@ -1423,15 +2079,15 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1423
2079
  done();
1424
2080
  }
1425
2081
  };
1426
- const rowActions = (entry) => copiedPath === entry.path ? /* @__PURE__ */ h("span", { className: "artifacts-tree-copied" }, copiedLabel || "已复制") : /* @__PURE__ */ h("button", {
2082
+ const rowActions = (entry) => copiedPath === entry.path ? /* @__PURE__ */ h("span", { className: "artifacts-tree-copied" }, copiedLabel || t("copied")) : /* @__PURE__ */ h("button", {
1427
2083
  type: "button",
1428
2084
  className: "artifacts-tree-ref",
1429
- title: "引用到输入框(失败则复制 @path)",
2085
+ title: t("refTitle"),
1430
2086
  onClick: (e) => {
1431
2087
  e.stopPropagation();
1432
2088
  copyRef(entry.path);
1433
2089
  }
1434
- }, "@引用");
2090
+ }, t("refBtn"));
1435
2091
  const renderNode = (entry, depth, flashDelay) => {
1436
2092
  const pad = { paddingLeft: 6 + depth * 20 };
1437
2093
  const isSelected = selectedPath === entry.path;
@@ -1459,7 +2115,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1459
2115
  }, 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
2116
  className: "artifacts-tree-row artifacts-tree-loading",
1461
2117
  style: { paddingLeft: 6 + (depth + 1) * 20 + 20 }
1462
- }, "加载中…") : node && node.error ? /* @__PURE__ */ h("div", {
2118
+ }, t("loading")) : node && node.error ? /* @__PURE__ */ h("div", {
1463
2119
  className: "artifacts-tree-row artifacts-tree-error",
1464
2120
  style: { paddingLeft: 6 + (depth + 1) * 20 + 20 }
1465
2121
  }, node.error) : node && node.entries ? node.entries.map((c) => renderNode(c, depth + 1)) : null : null);
@@ -1484,15 +2140,64 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1484
2140
  title: entry.path
1485
2141
  }, /* @__PURE__ */ h(FileCodeIcon, { size: 14 }), /* @__PURE__ */ h("span", { className: "artifacts-tree-name" }, entry.name), rowActions(entry));
1486
2142
  };
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))));
2143
+ const renderSearchRow = (path, idx) => {
2144
+ const entry = {
2145
+ name: basename(path),
2146
+ path,
2147
+ isDir: false,
2148
+ hidden: false
2149
+ };
2150
+ const slash = path.lastIndexOf("/");
2151
+ const dir = slash >= 0 ? path.slice(0, slash) : "";
2152
+ return /* @__PURE__ */ h("div", {
2153
+ key: path,
2154
+ role: "button",
2155
+ tabIndex: 0,
2156
+ className: "artifacts-tree-row artifacts-flash-in" + (selectedPath === path ? " is-selected" : ""),
2157
+ style: { animationDelay: Math.min(idx, 12) * 30 + "ms" },
2158
+ onClick: () => {
2159
+ if (onOpen) onOpen(path);
2160
+ },
2161
+ onKeyDown: (ev) => {
2162
+ if (ev.key === "Enter" || ev.key === " ") {
2163
+ ev.preventDefault();
2164
+ if (onOpen) onOpen(path);
2165
+ }
2166
+ },
2167
+ title: path
2168
+ }, /* @__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));
2169
+ };
2170
+ return /* @__PURE__ */ h("div", { className: "artifacts-tree" }, searchOpen ? /* @__PURE__ */ h("div", { className: "artifacts-searchbar" }, /* @__PURE__ */ h("input", {
2171
+ type: "text",
2172
+ className: "artifacts-search-input",
2173
+ placeholder: t("searchPlaceholder"),
2174
+ value: query,
2175
+ autoFocus: true,
2176
+ onChange: (e) => setQuery(e.currentTarget.value),
2177
+ onKeyDown: (ev) => {
2178
+ if (ev.key === "Escape") {
2179
+ if (onCloseSearch) onCloseSearch();
2180
+ else setQuery("");
2181
+ }
2182
+ }
2183
+ })) : null, /* @__PURE__ */ h("div", {
2184
+ className: "artifacts-tree-body",
2185
+ key: refreshToken
2186
+ }, 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", {
2187
+ type: "button",
2188
+ className: "artifacts-retry",
2189
+ onClick: () => loadRoot(false)
2190
+ }, 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
2191
  }
1489
- function ArtifactsPanel() {
1490
- const open = useOpen();
1491
- const settings = useSettings();
2192
+ const TABS_KEY = "dsh-flyout-sidebar:tabs";
2193
+ /**
2194
+ * 多标签预览状态('p:' 前缀 = 内容预览,'g:' 前缀 = git diff):打开/关闭/
2195
+ * 会话切换清空/持久化与恢复集中在此 hook,面板组件只消费结果。
2196
+ */
2197
+ function usePreviewTabs(sessionId) {
1492
2198
  const [tabs, setTabs] = React.useState([]);
1493
2199
  const [activeKey, setActiveKey] = React.useState(null);
1494
2200
  const [previewHidden, setPreviewHidden] = React.useState(false);
1495
- const sessionId = useSessionId();
1496
2201
  const firstSession = React.useRef(true);
1497
2202
  React.useEffect(() => {
1498
2203
  if (firstSession.current) {
@@ -1502,18 +2207,219 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1502
2207
  setTabs([]);
1503
2208
  setActiveKey(null);
1504
2209
  setPreviewHidden(false);
2210
+ try {
2211
+ sessionStorage.removeItem(TABS_KEY);
2212
+ } catch {}
2213
+ }, [sessionId]);
2214
+ const restoredSid = React.useRef(null);
2215
+ React.useEffect(() => {
2216
+ if (restoredSid.current !== sessionId) return;
2217
+ try {
2218
+ if (!tabs.length) sessionStorage.removeItem(TABS_KEY);
2219
+ else sessionStorage.setItem(TABS_KEY, JSON.stringify({
2220
+ sid: sessionId,
2221
+ tabs: tabs.map((tb) => ({
2222
+ key: tb.key,
2223
+ path: tb.path,
2224
+ git: tb.git
2225
+ })),
2226
+ activeKey
2227
+ }));
2228
+ } catch {}
2229
+ }, [
2230
+ tabs,
2231
+ activeKey,
2232
+ sessionId
2233
+ ]);
2234
+ const patchTab = (key, patch) => setTabs((prev) => prev.map((tb) => tb.key === key ? {
2235
+ ...tb,
2236
+ ...patch
2237
+ } : tb));
2238
+ const reqTokens = React.useRef({});
2239
+ const openTab = (key, path, git, initial) => {
2240
+ setPreviewHidden(false);
2241
+ reqTokens.current[key] = (reqTokens.current[key] || 0) + 1;
2242
+ setTabs((prev) => {
2243
+ const i = prev.findIndex((tb) => tb.key === key);
2244
+ if (i >= 0) {
2245
+ const next = prev.slice();
2246
+ const cur = next[i];
2247
+ if (cur) next[i] = {
2248
+ ...cur,
2249
+ ...initial,
2250
+ ok: void 0,
2251
+ error: void 0
2252
+ };
2253
+ return next;
2254
+ }
2255
+ return prev.concat([{
2256
+ key,
2257
+ path,
2258
+ git,
2259
+ ...initial
2260
+ }]);
2261
+ });
2262
+ setActiveKey(key);
2263
+ };
2264
+ const closeTab = (key) => {
2265
+ setTabs((prev) => {
2266
+ prev.findIndex((tb) => tb.key === key);
2267
+ return prev.filter((tb) => tb.key !== key);
2268
+ });
2269
+ setActiveKey((cur) => {
2270
+ if (cur !== key) return cur;
2271
+ const remaining = tabsRef.current.filter((tb) => tb.key !== key);
2272
+ if (!remaining.length) return null;
2273
+ const closedIdx = tabsRef.current.findIndex((tb) => tb.key === key);
2274
+ return remaining[Math.min(Math.max(closedIdx, 0), remaining.length - 1)]?.key ?? null;
2275
+ });
2276
+ };
2277
+ const tabsRef = React.useRef([]);
2278
+ React.useEffect(() => {
2279
+ tabsRef.current = tabs;
2280
+ }, [tabs]);
2281
+ const openFile = (path) => {
2282
+ const key = "p:" + path;
2283
+ const type = extType(path);
2284
+ const initial = {
2285
+ loading: false,
2286
+ type,
2287
+ diff: null
2288
+ };
2289
+ if (type !== "image" && type !== "pdf") initial.loading = true;
2290
+ openTab(key, path, false, initial);
2291
+ if (type === "image" || type === "pdf") return;
2292
+ const myReq = reqTokens.current[key];
2293
+ host.readArtifact(path, sessionId).then((res) => {
2294
+ if (reqTokens.current[key] !== myReq) return;
2295
+ patchTab(key, {
2296
+ loading: false,
2297
+ ...res
2298
+ });
2299
+ }).catch((e) => {
2300
+ if (reqTokens.current[key] !== myReq) return;
2301
+ patchTab(key, {
2302
+ loading: false,
2303
+ ok: false,
2304
+ error: String(e instanceof Error && e.message ? e.message : e)
2305
+ });
2306
+ });
2307
+ };
2308
+ const openGitDiff = (path) => {
2309
+ const key = "g:" + path;
2310
+ openTab(key, path, true, { loading: true });
2311
+ const myReq = reqTokens.current[key];
2312
+ host.gitDiff(path, sessionId).then((res) => {
2313
+ if (reqTokens.current[key] !== myReq) return;
2314
+ patchTab(key, {
2315
+ loading: false,
2316
+ ...res
2317
+ });
2318
+ }).catch((e) => {
2319
+ if (reqTokens.current[key] !== myReq) return;
2320
+ patchTab(key, {
2321
+ loading: false,
2322
+ ok: false,
2323
+ error: String(e instanceof Error && e.message ? e.message : e)
2324
+ });
2325
+ });
2326
+ };
2327
+ React.useEffect(() => {
2328
+ if (restoredSid.current === sessionId) return;
2329
+ restoredSid.current = sessionId;
2330
+ if (sessionId == null) return;
2331
+ try {
2332
+ const raw = sessionStorage.getItem(TABS_KEY);
2333
+ if (!raw) return;
2334
+ const saved = JSON.parse(raw);
2335
+ const sid = saved.sid;
2336
+ const list = saved.tabs;
2337
+ if (sid !== sessionId || !Array.isArray(list)) return;
2338
+ for (const st of list) {
2339
+ if (typeof st?.key !== "string" || typeof st?.path !== "string") continue;
2340
+ if (st.git) openGitDiff(st.path);
2341
+ else openFile(st.path);
2342
+ }
2343
+ const savedActive = saved.activeKey;
2344
+ if (typeof savedActive === "string") setActiveKey(savedActive);
2345
+ } catch {}
1505
2346
  }, [sessionId]);
2347
+ return {
2348
+ tabs,
2349
+ activeKey,
2350
+ setActiveKey,
2351
+ previewHidden,
2352
+ setPreviewHidden,
2353
+ openFile,
2354
+ openGitDiff,
2355
+ closeTab
2356
+ };
2357
+ }
2358
+ /** git 变更列表(文件面板的「变更」视图) */
2359
+ function GitChangesList({ files, error, activeDiffPath, flashKey, onOpen, onCopyPath, onQuote }) {
2360
+ const rows = [];
2361
+ if (error) rows.push(/* @__PURE__ */ h("div", {
2362
+ key: "err",
2363
+ className: "artifacts-tree-error artifacts-git-error"
2364
+ }, error));
2365
+ else if (files == null) rows.push(/* @__PURE__ */ h("div", {
2366
+ key: "load",
2367
+ className: "artifacts-empty"
2368
+ }, t("loadingChanges")));
2369
+ else if (!files.length) rows.push(/* @__PURE__ */ h("div", {
2370
+ key: "empty",
2371
+ className: "artifacts-empty"
2372
+ }, t("noChanges")));
2373
+ (files || []).forEach((e, idx) => {
2374
+ const label = gitLabel(e);
2375
+ const isActive = activeDiffPath === e.path;
2376
+ const flashPrefix = flashKey > 0 ? flashKey + ":" : "";
2377
+ rows.push(/* @__PURE__ */ h("div", {
2378
+ key: flashPrefix + e.path,
2379
+ className: "artifacts-item" + (isActive ? " is-active" : "") + (flashPrefix ? " artifacts-flash-in" : ""),
2380
+ style: flashPrefix ? { animationDelay: Math.min(idx, 12) * 45 + "ms" } : void 0
2381
+ }, /* @__PURE__ */ h("button", {
2382
+ type: "button",
2383
+ className: "artifacts-item-main",
2384
+ title: gitTitle(e),
2385
+ onClick: () => onOpen(e.path)
2386
+ }, /* @__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", {
2387
+ type: "button",
2388
+ className: "artifacts-minibtn",
2389
+ title: t("copyPath"),
2390
+ onClick: () => onCopyPath(e.path)
2391
+ }, "⧉"), /* @__PURE__ */ h("button", {
2392
+ type: "button",
2393
+ className: "artifacts-minibtn",
2394
+ title: t("refInput"),
2395
+ onClick: () => onQuote(e.path)
2396
+ }, "@"))));
2397
+ });
2398
+ return /* @__PURE__ */ h(Fragment, null, rows);
2399
+ }
2400
+ function ArtifactsPanel() {
2401
+ const open = useOpen();
2402
+ const settings = useSettings();
2403
+ useLang();
2404
+ const sessionId = useSessionId();
2405
+ const { tabs, activeKey, setActiveKey, previewHidden, setPreviewHidden, openFile, openGitDiff, closeTab } = usePreviewTabs(sessionId);
2406
+ const activeTab = tabs.find((t) => t.key === activeKey) || null;
1506
2407
  const [notice, setNotice] = React.useState("");
2408
+ const [winW, setWinW] = React.useState(() => typeof window !== "undefined" ? window.innerWidth : 1400);
1507
2409
  const [gitFiles, setGitFiles] = React.useState(null);
1508
2410
  const [gitError, setGitError] = React.useState(null);
1509
- const [panelWidth, setPanelWidth] = React.useState(null);
2411
+ const [panelFrac, setPanelFrac] = React.useState(null);
1510
2412
  const [resizing, setResizing] = React.useState(false);
1511
2413
  const [activeView, setActiveView] = React.useState(() => settings.showFileTree ? "tree" : "git");
1512
2414
  const [treeRefresh, setTreeRefresh] = React.useState(0);
2415
+ const [searchOpen, setSearchOpen] = React.useState(false);
1513
2416
  const [gitRefresh, setGitRefresh] = React.useState(0);
1514
2417
  const [gitRefreshing, setGitRefreshing] = React.useState(false);
1515
2418
  const [gitFlash, setGitFlash] = React.useState(0);
1516
2419
  const noticeTimer = React.useRef(null);
2420
+ React.useEffect(() => () => {
2421
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
2422
+ }, []);
1517
2423
  const gitForceRef = React.useRef(false);
1518
2424
  React.useEffect(() => {
1519
2425
  if (!open || activeView !== "git") return;
@@ -1532,7 +2438,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1532
2438
  setGitError(null);
1533
2439
  } else {
1534
2440
  setGitFiles([]);
1535
- setGitError(res && res.error || "git status 失败");
2441
+ setGitError(res && res.error || t("gitStatusFailed"));
1536
2442
  }
1537
2443
  }).catch((e) => {
1538
2444
  if (force) setGitRefreshing(false);
@@ -1595,8 +2501,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1595
2501
  if (document.body) obs.observe(document.body, opts);
1596
2502
  return () => obs.disconnect();
1597
2503
  }, []);
1598
- const minWidthPx = Math.max(80, Math.round(window.innerWidth * (settings.minPanelWidth || 0) / 100));
1599
- const widthPx = panelWidth != null ? Math.max(panelWidth, minWidthPx) : minWidthPx;
2504
+ React.useEffect(() => {
2505
+ const onResize = () => setWinW(window.innerWidth);
2506
+ window.addEventListener("resize", onResize);
2507
+ return () => window.removeEventListener("resize", onResize);
2508
+ }, []);
2509
+ const rightOffset = (() => {
2510
+ const n = parseFloat(document.documentElement.style.getPropertyValue("--dsh-sidebar-width"));
2511
+ return Number.isFinite(n) ? n : 0;
2512
+ })();
2513
+ const avail = Math.max(120, winW - rightOffset);
2514
+ const minWidthPx = Math.max(80, Math.round(winW * (settings.minPanelWidth || 0) / 100));
2515
+ const widthPx = panelFrac != null ? Math.max(minWidthPx, Math.round(panelFrac * avail)) : minWidthPx;
1600
2516
  React.useEffect(() => {
1601
2517
  const root = document.documentElement;
1602
2518
  root.style.setProperty("--dsh-flyout-sidebar-width", open ? widthPx + "px" : "0px");
@@ -1611,26 +2527,36 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1611
2527
  document.body.removeAttribute("data-dsh-flyout-dragging");
1612
2528
  };
1613
2529
  }, [resizing]);
2530
+ const resizeHandlers = React.useRef(null);
2531
+ React.useEffect(() => () => {
2532
+ const h = resizeHandlers.current;
2533
+ if (h) {
2534
+ document.removeEventListener("mousemove", h.onMove);
2535
+ document.removeEventListener("mouseup", h.onUp);
2536
+ }
2537
+ }, []);
1614
2538
  const { visible, slidOut } = useSlide();
1615
2539
  if (!visible) return null;
1616
2540
  const flyoutHref = "/flyout-sidebar" + (sessionId ? "?sessionId=" + encodeURIComponent(sessionId) : "");
1617
2541
  const startResize = (e) => {
1618
2542
  e.preventDefault();
1619
2543
  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
- })();
2544
+ const availAtStart = avail;
1625
2545
  const onMove = (ev) => {
1626
2546
  const w = window.innerWidth - ev.clientX - rightOffset;
1627
- setPanelWidth(Math.max(minWidthPx, Math.min(w, window.innerWidth - rightOffset - 24)));
2547
+ const frac = Math.max(minWidthPx / availAtStart, Math.min(w / availAtStart, (availAtStart - 24) / availAtStart));
2548
+ setPanelFrac(frac);
1628
2549
  };
1629
2550
  const onUp = () => {
1630
2551
  setResizing(false);
2552
+ resizeHandlers.current = null;
1631
2553
  document.removeEventListener("mousemove", onMove);
1632
2554
  document.removeEventListener("mouseup", onUp);
1633
2555
  };
2556
+ resizeHandlers.current = {
2557
+ onMove,
2558
+ onUp
2559
+ };
1634
2560
  document.addEventListener("mousemove", onMove);
1635
2561
  document.addEventListener("mouseup", onUp);
1636
2562
  };
@@ -1652,162 +2578,53 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1652
2578
  };
1653
2579
  const quotePath = (path) => {
1654
2580
  if (quoteToComposer(path)) {
1655
- flash("已插入输入框");
2581
+ flash(t("insertedInput"));
1656
2582
  return;
1657
2583
  }
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);
1691
- };
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
- });
2584
+ copyText("@" + path, t("copiedRef"));
1732
2585
  };
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: "复制"
2586
+ React.useEffect(() => {
2587
+ if (!open) return;
2588
+ const onKey = (ev) => {
2589
+ if (ev.key !== "Escape") return;
2590
+ const target = ev.target;
2591
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
2592
+ if (tabs.length && !previewHidden) {
2593
+ if (activeKey) closeTab(activeKey);
2594
+ else setPreviewHidden(true);
2595
+ } else store.setOpen(false);
1746
2596
  };
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
- });
2597
+ document.addEventListener("keydown", onKey);
2598
+ return () => document.removeEventListener("keydown", onKey);
2599
+ }, [
2600
+ open,
2601
+ tabs,
2602
+ previewHidden,
2603
+ activeKey
2604
+ ]);
1788
2605
  const previewOverlay = tabs.length && !previewHidden ? /* @__PURE__ */ h("div", {
1789
2606
  className: "artifacts-preview-overlay" + (slidOut ? " artifacts-slid-out" : ""),
1790
2607
  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", {
2608
+ "aria-label": t("previewRegion")
2609
+ }, /* @__PURE__ */ h("div", { className: "artifacts-preview-overlay-tabs" }, /* @__PURE__ */ h("div", { className: "artifacts-ptabs-scroll" }, tabs.map((tab) => /* @__PURE__ */ h("div", {
2610
+ key: tab.key,
2611
+ className: "artifacts-ptab" + (tab.key === activeKey ? " is-active" : ""),
2612
+ title: (tab.git ? t("diffTabPrefix") : "") + (tab.path || ""),
2613
+ onClick: () => setActiveKey(tab.key)
2614
+ }, /* @__PURE__ */ h("span", { className: "artifacts-ptab-name" }, basename(tab.path || "")), /* @__PURE__ */ h("button", {
1798
2615
  type: "button",
1799
2616
  className: "artifacts-ptab-close",
1800
- title: "关闭标签页",
2617
+ title: t("closeTab"),
1801
2618
  onClick: (e) => {
1802
2619
  e.stopPropagation();
1803
- closeTab(t.key);
2620
+ closeTab(tab.key);
1804
2621
  }
1805
2622
  }, "×")))), /* @__PURE__ */ h("button", {
1806
2623
  type: "button",
1807
2624
  className: "artifacts-preview-hide",
1808
- title: "隐藏预览(标签页保留)",
2625
+ title: t("hidePreview"),
1809
2626
  onClick: () => setPreviewHidden(true)
1810
- }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab) : null) : null;
2627
+ }, /* @__PURE__ */ h(PanelCollapseIcon, { size: 16 }))), activeTab ? renderPreview(activeTab, settings.codeWrap) : null) : null;
1811
2628
  return /* @__PURE__ */ h(Fragment, null, previewOverlay, /* @__PURE__ */ h("div", {
1812
2629
  className: "artifacts-panel" + (slidOut ? " artifacts-slid-out" : "") + (resizing ? " artifacts-resizing" : ""),
1813
2630
  style: { width: widthPx },
@@ -1815,23 +2632,29 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1815
2632
  "aria-label": "Artifacts"
1816
2633
  }, /* @__PURE__ */ h("div", {
1817
2634
  className: "artifacts-resize",
1818
- title: "拖动调整宽度",
2635
+ title: t("resizeHandle"),
1819
2636
  onMouseDown: startResize
1820
2637
  }), /* @__PURE__ */ h("div", { className: "artifacts-head" }, /* @__PURE__ */ h("div", { className: "artifacts-head-left" }, /* @__PURE__ */ h("button", {
1821
2638
  type: "button",
1822
2639
  className: "artifacts-toggle",
1823
- title: "收起侧边栏",
2640
+ title: t("collapsePanel"),
1824
2641
  onClick: () => store.setOpen(false)
1825
2642
  }, /* @__PURE__ */ h(PanelIcon, { size: 16 })), /* @__PURE__ */ h("a", {
1826
2643
  className: "artifacts-link",
1827
2644
  href: flyoutHref,
1828
2645
  target: "_blank",
1829
2646
  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", {
2647
+ title: t("flyoutOpen")
2648
+ }, /* @__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", {
2649
+ type: "button",
2650
+ className: "artifacts-toggle artifacts-search-toggle" + (searchOpen ? " is-active" : ""),
2651
+ title: t("searchToggle"),
2652
+ "aria-pressed": searchOpen,
2653
+ onClick: () => setSearchOpen((v) => !v)
2654
+ }, /* @__PURE__ */ h(SearchIcon, { size: 16 })) : null, /* @__PURE__ */ h("button", {
1832
2655
  type: "button",
1833
2656
  className: "artifacts-toggle",
1834
- title: activeView === "tree" ? "刷新文件树" : "刷新变更列表",
2657
+ title: activeView === "tree" ? t("refreshTree") : t("refreshChanges"),
1835
2658
  onClick: () => {
1836
2659
  if (activeView === "tree") setTreeRefresh((n) => n + 1);
1837
2660
  else {
@@ -1843,24 +2666,35 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1843
2666
  }, /* @__PURE__ */ h(RefreshIcon, { size: 16 })), settings.showFileTree ? /* @__PURE__ */ h("button", {
1844
2667
  type: "button",
1845
2668
  className: "artifacts-iconbtn artifacts-viewbtn" + (activeView === "git" ? " is-active" : ""),
1846
- title: activeView === "tree" ? "查看 Git 变更(未提交)" : "返回文件列表",
2669
+ title: activeView === "tree" ? t("viewGit") : t("backToFiles"),
1847
2670
  "aria-pressed": activeView === "git",
1848
2671
  onClick: () => setActiveView(activeView === "tree" ? "git" : "tree")
1849
2672
  }, activeView === "tree" ? /* @__PURE__ */ h(GitBranchIcon, { size: 16 }) : /* @__PURE__ */ h(FolderClosedIcon, { size: 16 })) : null), /* @__PURE__ */ h("div", { className: "artifacts-main" }, /* @__PURE__ */ h("div", {
1850
2673
  className: "artifacts-body" + (activeView === "git" && gitRefreshing ? " artifacts-refreshing" : ""),
1851
2674
  style: { flex: "1 1 auto" }
1852
- }, activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h(FileTree, {
2675
+ }, open ? activeView === "tree" && settings.showFileTree ? /* @__PURE__ */ h(FileTree, {
1853
2676
  onOpen: openFile,
1854
2677
  selectedPath: activeTab && !activeTab.git ? activeTab.path : null,
1855
- refreshToken: treeRefresh
1856
- }) : gitListRows))));
2678
+ refreshToken: treeRefresh,
2679
+ searchOpen,
2680
+ onCloseSearch: () => setSearchOpen(false)
2681
+ }) : /* @__PURE__ */ h(GitChangesList, {
2682
+ files: gitFiles,
2683
+ error: gitError,
2684
+ activeDiffPath: activeTab && activeTab.git ? activeTab.path : null,
2685
+ flashKey: gitFlash,
2686
+ onOpen: openGitDiff,
2687
+ onCopyPath: (p) => copyText(p, t("copiedPath")),
2688
+ onQuote: quotePath
2689
+ }) : null))));
1857
2690
  }
1858
2691
  function CornerButton() {
1859
2692
  const open = useOpen();
2693
+ useLang();
1860
2694
  return /* @__PURE__ */ h("button", {
1861
2695
  type: "button",
1862
2696
  className: "artifacts-corner-btn" + (open ? " artifacts-slid-out" : ""),
1863
- title: "弹出式侧边栏",
2697
+ title: t("flyoutTitle"),
1864
2698
  "aria-expanded": open,
1865
2699
  onClick: () => store.toggle()
1866
2700
  }, /* @__PURE__ */ h(PanelIcon, { size: 18 }));
@@ -1878,23 +2712,29 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1878
2712
  }
1879
2713
  function SettingsSection() {
1880
2714
  const settings = useSettings();
2715
+ useLang();
1881
2716
  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: "页面加载后侧边栏默认展开;关闭则默认收起,点右上角图标再打开。",
2717
+ return /* @__PURE__ */ h("div", { className: "artifacts-settings" }, /* @__PURE__ */ h("p", { className: "artifacts-setintro" }, t("settingsIntro")), /* @__PURE__ */ h("div", { className: "artifacts-setgroup" }, /* @__PURE__ */ h(SettingsToggle, {
2718
+ label: t("setDefaultOpen"),
2719
+ desc: t("setDefaultOpenDesc"),
1885
2720
  value: settings.defaultOpen,
1886
2721
  onToggle: (v) => set("defaultOpen", v)
1887
2722
  }), /* @__PURE__ */ h(SettingsToggle, {
1888
- label: "自动刷新",
1889
- desc: "开启后侧边栏展开时将即时同步并更新产物列表",
2723
+ label: t("setAutoRefresh"),
2724
+ desc: t("setAutoRefreshDesc"),
1890
2725
  value: settings.autoRefresh,
1891
2726
  onToggle: (v) => set("autoRefresh", v)
1892
2727
  }), /* @__PURE__ */ h(SettingsToggle, {
1893
- label: "文件树",
1894
- desc: "在侧边栏显示「文件树」标签页,浏览工作区目录。",
2728
+ label: t("setFileTree"),
2729
+ desc: t("setFileTreeDesc"),
1895
2730
  value: settings.showFileTree,
1896
2731
  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", {
2732
+ }), /* @__PURE__ */ h(SettingsToggle, {
2733
+ label: t("setCodeWrap"),
2734
+ desc: t("setCodeWrapDesc"),
2735
+ value: settings.codeWrap,
2736
+ onToggle: (v) => set("codeWrap", v)
2737
+ }), /* @__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
2738
  type: "number",
1899
2739
  className: "artifacts-widthinput",
1900
2740
  min: 20,
@@ -1904,8 +2744,19 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
1904
2744
  const n = parseInt(e.currentTarget.value, 10);
1905
2745
  if (Number.isNaN(n)) return;
1906
2746
  set("minPanelWidth", Math.max(20, Math.min(60, n)));
2747
+ },
2748
+ onBlur: (e) => {
2749
+ const n = parseInt(e.currentTarget.value, 10);
2750
+ set("minPanelWidth", Number.isNaN(n) ? 20 : Math.max(20, Math.min(60, n)));
2751
+ }
2752
+ }), /* @__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", {
2753
+ className: "artifacts-langselect",
2754
+ value: getLanguageSetting() || "",
2755
+ onChange: (e) => {
2756
+ const v = e.currentTarget.value;
2757
+ setLanguage(v === "zh" || v === "en" ? v : null);
1907
2758
  }
1908
- }), /* @__PURE__ */ h("span", { className: "artifacts-suffix" }, "%")))));
2759
+ }, /* @__PURE__ */ h("option", { value: "" }, t("langAuto")), /* @__PURE__ */ h("option", { value: "zh" }, t("langZh")), /* @__PURE__ */ h("option", { value: "en" }, t("langEn")))))));
1909
2760
  }
1910
2761
  //#endregion
1911
2762
  //#region src/client/index.tsx