dsh-workspace-explorer 1.1.1 → 1.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 版本记录
2
2
 
3
+ ## v1.1.2 · 会话文件定位与拖放目标
4
+
5
+ - **单击在当前浏览页定位。** 点击 AI 交付的文件、改动卡片中的具体文件或回复里的本地文件链接,当前文件浏览页会跳到所在目录、选中文件并展开预览;没有打开的浏览页时会新建一个。
6
+ - **拖到右侧打开 DSH 原生视图。** 交付文件和本机文件链接拖到右侧提示区后打开 DSH 文档预览,改动卡片打开该文件的 DSH 差异审阅,可查看原生红绿对比。按住 Shift 点击改动文件也能直接打开差异。
7
+ - **拖放提示跟随目标。** 拖动交付文件时提示在 DSH 文档预览中打开,拖动改动文件时提示在 DSH 差异审阅中打开。
8
+ - **按文件头声明的编码预览 Python。** `.py` 文件会读取开头注释里的 `coding` 声明(如 GBK),按声明的编码显示;其他文本仍按 UTF-8 校验。
9
+
3
10
  ## v1.1.1 · 独立侧栏与会话文件拖放
4
11
 
5
12
  - **文件浏览独立于会话。** 切换会话或工作区时,右侧保留文件浏览标签、当前目录和预览。只有点击展开或收起按钮时才播放侧栏动画。
package/README.md CHANGED
@@ -19,7 +19,7 @@ dsh plugin --profile web add dsh-workspace-explorer
19
19
  在 Harness 的「插件 → 添加插件」中粘贴下方安装包链接,也可粘贴已下载的本地 `.tgz` 路径:
20
20
 
21
21
  ```text
22
- https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.1/dsh-workspace-explorer-1.1.1.tgz
22
+ https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.2/dsh-workspace-explorer-1.1.2.tgz
23
23
  ```
24
24
 
25
25
  已安装早期预览包的用户,先在插件列表移除旧条目,再重新安装。发布变更见 [版本记录](CHANGELOG.md)。
@@ -30,9 +30,9 @@ https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.1/dsh-wo
30
30
 
31
31
  ## 界面实拍
32
32
 
33
- 大图标视图、收藏夹和图片预览
33
+ **大图标视图、收藏夹和视频预览**
34
34
 
35
- ![大图标视图中的图片缩略图、收藏夹菜单和下方图片预览](docs/screenshots/grid-favorites.png)
35
+ ![大图标视图中的图片和视频缩略图、收藏夹菜单和下方视频预览](docs/screenshots/grid-favorites.png)
36
36
 
37
37
  **工作区搜索**
38
38
 
@@ -44,7 +44,7 @@ https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.1/dsh-wo
44
44
 
45
45
  在顶部标签栏打开和切换多个文件夹,可拖动标签调整顺序。每个标签页各自保留目录、导航历史和预览。
46
46
 
47
- ![顶部标签栏同时打开了三个文件夹,当前选中 preview-test](docs/screenshots/stl-preview.png)
47
+ ![顶部同时打开三个文件夹标签,当前显示项目归档的文件列表和文本预览](docs/screenshots/stl-preview.png)
48
48
 
49
49
  ## 找到文件,保留浏览位置
50
50
 
@@ -71,7 +71,7 @@ https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.1/dsh-wo
71
71
  | 图片 | PNG、JPG / JPEG、GIF、WebP、BMP、ICO、AVIF | 显示图片 |
72
72
  | 视频 | MP4、WebM、M4V、MOV、MKV 等 | 尝试显示首帧,点击播放。取决于 DSH 内置浏览器能否解码 |
73
73
  | 音频 | MP3、WAV、OGG、OGA、M4A、AAC、FLAC、Opus | 播放音频,同样取决于解码支持 |
74
- | 纯文本与代码 | UTF-8 编码的 TXT、MD、JSON、CSV、HTML、脚本等 | 显示原始文本,最多读取前 2 MB |
74
+ | 纯文本与代码 | TXT、MD、JSON、CSV、HTML、PY 等 | 默认按 UTF-8 显示,Python 脚本也可按文件头声明的编码预览。最多读取前 2 MB |
75
75
  | 字幕 | SRT、VTT、ASS、SSA | 查看字幕时间和正文,也可切换到原始文本 |
