@weniatall/dsh-file-viewer 0.3.12 → 0.3.14

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.
Files changed (2) hide show
  1. package/lib/client.js +179 -16
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -163,6 +163,21 @@ window.__ModuleLoader__.load({
163
163
  );
164
164
  }
165
165
 
166
+ function SvgIconCopy({ size = 13 }) {
167
+ return react.createElement("svg", {
168
+ width: size, height: size, viewBox: "0 0 16 16", fill: "none",
169
+ stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round",
170
+ style: { flex: "none" },
171
+ },
172
+ react.createElement("rect", {
173
+ x: "5", y: "5", width: "8.5", height: "8.5", rx: "1.5",
174
+ }),
175
+ react.createElement("path", {
176
+ d: "M3.5 10.5H2.75C2.05964 10.5 1.5 9.94036 1.5 9.25V2.75C1.5 2.05964 2.05964 1.5 2.75 1.5H9.25C9.94036 1.5 10.5 2.05964 10.5 2.75V3.5",
177
+ })
178
+ );
179
+ }
180
+
166
181
  const Component = react.Component || class Component { constructor(props) { this.props = props; this.state = {}; } setState(s) { Object.assign(this.state, typeof s === "function" ? s(this.state) : s); } };
167
182
  class ErrorBoundary extends Component {
168
183
  constructor(props) {
@@ -557,6 +572,18 @@ window.__ModuleLoader__.load({
557
572
  .fv-tab{height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:none;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
558
573
  .fv-tab[aria-selected="true"]{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}
559
574
  .fv-tab-diff{display:inline-flex;align-items:center;gap:4px}
575
+ .fv-path-copy-wrap{position:relative;display:inline-flex;align-items:center}
576
+ .fv-tab.fv-tab-copy-path{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;cursor:pointer;user-select:none;transition:all .15s ease}
577
+ .fv-tab.fv-tab-copy-path:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
578
+ .fv-tab.fv-tab-copy-path[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-brand-primary,#4d6bfe);border-color:var(--dsw-alias-brand-primary,#4d6bfe)}
579
+ .fv-tab-copy-path .fv-tab-arrow{font-size:9px;margin-left:2px;opacity:.75}
580
+ .fv-path-copy-menu{position:absolute;top:calc(100% + 5px);left:0;z-index:60;min-width:210px;max-width:380px;padding:5px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);animation:fv-menu-in .12s cubic-bezier(.2,.8,.2,1)}
581
+ .fv-path-copy-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:6px 10px;border:none;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;user-select:none;transition:background-color .12s ease}
582
+ .fv-path-copy-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
583
+ .fv-path-copy-item-head{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:500}
584
+ .fv-path-copy-item-icon{flex:none;font-size:13px}
585
+ .fv-path-copy-item-label{flex:1 1 auto;white-space:nowrap}
586
+ .fv-path-copy-item-preview{font-size:11px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-code,monospace);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:21px;line-height:1.35}
560
587
  .fv-diff-stat{font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;margin-left:2px}
561
588
  .fv-diff-add{color:var(--dsw-alias-state-success-primary,#10b981)}
562
589
  .fv-diff-del{color:var(--dsw-alias-state-error-primary,#ef4444)}
@@ -3602,6 +3629,136 @@ window.__ModuleLoader__.load({
3602
3629
  : null);
3603
3630
  }
3604
3631
 
3632
+ function isAbsolutePath(p) {
3633
+ if (typeof p !== "string" || p === "") return false;
3634
+ return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith("/") || p.startsWith("\\\\");
3635
+ }
3636
+
3637
+ function resolveAbsPath(rootPath, relPath) {
3638
+ if (!rootPath) return relPath || "";
3639
+ const cleanRel = (relPath || "").replace(/^\.[\\/]+/, "").replace(/^[\\/]+/, "");
3640
+ if (isAbsolutePath(cleanRel)) return cleanRel;
3641
+ if (rootPath.includes("\\")) {
3642
+ return rootPath.replace(/[\\/]+$/, "") + "\\" + cleanRel.replace(/\//g, "\\");
3643
+ }
3644
+ return rootPath.replace(/[\\/]+$/, "") + "/" + cleanRel.replace(/\\/g, "/");
3645
+ }
3646
+
3647
+ function extractRelativeAndAbsolutePaths(item, rootPath) {
3648
+ if (!item) return { relPath: "", absPath: "" };
3649
+ let rel = item.path || "";
3650
+ if (isAbsolutePath(rel)) {
3651
+ // 如果 path 意外传了绝对路径,且位于 rootPath 之下,提取相对路径
3652
+ const normRoot = rootPath ? rootPath.replace(/\\/g, "/").replace(/\/+$/, "") : "";
3653
+ const normRel = rel.replace(/\\/g, "/");
3654
+ if (normRoot && (normRel.toLowerCase().startsWith(normRoot.toLowerCase() + "/") || normRel.startsWith(normRoot + "/"))) {
3655
+ rel = normRel.slice(normRoot.length + 1);
3656
+ }
3657
+ }
3658
+ if (!rel) {
3659
+ rel = item.name || "";
3660
+ }
3661
+ const cleanRel = String(rel).replace(/^\.[\\/]+/, "").replace(/^[\\/]+/, "");
3662
+
3663
+ // 绝对路径:优先考虑真实宿主路径 displayPath,或由 rootPath 与相对路径拼接
3664
+ let abs = "";
3665
+ if (isAbsolutePath(item.displayPath)) {
3666
+ abs = item.displayPath;
3667
+ } else if (isAbsolutePath(item.path)) {
3668
+ abs = item.path;
3669
+ } else {
3670
+ abs = resolveAbsPath(rootPath, cleanRel);
3671
+ }
3672
+
3673
+ return { relPath: cleanRel, absPath: abs };
3674
+ }
3675
+
3676
+ /** Path copy dropdown menu on viewer toolbar. */
3677
+ function PathCopyDropdown({ meta, root, onToast }) {
3678
+ const [open, setOpen] = react.useState(false);
3679
+ const containerRef = react.useRef(null);
3680
+
3681
+ react.useEffect(() => {
3682
+ if (!open) return undefined;
3683
+ const handlePointerDown = (e) => {
3684
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
3685
+ setOpen(false);
3686
+ }
3687
+ };
3688
+ const handleKeyDown = (e) => {
3689
+ if (e.key === "Escape") setOpen(false);
3690
+ };
3691
+ window.addEventListener("pointerdown", handlePointerDown);
3692
+ window.addEventListener("keydown", handleKeyDown);
3693
+ return () => {
3694
+ window.removeEventListener("pointerdown", handlePointerDown);
3695
+ window.removeEventListener("keydown", handleKeyDown);
3696
+ };
3697
+ }, [open]);
3698
+
3699
+ if (!meta || (!meta.path && !meta.displayPath && !meta.name)) return null;
3700
+
3701
+ const { relPath, absPath } = extractRelativeAndAbsolutePaths(meta, root);
3702
+
3703
+ const handleCopy = (e, text, label) => {
3704
+ e.preventDefault();
3705
+ e.stopPropagation();
3706
+ copyToClipboard(text).then((ok) => {
3707
+ if (onToast) onToast(ok ? "已复制" + label : "复制失败");
3708
+ setOpen(false);
3709
+ });
3710
+ };
3711
+
3712
+ return react.createElement("div", {
3713
+ ref: containerRef,
3714
+ className: "fv-path-copy-wrap",
3715
+ },
3716
+ react.createElement("button", {
3717
+ type: "button",
3718
+ className: "fv-tab fv-tab-copy-path",
3719
+ "aria-expanded": open ? "true" : "false",
3720
+ "aria-label": "路径复制",
3721
+ title: "点击选择复制相对路径或绝对路径",
3722
+ onClick: (e) => {
3723
+ e.stopPropagation();
3724
+ setOpen((curr) => !curr);
3725
+ },
3726
+ },
3727
+ react.createElement(SvgIconCopy, { size: 12 }),
3728
+ react.createElement("span", { className: "fv-tab-text" }, "复制路径"),
3729
+ react.createElement("span", { className: "fv-tab-arrow" }, open ? "▴" : "▾")),
3730
+ open
3731
+ ? react.createElement("div", {
3732
+ className: "fv-path-copy-menu",
3733
+ role: "menu",
3734
+ "aria-label": "路径复制选项",
3735
+ },
3736
+ react.createElement("button", {
3737
+ type: "button",
3738
+ className: "fv-path-copy-item",
3739
+ role: "menuitem",
3740
+ title: relPath,
3741
+ onClick: (e) => handleCopy(e, relPath, "相对路径"),
3742
+ },
3743
+ react.createElement("div", { className: "fv-path-copy-item-head" },
3744
+ react.createElement("span", { className: "fv-path-copy-item-icon" }, "📋"),
3745
+ react.createElement("span", { className: "fv-path-copy-item-label" }, "复制相对路径")),
3746
+ react.createElement("span", { className: "fv-path-copy-item-preview" }, relPath)),
3747
+ react.createElement("div", { className: "fv-context-divider" }),
3748
+ react.createElement("button", {
3749
+ type: "button",
3750
+ className: "fv-path-copy-item",
3751
+ role: "menuitem",
3752
+ title: absPath,
3753
+ onClick: (e) => handleCopy(e, absPath, "绝对路径"),
3754
+ },
3755
+ react.createElement("div", { className: "fv-path-copy-item-head" },
3756
+ react.createElement("span", { className: "fv-path-copy-item-icon" }, "📁"),
3757
+ react.createElement("span", { className: "fv-path-copy-item-label" }, "复制绝对路径")),
3758
+ react.createElement("span", { className: "fv-path-copy-item-preview" }, absPath)))
3759
+ : null);
3760
+ }
3761
+
3605
3762
  function apply(ctx) {
3606
3763
  ensureStyles(typeof document === "undefined" ? null : document);
3607
3764
  // `null` is closed; an object is open. Carrying the requesting session lets
@@ -4097,7 +4254,7 @@ window.__ModuleLoader__.load({
4097
4254
 
4098
4255
  /** Line-numbered diff viewer with dual line number gutters. */
4099
4256
  /** Text, Markdown and JSON: one windowed read, three renderings. */
4100
- function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }) {
4257
+ function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }) {
4101
4258
  // Callers mount this with a per-file key, so paging state starts fresh
4102
4259
  // for every file instead of leaking the previous file's window into the
4103
4260
  // first request.
@@ -4383,11 +4540,16 @@ window.__ModuleLoader__.load({
4383
4540
  : [contentTab, sourceTab, diffTab])].filter((tab) => tab !== null)
4384
4541
  : [];
4385
4542
 
4386
- const tabs = showTabs || Boolean(backControl || langControl || sizeControl)
4543
+ const copyControl = meta !== null
4544
+ ? react.createElement(PathCopyDropdown, { meta, root, onToast })
4545
+ : null;
4546
+
4547
+ const tabs = showTabs || Boolean(backControl || langControl || sizeControl || copyControl)
4387
4548
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
4388
4549
  react.createElement("div", { className: "fv-sheet-tabs-start" },
4389
4550
  backControl,
4390
- ...tabButtons),
4551
+ ...tabButtons,
4552
+ copyControl),
4391
4553
  (sizeControl || langControl)
4392
4554
  ? react.createElement("div", { className: "fv-sheet-tabs-end" },
4393
4555
  sizeControl,
@@ -4459,7 +4621,11 @@ window.__ModuleLoader__.load({
4459
4621
  }
4460
4622
 
4461
4623
  /** Pick the viewer for a file and keep unsupported formats useful. */
4462
- function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl }) {
4624
+ function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast }) {
4625
+ const copyControl = meta !== null
4626
+ ? react.createElement(PathCopyDropdown, { meta, root, onToast })
4627
+ : null;
4628
+
4463
4629
  if (meta === null) {
4464
4630
  // A tab exists but its metadata is still in flight. Saying "pick a
4465
4631
  // file" there would be a lie, and the reader would click again.
@@ -4470,8 +4636,8 @@ window.__ModuleLoader__.load({
4470
4636
  }
4471
4637
  if (meta.tooLarge || meta.kind === "binary") {
4472
4638
  return react.createElement("div", { className: "fv-binary-pane" },
4473
- backControl ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4474
- react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4639
+ (backControl || copyControl) ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4640
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4475
4641
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null) : null,
4476
4642
  react.createElement("div", { className: "fv-note" },
4477
4643
  react.createElement("strong", null, meta.name),
@@ -4506,9 +4672,9 @@ window.__ModuleLoader__.load({
4506
4672
  react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
4507
4673
  : null;
4508
4674
 
4509
- const headBar = (backControl || sizeControl)
4675
+ const headBar = (backControl || sizeControl || copyControl)
4510
4676
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4511
- react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4677
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4512
4678
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null)
4513
4679
  : null;
4514
4680
 
@@ -4526,7 +4692,7 @@ window.__ModuleLoader__.load({
4526
4692
  return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
4527
4693
  }
4528
4694
  return react.createElement(react.Fragment, null, banner,
4529
- react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }));
4695
+ react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }));
4530
4696
  }
4531
4697
 
4532
4698
 
@@ -4561,12 +4727,7 @@ window.__ModuleLoader__.load({
4561
4727
 
4562
4728
  if (!menu) return null;
4563
4729
  const { x, y, entry } = menu;
4564
- const relPath = entry.displayPath || entry.path;
4565
- const absPath = rootPath
4566
- ? (rootPath.includes("\\")
4567
- ? (rootPath.replace(/[\\/]+$/, "") + "\\" + relPath.replace(/\//g, "\\"))
4568
- : (rootPath.replace(/[\\/]+$/, "") + "/" + relPath.replace(/\\/g, "/")))
4569
- : relPath;
4730
+ const { relPath, absPath } = extractRelativeAndAbsolutePaths(entry, rootPath);
4570
4731
 
4571
4732
  const handleCopyRel = (e) => {
4572
4733
  e.stopPropagation();
@@ -6250,6 +6411,7 @@ window.__ModuleLoader__.load({
6250
6411
  backControl,
6251
6412
  sizeControl,
6252
6413
  langControl,
6414
+ onToast: showToast,
6253
6415
  })
6254
6416
  : react.createElement(FileView, {
6255
6417
  meta,
@@ -6268,6 +6430,7 @@ window.__ModuleLoader__.load({
6268
6430
  backControl,
6269
6431
  sizeControl,
6270
6432
  langControl,
6433
+ onToast: showToast,
6271
6434
  })))),
6272
6435
  contextMenu !== null
6273
6436
  ? react.createElement(ContextMenu, {
@@ -6606,7 +6769,7 @@ window.__ModuleLoader__.load({
6606
6769
  langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
6607
6770
  MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
6608
6771
  openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
6609
- triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconBranch, SvgIconCommit,
6772
+ triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, extractRelativeAndAbsolutePaths, isAbsolutePath, SvgIconBranch, SvgIconCommit,
6610
6773
  // Git panel: controller, views and the small formatters they share.
6611
6774
  GIT_PAGE_SIZE, createGitController, GitSidebar, CommitDetail, WorkspaceChangeDetail, DiffView, kindOfPath,
6612
6775
  formatCommitDate, formatCommitTimestamp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weniatall/dsh-file-viewer",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Workspace file viewer and Git panel for DeepSeek Harness: file tree, syntax-highlighted code, images, PDF, Excel and Word, plus local/remote branches, paged commit history, commit diffs, working-tree changes and changed-file navigation",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",