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 +7 -0
- package/README.md +6 -6
- package/docs/screenshots/grid-favorites.png +0 -0
- package/docs/screenshots/stl-preview.png +0 -0
- package/docs/screenshots/workspace-search.png +0 -0
- package/lib/client.js +64 -27
- package/package.json +1 -1
- package/src/client/conversation-drop.js +6 -13
- package/src/client/main.js +57 -14
- package/src/client/sidebar-motion.js +69 -69
- package/src/document-types.js +2 -2
- package/src/host/ExplorerShell.cs +183 -183
- package/src/host/files.js +24 -5
- package/src/host/pdf-assets.js +30 -30
- package/src/host/shell-files.js +81 -81
- package/src/thumbnail-types.js +18 -18
- package/src/ui/composer-drop.js +119 -119
- package/src/ui/favorites.js +176 -176
- package/src/ui/file-menu.js +125 -125
- package/src/ui/horizontal-wheel.js +11 -11
- package/src/ui/pdf-preview.js +135 -135
- package/src/ui/preview-unavailable.js +6 -6
- package/src/ui/preview-zoom.js +47 -47
- package/src/ui/thumbnail.js +105 -105
- package/src/vendor/ui/extra-views.js +1 -1
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.
|
|
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
|
-

|
|
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
|
-

|
|
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
|
-
| 纯文本与代码 |
|
|
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
|
|
89
|
+
**打开会话里的文件。** 点击 AI 交付的文件、改动卡片中的具体文件或回复里的本地文件链接,当前文件浏览页会跳到所在目录、选中文件并展开预览。拖到右侧标出的区域后,交付文件和文件链接打开 DSH 文档预览,改动卡片打开该文件的 DSH 差异审阅;按住 Shift 点击改动文件也可直接查看原生红绿对比。
|
|
90
90
|
|
|
91
91
|
**拖入文件就是添加附件。** 从列表或大图标视图把文件拖到左侧输入框,文件会加入当前会话的草稿,发送前仍可检查。遇到 DSH 无法接收的文件,可右键「复制文件路径」,再把路径粘贴到对话里。
|
|
92
92
|
|
|
Binary file
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
12066
|
+
if (onUnavailable) onUnavailable();
|
|
12067
|
+
else openDocument(address, { params });
|
|
12072
12068
|
return;
|
|
12073
12069
|
}
|
|
12074
|
-
const existing =
|
|
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)
|
|
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
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
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
|
-
|
|
12101
|
-
const
|
|
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, (
|
|
12109
|
-
|
|
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,19 +1,18 @@
|
|
|
1
|
-
const
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/src/client/main.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
262
|
+
if (onUnavailable) onUnavailable();
|
|
263
|
+
else openDocument(address, { params });
|
|
262
264
|
return;
|
|
263
265
|
}
|
|
264
|
-
const existing =
|
|
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)
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
const
|
|
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, (
|
|
299
|
-
|
|
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
|
+
}
|
package/src/document-types.js
CHANGED
|
@@ -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';
|