76
76
  | 字体 | TTF、OTF、WOFF、WOFF2 | 查看字体样张 |
77
77
  | 3D 模型 | STL、OBJ、PLY、GLB、glTF | 旋转、缩放、平移模型,可切换线框。glTF 须内嵌模型数据 |
@@ -86,7 +86,7 @@ https://github.com/tm5452/dsh-workspace-explorer/releases/download/v1.1.1/dsh-wo
86
86
 
87
87
  ## 在会话与文件之间切换
88
88
 
89
- **打开会话里的文件。** 点击 AI 交付的文件卡片或回复中的本地文件链接,当前文件浏览页会跳到所在文件夹、选中文件并显示预览。把卡片或链接拖到右侧标出的区域,可在新标签页预览。预览栏右上角仍可使用 DSH 原生文档预览。
89
+ **打开会话里的文件。** 点击 AI 交付的文件、改动卡片中的具体文件或回复里的本地文件链接,当前文件浏览页会跳到所在目录、选中文件并展开预览。拖到右侧标出的区域后,交付文件和文件链接打开 DSH 文档预览,改动卡片打开该文件的 DSH 差异审阅;按住 Shift 点击改动文件也可直接查看原生红绿对比。
90
90
 
91
91
  **拖入文件就是添加附件。** 从列表或大图标视图把文件拖到左侧输入框,文件会加入当前会话的草稿,发送前仍可检查。遇到 DSH 无法接收的文件,可右键「复制文件路径」,再把路径粘贴到对话里。
92
92
 
Binary file
Binary file
package/lib/client.js CHANGED
@@ -11659,22 +11659,21 @@ window.__ModuleLoader__.load({
11659
11659
  __modules["client/conversation-drop.js"] = function (module, exports, require) {
11660
11660
  "use strict";
11661
11661
  const __exports = exports;
11662
- const sourceSelector = '[data-presented-file] > button[title], [data-chat-flow] button[class*="_fileMention_"]';
11662
+ const changedFileButton = '[data-changed-files] button[aria-describedby]';
11663
+ const sourceSelector = '[data-presented-file] > button[title], [data-chat-flow] button[class*="_fileMention_"], ' + changedFileButton;
11663
11664
  const dragThreshold = 5;
11664
11665
 
11665
- function installConversationDrop(mounted, readFile, openTab) {
11666
+ function installConversationDrop(mounted, readFile, openPreview) {
11666
11667
  const lifetime = new AbortController();
11667
11668
  const overlay = document.createElement('div');
11668
11669
  overlay.className = 'dwe-conversation-drop';
11669
11670
  overlay.setAttribute('aria-hidden', 'true');
11670
11671
  const area = document.createElement('div');
11671
11672
  area.className = 'dwe-conversation-drop-area';
11672
- area.textContent = '拖动到这里以新标签页打开';
11673
11673
  const icon = document.createElement('div');
11674
11674
  icon.className = 'dwe-conversation-drag-icon';
11675
11675
  const iconRoot = reactDomClient.createRoot(icon);
11676
11676
  overlay.append(area, icon);
11677
- let session = new AbortController();
11678
11677
  let gesture;
11679
11678
  let suppressClick = false;
11680
11679
 
@@ -11735,6 +11734,7 @@ window.__ModuleLoader__.load({
11735
11734
  // Keep tracking even when the chat rerenders the file card during the drag.
11736
11735
  document.body.setPointerCapture(event.pointerId);
11737
11736
  layout();
11737
+ area.textContent = file.review ? '拖到此处在DSH差异审阅中打开' : '拖到此处在DSH文档预览中打开';
11738
11738
  document.body.append(overlay);
11739
11739
  iconRoot.render(react.createElement(primitives.FileTypeIcon, { path: file.file, size: 28 }));
11740
11740
  }
@@ -11752,7 +11752,7 @@ window.__ModuleLoader__.load({
11752
11752
  if (!shouldOpen) return;
11753
11753
  const hit = document.elementFromPoint(event.clientX, event.clientY);
11754
11754
  const paneId = hit?.closest('[data-sidebar-right-panel] [data-dockkit-pane]')?.dataset.dockkitPane;
11755
- openTab(file, paneId, session.signal);
11755
+ openPreview(file, paneId);
11756
11756
  }
11757
11757
 
11758
11758
  function cancelPointer(event) {
@@ -11773,14 +11773,8 @@ window.__ModuleLoader__.load({
11773
11773
  event.stopImmediatePropagation();
11774
11774
  }
11775
11775
 
11776
- function switchSession() {
11777
- reset();
11778
- session.abort();
11779
- session = new AbortController();
11780
- }
11781
-
11782
11776
  const options = { capture: true, signal: lifetime.signal };
11783
- const stop = mounted.subscribe(switchSession);
11777
+ const stop = mounted.subscribe(reset);
11784
11778
  document.addEventListener('pointerdown', arm, options);
11785
11779
  document.addEventListener('pointermove', move, options);
11786
11780
  document.addEventListener('pointerup', release, options);
@@ -11797,11 +11791,11 @@ window.__ModuleLoader__.load({
11797
11791
  reset();
11798
11792
  iconRoot.unmount();
11799
11793
  stop();
11800
- session.abort();
11801
11794
  lifetime.abort();
11802
11795
  };
11803
11796
  }
11804
11797
 
11798
+ __exports["changedFileButton"] = changedFileButton;
11805
11799
  __exports["installConversationDrop"] = installConversationDrop;
11806
11800
  };
11807
11801
 
@@ -11811,15 +11805,16 @@ window.__ModuleLoader__.load({
11811
11805
  const { Explorer, ExplorerTitle } = __require("ui/explorer.js");
11812
11806
  const { explorerCss } = __require("ui/styles.js");
11813
11807
  const { scrollHorizontally } = __require("ui/horizontal-wheel.js");
11814
- const { parentOf, sessionFilePath, underPath, windowsPath } = __require("ui/navigation.js");
11808
+ const { parentOf, resourceAddress, sessionFilePath, underPath, windowsPath } = __require("ui/navigation.js");
11815
11809
  const { renameFavorites } = __require("ui/favorites.js");
11816
11810
  const { installSidebarMotion } = __require("client/sidebar-motion.js");
11817
- const { installConversationDrop } = __require("client/conversation-drop.js");
11811
+ const { changedFileButton, installConversationDrop } = __require("client/conversation-drop.js");
11818
11812
 
11819
11813
  const id = 'dsh-workspace-explorer';
11820
11814
  const inject = ['slots'];
11821
11815
  const h = react.createElement;
11822
11816
  const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
11817
+ const changesReviewPrefix = 'dsh-resource://changes-review/session/';
11823
11818
 
11824
11819
  function apply(ctx) {
11825
11820
  if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
@@ -12063,21 +12058,22 @@ window.__ModuleLoader__.load({
12063
12058
  return bindings.get(sidebar.active()?.id) ?? activeView ?? viewList[0];
12064
12059
  }
12065
12060
 
12066
- async function openConversationFile(request, signal, { view, newTab, paneId }) {
12061
+ async function openConversationFile(request, signal, { view, onUnavailable }) {
12067
12062
  const { address, sessionId, file, params } = request;
12068
12063
  const metadata = await statFile(sessionId, file, signal);
12069
12064
  if (signal.aborted || sidebar.mounted.getSnapshot() !== sessionId) return;
12070
12065
  if (!metadata) {
12071
- openDocument(address, { params, paneId });
12066
+ if (onUnavailable) onUnavailable();
12067
+ else openDocument(address, { params });
12072
12068
  return;
12073
12069
  }
12074
- const existing = newTab ? undefined : viewList.includes(view) ? view : currentExplorer();
12070
+ const existing = viewList.includes(view) ? view : currentExplorer();
12075
12071
  const tabId = existing && [...bindings].find(([, item]) => item === existing)?.[0];
12076
12072
  if (tabId) {
12077
12073
  sidebar.focus(tabId);
12078
12074
  if (!sidebar.isExpanded() && !existing.target?.isFloating) sidebar.toggleExpanded();
12079
12075
  }
12080
- const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute), paneId);
12076
+ const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute));
12081
12077
  if (target) revealFile(target, metadata, params?.line);
12082
12078
  }
12083
12079
 
@@ -12085,31 +12081,72 @@ window.__ModuleLoader__.load({
12085
12081
  const openResource = sidebar.openResource;
12086
12082
  let fileClick;
12087
12083
  let dragSource;
12084
+ let openingReview = false;
12085
+ function openFile(request, onUnavailable) {
12086
+ fileClick?.abort();
12087
+ fileClick = new AbortController();
12088
+ void openConversationFile(request, fileClick.signal, { view: currentExplorer(), onUnavailable });
12089
+ }
12088
12090
  function open(address, options) {
12091
+ const changed = dragSource?.changed;
12092
+ if (changed && address.startsWith(changesReviewPrefix)) {
12093
+ if (address.startsWith(changesReviewPrefix + encodeURIComponent(changed.sessionId) + '/')) {
12094
+ dragSource.file = { ...changed, review: { address, params: options?.params } };
12095
+ }
12096
+ return;
12097
+ }
12089
12098
  const request = (!options || Object.keys(options).every(key => key === 'params')) && fileRequest(address, options);
12090
12099
  if (!request) return openResource.call(this, address, options);
12091
12100
  if (dragSource) {
12092
12101
  dragSource.file = request;
12093
12102
  return;
12094
12103
  }
12095
- fileClick?.abort();
12096
- fileClick = new AbortController();
12097
- void openConversationFile(request, fileClick.signal, { view: currentExplorer() });
12104
+ openFile(request);
12105
+ }
12106
+ function changedFileRequest(button) {
12107
+ const card = button.closest('[data-changed-files]');
12108
+ const description = document.getElementById(button.getAttribute('aria-describedby'));
12109
+ const sessionId = card?.closest('[data-conversation-content]')?.dataset.conversationSession;
12110
+ if (sessionId !== sidebar.mounted.getSnapshot() || !card?.contains(description)) return;
12111
+ const absolute = windowsPath(description.textContent);
12112
+ if (absolute) return fileRequest(resourceAddress(absolute, sessionId));
12113
+ }
12114
+ function clickChangedFile(event) {
12115
+ // Shift-click retains the native change review for an individual file.
12116
+ if (openingReview || dragSource?.changed || event.shiftKey || !(event.target instanceof Element)) return;
12117
+ const button = event.target.closest(changedFileButton);
12118
+ const request = button && changedFileRequest(button);
12119
+ if (!request) return;
12120
+ event.preventDefault();
12121
+ event.stopPropagation();
12122
+ openFile(request, () => {
12123
+ if (!button.isConnected) return;
12124
+ openingReview = true;
12125
+ try { button.click(); }
12126
+ finally { openingReview = false; }
12127
+ });
12098
12128
  }
12099
12129
  function readDragSource(button) {
12100
- // Let Harness resolve the same path and line as a real click, without opening a tab yet.
12101
- const capture = {};
12130
+ const isChanged = button.matches(changedFileButton);
12131
+ const changed = isChanged ? changedFileRequest(button) : undefined;
12132
+ if (isChanged && !changed) return;
12133
+ // Capture Harness's file or review address without opening the tab until drop.
12134
+ const capture = { changed };
12102
12135
  dragSource = capture;
12103
12136
  try { button.click(); }
12104
12137
  finally { dragSource = undefined; }
12105
- return capture.file;
12138
+ return capture.file ?? changed;
12106
12139
  }
12107
12140
  sidebar.openResource = open;
12108
- const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, (request, paneId, signal) => {
12109
- void openConversationFile(request, signal, { newTab: true, paneId });
12141
+ const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, ({ address, sessionId, params, review }, paneId) => {
12142
+ if (sidebar.mounted.getSnapshot() !== sessionId) return;
12143
+ if (review) sidebar.openResource(review.address, { paneId, params: review.params });
12144
+ else openDocument(address, { paneId, params });
12110
12145
  });
12146
+ document.addEventListener('click', clickChangedFile, true);
12111
12147
  const stop = sidebar.mounted.subscribe(() => fileClick?.abort());
12112
12148
  return () => {
12149
+ document.removeEventListener('click', clickChangedFile, true);
12113
12150
  stopDrop();
12114
12151
  stop();
12115
12152
  fileClick?.abort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workspace-explorer",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Windows disk browsing, file operations and persistent preview in DeepSeek Harness Desktop.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,19 +1,18 @@
1
- const sourceSelector = '[data-presented-file] > button[title], [data-chat-flow] button[class*="_fileMention_"]';
1
+ export const changedFileButton = '[data-changed-files] button[aria-describedby]';
2
+ const sourceSelector = '[data-presented-file] > button[title], [data-chat-flow] button[class*="_fileMention_"], ' + changedFileButton;
2
3
  const dragThreshold = 5;
3
4
 
4
- export function installConversationDrop(mounted, readFile, openTab) {
5
+ export function installConversationDrop(mounted, readFile, openPreview) {
5
6
  const lifetime = new AbortController();
6
7
  const overlay = document.createElement('div');
7
8
  overlay.className = 'dwe-conversation-drop';
8
9
  overlay.setAttribute('aria-hidden', 'true');
9
10
  const area = document.createElement('div');
10
11
  area.className = 'dwe-conversation-drop-area';
11
- area.textContent = '拖动到这里以新标签页打开';
12
12
  const icon = document.createElement('div');
13
13
  icon.className = 'dwe-conversation-drag-icon';
14
14
  const iconRoot = reactDomClient.createRoot(icon);
15
15
  overlay.append(area, icon);
16
- let session = new AbortController();
17
16
  let gesture;
18
17
  let suppressClick = false;
19
18
 
@@ -74,6 +73,7 @@ export function installConversationDrop(mounted, readFile, openTab) {
74
73
  // Keep tracking even when the chat rerenders the file card during the drag.
75
74
  document.body.setPointerCapture(event.pointerId);
76
75
  layout();
76
+ area.textContent = file.review ? '拖到此处在DSH差异审阅中打开' : '拖到此处在DSH文档预览中打开';
77
77
  document.body.append(overlay);
78
78
  iconRoot.render(react.createElement(primitives.FileTypeIcon, { path: file.file, size: 28 }));
79
79
  }
@@ -91,7 +91,7 @@ export function installConversationDrop(mounted, readFile, openTab) {
91
91
  if (!shouldOpen) return;
92
92
  const hit = document.elementFromPoint(event.clientX, event.clientY);
93
93
  const paneId = hit?.closest('[data-sidebar-right-panel] [data-dockkit-pane]')?.dataset.dockkitPane;
94
- openTab(file, paneId, session.signal);
94
+ openPreview(file, paneId);
95
95
  }
96
96
 
97
97
  function cancelPointer(event) {
@@ -112,14 +112,8 @@ export function installConversationDrop(mounted, readFile, openTab) {
112
112
  event.stopImmediatePropagation();
113
113
  }
114
114
 
115
- function switchSession() {
116
- reset();
117
- session.abort();
118
- session = new AbortController();
119
- }
120
-
121
115
  const options = { capture: true, signal: lifetime.signal };
122
- const stop = mounted.subscribe(switchSession);
116
+ const stop = mounted.subscribe(reset);
123
117
  document.addEventListener('pointerdown', arm, options);
124
118
  document.addEventListener('pointermove', move, options);
125
119
  document.addEventListener('pointerup', release, options);
@@ -136,7 +130,6 @@ export function installConversationDrop(mounted, readFile, openTab) {
136
130
  reset();
137
131
  iconRoot.unmount();
138
132
  stop();
139
- session.abort();
140
133
  lifetime.abort();
141
134
  };
142
135
  }
@@ -1,15 +1,16 @@
1
1
  import { Explorer, ExplorerTitle } from '../ui/explorer.js';
2
2
  import { explorerCss } from '../ui/styles.js';
3
3
  import { scrollHorizontally } from '../ui/horizontal-wheel.js';
4
- import { parentOf, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
4
+ import { parentOf, resourceAddress, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
5
5
  import { renameFavorites } from '../ui/favorites.js';
6
6
  import { installSidebarMotion } from './sidebar-motion.js';
7
- import { installConversationDrop } from './conversation-drop.js';
7
+ import { changedFileButton, installConversationDrop } from './conversation-drop.js';
8
8
 
9
9
  const id = 'dsh-workspace-explorer';
10
10
  const inject = ['slots'];
11
11
  const h = react.createElement;
12
12
  const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
13
+ const changesReviewPrefix = 'dsh-resource://changes-review/session/';
13
14
 
14
15
  function apply(ctx) {
15
16
  if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
@@ -253,21 +254,22 @@ function apply(ctx) {
253
254
  return bindings.get(sidebar.active()?.id) ?? activeView ?? viewList[0];
254
255
  }
255
256
 
256
- async function openConversationFile(request, signal, { view, newTab, paneId }) {
257
+ async function openConversationFile(request, signal, { view, onUnavailable }) {
257
258
  const { address, sessionId, file, params } = request;
258
259
  const metadata = await statFile(sessionId, file, signal);
259
260
  if (signal.aborted || sidebar.mounted.getSnapshot() !== sessionId) return;
260
261
  if (!metadata) {
261
- openDocument(address, { params, paneId });
262
+ if (onUnavailable) onUnavailable();
263
+ else openDocument(address, { params });
262
264
  return;
263
265
  }
264
- const existing = newTab ? undefined : viewList.includes(view) ? view : currentExplorer();
266
+ const existing = viewList.includes(view) ? view : currentExplorer();
265
267
  const tabId = existing && [...bindings].find(([, item]) => item === existing)?.[0];
266
268
  if (tabId) {
267
269
  sidebar.focus(tabId);
268
270
  if (!sidebar.isExpanded() && !existing.target?.isFloating) sidebar.toggleExpanded();
269
271
  }
270
- const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute), paneId);
272
+ const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute));
271
273
  if (target) revealFile(target, metadata, params?.line);
272
274
  }
273
275
 
@@ -275,31 +277,72 @@ function apply(ctx) {
275
277
  const openResource = sidebar.openResource;
276
278
  let fileClick;
277
279
  let dragSource;
280
+ let openingReview = false;
281
+ function openFile(request, onUnavailable) {
282
+ fileClick?.abort();
283
+ fileClick = new AbortController();
284
+ void openConversationFile(request, fileClick.signal, { view: currentExplorer(), onUnavailable });
285
+ }
278
286
  function open(address, options) {
287
+ const changed = dragSource?.changed;
288
+ if (changed && address.startsWith(changesReviewPrefix)) {
289
+ if (address.startsWith(changesReviewPrefix + encodeURIComponent(changed.sessionId) + '/')) {
290
+ dragSource.file = { ...changed, review: { address, params: options?.params } };
291
+ }
292
+ return;
293
+ }
279
294
  const request = (!options || Object.keys(options).every(key => key === 'params')) && fileRequest(address, options);
280
295
  if (!request) return openResource.call(this, address, options);
281
296
  if (dragSource) {
282
297
  dragSource.file = request;
283
298
  return;
284
299
  }
285
- fileClick?.abort();
286
- fileClick = new AbortController();
287
- void openConversationFile(request, fileClick.signal, { view: currentExplorer() });
300
+ openFile(request);
301
+ }
302
+ function changedFileRequest(button) {
303
+ const card = button.closest('[data-changed-files]');
304
+ const description = document.getElementById(button.getAttribute('aria-describedby'));
305
+ const sessionId = card?.closest('[data-conversation-content]')?.dataset.conversationSession;
306
+ if (sessionId !== sidebar.mounted.getSnapshot() || !card?.contains(description)) return;
307
+ const absolute = windowsPath(description.textContent);
308
+ if (absolute) return fileRequest(resourceAddress(absolute, sessionId));
309
+ }
310
+ function clickChangedFile(event) {
311
+ // Shift-click retains the native change review for an individual file.
312
+ if (openingReview || dragSource?.changed || event.shiftKey || !(event.target instanceof Element)) return;
313
+ const button = event.target.closest(changedFileButton);
314
+ const request = button && changedFileRequest(button);
315
+ if (!request) return;
316
+ event.preventDefault();
317
+ event.stopPropagation();
318
+ openFile(request, () => {
319
+ if (!button.isConnected) return;
320
+ openingReview = true;
321
+ try { button.click(); }
322
+ finally { openingReview = false; }
323
+ });
288
324
  }
289
325
  function readDragSource(button) {
290
- // Let Harness resolve the same path and line as a real click, without opening a tab yet.
291
- const capture = {};
326
+ const isChanged = button.matches(changedFileButton);
327
+ const changed = isChanged ? changedFileRequest(button) : undefined;
328
+ if (isChanged && !changed) return;
329
+ // Capture Harness's file or review address without opening the tab until drop.
330
+ const capture = { changed };
292
331
  dragSource = capture;
293
332
  try { button.click(); }
294
333
  finally { dragSource = undefined; }
295
- return capture.file;
334
+ return capture.file ?? changed;
296
335
  }
297
336
  sidebar.openResource = open;
298
- const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, (request, paneId, signal) => {
299
- void openConversationFile(request, signal, { newTab: true, paneId });
337
+ const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, ({ address, sessionId, params, review }, paneId) => {
338
+ if (sidebar.mounted.getSnapshot() !== sessionId) return;
339
+ if (review) sidebar.openResource(review.address, { paneId, params: review.params });
340
+ else openDocument(address, { paneId, params });
300
341
  });
342
+ document.addEventListener('click', clickChangedFile, true);
301
343
  const stop = sidebar.mounted.subscribe(() => fileClick?.abort());
302
344
  return () => {
345
+ document.removeEventListener('click', clickChangedFile, true);
303
346
  stopDrop();
304
347
  stop();
305
348
  fileClick?.abort();
@@ -1,69 +1,69 @@
1
- const dockItems = ':is([data-dockkit-host="dock"], [data-dockkit-empty], [data-dockkit-divider])';
2
- const toggleButtons = 'button:is([data-sidebar-right-expand], [data-sidebar-right-toggle])';
3
-
4
- export function installSidebarMotion(sidebar) {
5
- let allowed;
6
- let collectFrame;
7
- const instantFrames = new Map();
8
-
9
- function stopMotion() {
10
- cancelAnimationFrame(collectFrame);
11
- if (!allowed) return;
12
- for (const item of allowed.items) delete item.dataset.dweSidebarToggleMotion;
13
- allowed = undefined;
14
- }
15
-
16
- function allowButtonMotion(event) {
17
- const button = event.target instanceof Element && event.target.closest(toggleButtons);
18
- if (!button || button.disabled) return;
19
- const sessionId = sidebar.mounted.getSnapshot();
20
- const panel = sessionId && document.querySelector(
21
- '[data-sidebar-right-panel][data-sidebar-right-session="' + CSS.escape(sessionId) + '"]',
22
- );
23
- const frame = panel?.closest('[data-rightbar-col]')?.parentElement;
24
- if (!frame) return;
25
-
26
- // A tab selected during the slide must not inherit permission to animate.
27
- stopMotion();
28
- const items = [...panel.querySelectorAll(dockItems + ':not([hidden])')];
29
- const motion = { frame, items };
30
- allowed = motion;
31
- for (const item of items) item.dataset.dweSidebarToggleMotion = '';
32
- collectFrame = requestAnimationFrame(() => {
33
- const transitions = [...frame.getAnimations(), ...items.flatMap(item => item.getAnimations())]
34
- .filter(animation => ['transform', 'grid-template-columns'].includes(animation.transitionProperty));
35
- Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
36
- if (allowed === motion) stopMotion();
37
- });
38
- });
39
- }
40
-
41
- // The shell shares one grid transition between the left and right columns.
42
- // Snap right-track changes outside the toggle controls so the left controls still animate.
43
- const track = new MutationObserver(records => {
44
- for (const frame of new Set(records.map(record => record.target))) {
45
- if (allowed?.frame === frame || !frame.querySelector(':scope > [data-rightbar-col]')) continue;
46
- cancelAnimationFrame(instantFrames.get(frame));
47
- frame.dataset.dweSidebarTrackInstant = '';
48
- instantFrames.set(frame, requestAnimationFrame(() => {
49
- instantFrames.set(frame, requestAnimationFrame(() => {
50
- delete frame.dataset.dweSidebarTrackInstant;
51
- instantFrames.delete(frame);
52
- }));
53
- }));
54
- }
55
- });
56
- track.observe(document.documentElement, { subtree: true, attributeFilter: ['data-rightbar-collapsed'] });
57
- document.addEventListener('click', allowButtonMotion, true);
58
- const disposeSwitch = sidebar.mounted.subscribe(stopMotion);
59
- return () => {
60
- stopMotion();
61
- disposeSwitch();
62
- track.disconnect();
63
- for (const [frame, request] of instantFrames) {
64
- cancelAnimationFrame(request);
65
- delete frame.dataset.dweSidebarTrackInstant;
66
- }
67
- document.removeEventListener('click', allowButtonMotion, true);
68
- };
69
- }
1
+ const dockItems = ':is([data-dockkit-host="dock"], [data-dockkit-empty], [data-dockkit-divider])';
2
+ const toggleButtons = 'button:is([data-sidebar-right-expand], [data-sidebar-right-toggle])';
3
+
4
+ export function installSidebarMotion(sidebar) {
5
+ let allowed;
6
+ let collectFrame;
7
+ const instantFrames = new Map();
8
+
9
+ function stopMotion() {
10
+ cancelAnimationFrame(collectFrame);
11
+ if (!allowed) return;
12
+ for (const item of allowed.items) delete item.dataset.dweSidebarToggleMotion;
13
+ allowed = undefined;
14
+ }
15
+
16
+ function allowButtonMotion(event) {
17
+ const button = event.target instanceof Element && event.target.closest(toggleButtons);
18
+ if (!button || button.disabled) return;
19
+ const sessionId = sidebar.mounted.getSnapshot();
20
+ const panel = sessionId && document.querySelector(
21
+ '[data-sidebar-right-panel][data-sidebar-right-session="' + CSS.escape(sessionId) + '"]',
22
+ );
23
+ const frame = panel?.closest('[data-rightbar-col]')?.parentElement;
24
+ if (!frame) return;
25
+
26
+ // A tab selected during the slide must not inherit permission to animate.
27
+ stopMotion();
28
+ const items = [...panel.querySelectorAll(dockItems + ':not([hidden])')];
29
+ const motion = { frame, items };
30
+ allowed = motion;
31
+ for (const item of items) item.dataset.dweSidebarToggleMotion = '';
32
+ collectFrame = requestAnimationFrame(() => {
33
+ const transitions = [...frame.getAnimations(), ...items.flatMap(item => item.getAnimations())]
34
+ .filter(animation => ['transform', 'grid-template-columns'].includes(animation.transitionProperty));
35
+ Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
36
+ if (allowed === motion) stopMotion();
37
+ });
38
+ });
39
+ }
40
+
41
+ // The shell shares one grid transition between the left and right columns.
42
+ // Snap right-track changes outside the toggle controls so the left controls still animate.
43
+ const track = new MutationObserver(records => {
44
+ for (const frame of new Set(records.map(record => record.target))) {
45
+ if (allowed?.frame === frame || !frame.querySelector(':scope > [data-rightbar-col]')) continue;
46
+ cancelAnimationFrame(instantFrames.get(frame));
47
+ frame.dataset.dweSidebarTrackInstant = '';
48
+ instantFrames.set(frame, requestAnimationFrame(() => {
49
+ instantFrames.set(frame, requestAnimationFrame(() => {
50
+ delete frame.dataset.dweSidebarTrackInstant;
51
+ instantFrames.delete(frame);
52
+ }));
53
+ }));
54
+ }
55
+ });
56
+ track.observe(document.documentElement, { subtree: true, attributeFilter: ['data-rightbar-collapsed'] });
57
+ document.addEventListener('click', allowButtonMotion, true);
58
+ const disposeSwitch = sidebar.mounted.subscribe(stopMotion);
59
+ return () => {
60
+ stopMotion();
61
+ disposeSwitch();
62
+ track.disconnect();
63
+ for (const [frame, request] of instantFrames) {
64
+ cancelAnimationFrame(request);
65
+ delete frame.dataset.dweSidebarTrackInstant;
66
+ }
67
+ document.removeEventListener('click', allowButtonMotion, true);
68
+ };
69
+ }
@@ -1,2 +1,2 @@
1
- export const officeExtensions = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
2
- export const pdfAssetsPath = '/api/dsh.workspaceExplorer/pdf';
1
+ export const officeExtensions = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
2
+ export const pdfAssetsPath = '/api/dsh.workspaceExplorer/pdf';