dsh-skip-preview 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +137 -2
- package/cordis.patch.yml +6 -0
- package/lib/client.js +273 -0
- package/lib/index.js +15 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-skip-preview contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-skip-preview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](#english) | 中文
|
|
4
|
+
|
|
5
|
+
在 DSH 对话里按住 **⌘**(Windows 为 **Ctrl**)点击文件链接,**跳过右侧预览,直接用系统默认应用打开**。
|
|
6
|
+
|
|
7
|
+
普通点击不变,仍然是右侧 Sidebar 预览。
|
|
8
|
+
|
|
9
|
+
## 解决什么问题
|
|
10
|
+
|
|
11
|
+
DSH 里对话正文提到的文件,点击后一律进右侧侧边栏预览。这在很多情况下不是想要的:
|
|
12
|
+
|
|
13
|
+
- **侧栏太窄。** 右侧面板宽度基本不可调,读 Markdown、PDF 或表格时排版被压扁。
|
|
14
|
+
- **想改文件。** 你本来就打算在编辑器里改它,多一次「先进预览、再点打开」是无谓的一步。
|
|
15
|
+
- **系统程序更合适。** `.md` 用你的编辑器、`.xlsx` 用 Excel / WPS、`.pdf` 用预览器——DSH 的内置预览只是其中一个选项。
|
|
16
|
+
|
|
17
|
+
`dsh-skip-preview` 给预览加一条**并行出口**,而不是替换预览:想看就看,想开就按 ⌘。
|
|
18
|
+
|
|
19
|
+
## 用法
|
|
20
|
+
|
|
21
|
+
| 操作 | macOS | Windows / Linux |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| 左键点击文件链接 | 预览(DSH 原有行为) | 预览(DSH 原有行为) |
|
|
24
|
+
| **⌘ + 左键** | **用系统默认应用打开** | —(无 ⌘ 键) |
|
|
25
|
+
| **Ctrl + 左键** | 预览(macOS 上 Ctrl+点击是系统右键) | **用系统默认应用打开** |
|
|
26
|
+
| 带 Shift / Alt / 另一个修饰键 | 一律预览 | 一律预览 |
|
|
27
|
+
| 打开失败时 | 自动退回预览,Console 记录原因 | 同左 |
|
|
28
|
+
|
|
29
|
+
触发键**只认一个**,按平台判定:macOS 认 `metaKey`(⌘),Windows/Linux 认 `ctrlKey`。
|
|
30
|
+
除这一个组合外,插件**不接管任何点击**,全部原样交回 DSH 和浏览器。
|
|
31
|
+
|
|
32
|
+
> 具体的应用由系统文件关联决定。`.md` 用哪个程序打开,取决于你在系统里设置的默认程序,
|
|
33
|
+
> 本插件不指定、也不修改它。
|
|
34
|
+
|
|
35
|
+
## 覆盖范围
|
|
36
|
+
|
|
37
|
+
包装的是 `sidebarRight.openResource`——会话正文里的文件进入右侧面板的唯一入口,因此
|
|
38
|
+
**Markdown 链接、行内代码路径、右侧文件树**都生效。
|
|
39
|
+
|
|
40
|
+
**不覆盖**:`present` 出来的交付卡片,它走 `ui-deliverables` 自己的路由。
|
|
41
|
+
|
|
42
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
dsh plugin --profile desktop add dsh-skip-preview
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
装完**完全退出并重启 DSH**。刷新页面不足以重载客户端 bundle。
|
|
49
|
+
|
|
50
|
+
从 GitHub 源码安装:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
dsh plugin --profile desktop add github:<你的用户名>/dsh-skip-preview
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 兼容性
|
|
57
|
+
|
|
58
|
+
- 需要 DSH `>= 0.2.1-rc.5`(`package.json` 的 `dsh.engines.dsh` 已声明)。
|
|
59
|
+
- 启动时做**自检**:若 DSH 改了 `sidebarRight.openResource` 等方法名,Console 会打印
|
|
60
|
+
`[dsh-skip-preview] 启动自检失败` 并说明受影响的行为,而不是静默失效。
|
|
61
|
+
|
|
62
|
+
## 实现要点
|
|
63
|
+
|
|
64
|
+
- **在服务层包装,不碰 DOM。** 正文文件链接是 `<button class*="fileLink">`,行内代码路径是
|
|
65
|
+
`<code><button title=…>`,两套形状;而它们最终都经过 `openResource`,包一处覆盖全部入口,
|
|
66
|
+
也不受 DSH 改样式类名影响。
|
|
67
|
+
- **修饰键取自捕获阶段。** `openResource(address)` 只收到地址、拿不到事件,所以在
|
|
68
|
+
`pointerdown` / `click` 的捕获阶段记录真实按键(早于 React 根容器的冒泡监听)。
|
|
69
|
+
- **失败回退预览。** 宿主拒绝时(路径不在工作区、文件不存在)会 throw 或回 `ok:false`,
|
|
70
|
+
两种都退回预览,避免「点了没反应」。
|
|
71
|
+
- **幂等 + 可还原。** 重复 apply 不会重复包装;卸载时只在仍是自己那层包装时才还原,
|
|
72
|
+
不踩别人后来的包装。
|
|
73
|
+
|
|
74
|
+
## 已知限制
|
|
75
|
+
|
|
76
|
+
- **依赖 `sidebarRight.openResource` 这个方法名。** 它是 DSH 的公开服务面,但方法签名不是
|
|
77
|
+
稳定契约。已有启动自检兜底,但仍可能因 DSH 升级而失效。
|
|
78
|
+
- **手势快照是全局单例。** 任意一次点击都会覆盖它。1 秒 TTL 把影响压得很小,但没有根除。
|
|
79
|
+
- **触发键写死在代码里。** 没有设置项;要换键得改 `TRIGGER_IS_META` 附近几行。
|
|
80
|
+
- **可发现性为零。** 界面没有任何提示,纯靠记忆。
|
|
81
|
+
- **Windows/Linux 未实测。** `ctrlKey` 分支有测试覆盖,但没在真实 Windows 环境跑过。
|
|
82
|
+
- **不支持交付卡片。** 见「覆盖范围」。
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
<a id="english"></a>
|
|
91
|
+
## English
|
|
92
|
+
|
|
93
|
+
Hold **⌘** (**Ctrl** on Windows) and click a file link in a DSH conversation to **skip the
|
|
94
|
+
right sidebar preview and open the file with your system default application**. A plain click
|
|
95
|
+
still previews, exactly as before.
|
|
96
|
+
|
|
97
|
+
### Why
|
|
98
|
+
|
|
99
|
+
DSH opens every file mentioned in chat in the right sidebar. That is often not what you want:
|
|
100
|
+
the sidebar is narrow and hard to read long Markdown, and if you were going to edit the file
|
|
101
|
+
anyway, the preview is a wasted step. This plugin adds a **parallel exit** rather than
|
|
102
|
+
replacing the preview.
|
|
103
|
+
|
|
104
|
+
### Behaviour
|
|
105
|
+
|
|
106
|
+
| Action | Result |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Plain click | Sidebar preview (unchanged DSH behaviour) |
|
|
109
|
+
| **⌘ + click** (macOS) / **Ctrl + click** (Windows/Linux) | Opens with the system default app |
|
|
110
|
+
| Shift / Alt / the other modifier | Always previews — never intercepted |
|
|
111
|
+
| Host rejects the open | Falls back to preview, logs the reason |
|
|
112
|
+
|
|
113
|
+
Only that one combination is intercepted; every other click is passed through untouched.
|
|
114
|
+
Which application handles a file is decided by your OS file associations — the plugin does
|
|
115
|
+
not pick or change it.
|
|
116
|
+
|
|
117
|
+
### Scope
|
|
118
|
+
|
|
119
|
+
Wraps `sidebarRight.openResource`, the single entry through which conversation files reach
|
|
120
|
+
the right panel — so Markdown links, inline-code paths, and the file tree all work.
|
|
121
|
+
**Not** covered: `present` deliverable cards, which use `ui-deliverables`' own route.
|
|
122
|
+
|
|
123
|
+
### Install
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
dsh plugin --profile desktop add dsh-skip-preview
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Fully quit and restart DSH afterwards; a page refresh does not reload the client bundle.
|
|
130
|
+
|
|
131
|
+
### Compatibility
|
|
132
|
+
|
|
133
|
+
Requires DSH `>= 0.2.1-rc.5`. A startup self-check logs an explicit error if DSH renames the
|
|
134
|
+
wrapped service methods, instead of failing silently.
|
|
135
|
+
|
|
136
|
+
### License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-skip-preview — 客户端半区。
|
|
3
|
+
*
|
|
4
|
+
* 背景:会话正文里的文件链接(Markdown 的 `[a.md](a.md)`、行内代码路径)点击后
|
|
5
|
+
* 都汇到 `sidebarRight.openResource(dsh-resource://file/session/<id>/<path>)`,
|
|
6
|
+
* 一律在右侧 Sidebar 预览,没有修饰键分支。而「用系统默认应用打开」的通道宿主
|
|
7
|
+
* 早就有了:`session.openWorkspacePath({ path })`(文档预览标题栏的「打开」用的就是它)。
|
|
8
|
+
*
|
|
9
|
+
* 所以本插件在服务层包一层 `openResource`:按住 ⌘(Windows 为 Ctrl)点击时,把
|
|
10
|
+
* 会话文件地址解析成绝对路径,改走 `openWorkspacePath`,不打开预览;其余情况原样
|
|
11
|
+
* 交回原实现。
|
|
12
|
+
*
|
|
13
|
+
* 为什么用服务层而不是 DOM 选择器:正文文件链接是 `<button class="…fileLink">`,
|
|
14
|
+
* 行内代码路径是 `<code><button title=…>`,两套 DOM 形状;而它们最终都经过同一个
|
|
15
|
+
* `openResource`,包这一处就覆盖全部入口,也不怕 DSH 改样式类名。
|
|
16
|
+
*
|
|
17
|
+
* 修饰键状态取自 document 捕获阶段的真实事件 —— 它早于 React 在根容器上的冒泡
|
|
18
|
+
* 监听执行,所以 `openResource` 被调用时读到的一定是「这一下」的按键状态。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
window.__ModuleLoader__.load({
|
|
22
|
+
id: "dsh-skip-preview",
|
|
23
|
+
factory: (require) => {
|
|
24
|
+
var module = { exports: {} };
|
|
25
|
+
var exports = module.exports;
|
|
26
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
27
|
+
|
|
28
|
+
const NS = "dsh-skip-preview";
|
|
29
|
+
|
|
30
|
+
/** 需要的客户端服务:Remote 调用原生打开;sessions 与 sidebarRight 在 apply 内按需注入。 */
|
|
31
|
+
const inject = ["remote", "remote.session"];
|
|
32
|
+
|
|
33
|
+
/** `dsh-resource://file/` 地址前缀,与 ui-sidebar-right 的地址语法一致。 */
|
|
34
|
+
const FILE_PREFIX = "dsh-resource://file/";
|
|
35
|
+
|
|
36
|
+
/** 绝对路径的样子:POSIX 根、Windows 盘符或 UNC。 */
|
|
37
|
+
const ABSOLUTE_PATH = /^(?:[A-Za-z]:[\\/]|\\\\|\/)/;
|
|
38
|
+
|
|
39
|
+
/** Windows 风格 cwd 的判定,用来决定拼接分隔符。 */
|
|
40
|
+
const WINDOWS_PATH = /^[A-Za-z]:[\\/]|^\\\\/;
|
|
41
|
+
|
|
42
|
+
/** 手势快照的有效期:程序化触发的 openResource 不该被很久以前的按键影响。 */
|
|
43
|
+
const GESTURE_TTL_MS = 1000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 这台机器的触发键是不是 `event.metaKey`(macOS 的 ⌘)。
|
|
47
|
+
*
|
|
48
|
+
* 只认一个键,且按平台判定:macOS 上就是 ⌘,Windows/Linux 上没有 ⌘ 键,
|
|
49
|
+
* 对应的是 Ctrl。这样「⌘ + 点击」在 mac 上不会因为顺带接受 Ctrl 而多出触发面
|
|
50
|
+
* —— macOS 的 Ctrl+点击会被系统解释成右键,本就不该被我们接管。
|
|
51
|
+
* @returns 触发键是否落在 metaKey 上。
|
|
52
|
+
*/
|
|
53
|
+
const TRIGGER_IS_META = (() => {
|
|
54
|
+
const platform = typeof navigator === "undefined" ? "" : String(navigator.platform ?? "");
|
|
55
|
+
return /Mac|iPod|iPhone|iPad/.test(platform);
|
|
56
|
+
})();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 最近一次点击的修饰键快照。
|
|
60
|
+
*
|
|
61
|
+
* `openResource` 只收到地址、拿不到事件,所以在捕获阶段把真实按键记下来由它消费。
|
|
62
|
+
*/
|
|
63
|
+
const gesture = { trigger: false, other: false, at: 0 };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 记下这一下点击的修饰键。
|
|
67
|
+
* @param event - 任意鼠标/指针事件。
|
|
68
|
+
*/
|
|
69
|
+
function recordGesture(event) {
|
|
70
|
+
gesture.trigger = TRIGGER_IS_META ? event.metaKey === true : event.ctrlKey === true;
|
|
71
|
+
// 带别的修饰键时不接管:这一下原样留给 DSH 与浏览器自己处理。
|
|
72
|
+
gesture.other = event.shiftKey === true || event.altKey === true
|
|
73
|
+
|| (TRIGGER_IS_META ? event.ctrlKey === true : event.metaKey === true);
|
|
74
|
+
gesture.at = Date.now();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 当前手势是否请求「用系统默认应用打开」——只判断,不消费。
|
|
79
|
+
*
|
|
80
|
+
* 判定放在决定处理之后再消费,避免非文件资源(如 `dsh-resource://browser/…`)
|
|
81
|
+
* 白白吃掉一次点击的按键状态。
|
|
82
|
+
* @returns 这一下是否命中。
|
|
83
|
+
*/
|
|
84
|
+
function isGestureHit() {
|
|
85
|
+
const fresh = Date.now() - gesture.at <= GESTURE_TTL_MS;
|
|
86
|
+
return fresh && gesture.trigger && !gesture.other;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 让当前手势失效:一次点击只驱动一次打开,避免残留状态影响后续程序化调用。
|
|
91
|
+
*/
|
|
92
|
+
function clearGesture() {
|
|
93
|
+
gesture.at = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 把会话文件地址读回 `{ sessionId, path }`,不解析 `.` 与 `..`。
|
|
98
|
+
*
|
|
99
|
+
* 地址有两种形态:工作区内文件存相对段(`session/s1/notes/a.md`),工作区外的
|
|
100
|
+
* 绝对路径保留一个空的首段(`session/s1//elsewhere/c.md`,见 `fileAddressFor`)。
|
|
101
|
+
* @param address - `dsh-resource://file/session/<sessionId>/<path>`。
|
|
102
|
+
* @returns 解析结果;不是会话文件地址时返回 null。
|
|
103
|
+
*/
|
|
104
|
+
function parseSessionFileAddress(address) {
|
|
105
|
+
if (typeof address !== "string" || !address.startsWith(FILE_PREFIX)) return null;
|
|
106
|
+
const end = address.search(/[?#]/);
|
|
107
|
+
const rest = address.slice(FILE_PREFIX.length, end === -1 ? void 0 : end);
|
|
108
|
+
const parts = rest.split("/");
|
|
109
|
+
if (parts[0] !== "session") return null;
|
|
110
|
+
|
|
111
|
+
const rawId = parts[1];
|
|
112
|
+
const segments = parts.slice(2);
|
|
113
|
+
if (rawId === undefined || rawId === "" || segments.length === 0) return null;
|
|
114
|
+
|
|
115
|
+
// 恰好一个前导空段 = 绝对路径;其余空段是畸形地址。
|
|
116
|
+
const absolute = segments[0] === "" && segments.length > 1;
|
|
117
|
+
const body = absolute ? segments.slice(1) : segments;
|
|
118
|
+
if (body.length === 0 || body.some((segment) => segment === "")) return null;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const decoded = body.map((segment) => decodeURIComponent(segment));
|
|
122
|
+
const path = absolute ? `/${decoded.join("/")}` : decoded.join("/");
|
|
123
|
+
return { sessionId: decodeURIComponent(rawId), path };
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 把工作区相对路径拼成宿主机绝对路径;本来就是绝对路径时原样返回。
|
|
131
|
+
* @param cwd - 会话工作区根目录,可能未知。
|
|
132
|
+
* @param path - 相对或绝对路径。
|
|
133
|
+
* @returns 绝对路径;需要拼接却没有 cwd 时返回 null。
|
|
134
|
+
*/
|
|
135
|
+
function resolveToAbsolute(cwd, path) {
|
|
136
|
+
if (ABSOLUTE_PATH.test(path)) return path;
|
|
137
|
+
if (cwd === undefined || cwd === "") return null;
|
|
138
|
+
const separator = WINDOWS_PATH.test(cwd) && cwd.includes("\\") ? "\\" : "/";
|
|
139
|
+
return `${cwd.replace(/[/\\]+$/, "")}${separator}${path.replace(/^[/\\]+/, "")}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 交给宿主用系统默认应用打开一个绝对路径。
|
|
144
|
+
*
|
|
145
|
+
* 宿主拒绝时(路径不在工作区、文件不存在)Remote 会 throw;另外也可能正常返回
|
|
146
|
+
* 一个 `ok: false` 的结果,两种都当作失败向上抛,交给调用方回退预览。
|
|
147
|
+
* @param ctx - 客户端上下文。
|
|
148
|
+
* @param path - 宿主机绝对路径。
|
|
149
|
+
* @throws 当宿主没有受理这次打开时。
|
|
150
|
+
*/
|
|
151
|
+
async function openWithDefaultApp(ctx, path) {
|
|
152
|
+
const result = await ctx.remote.session.openWorkspacePath({ path });
|
|
153
|
+
// Remote 正常形状是 `{ ok, value }`;直接调用时也可能只拿到 `{ opened }`。
|
|
154
|
+
const accepted = result?.ok === true || result?.opened === true || result?.value?.opened === true;
|
|
155
|
+
if (!accepted) throw new Error("host declined to open the path");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 启动自检:确认插件依赖的宿主接口仍然存在且形状符合预期。
|
|
160
|
+
*
|
|
161
|
+
* 本插件包装的是 `sidebarRight.openResource` 这个方法名——它是 DSH 的公开服务面,
|
|
162
|
+
* 但不是稳定的 API 契约。DSH 升级后若重命名或改变签名,插件会静默失效(点了没反应,
|
|
163
|
+
* 用户不知道原因)。所以在这里显式检查一次,把「静默失效」变成「明确的控制台报错」。
|
|
164
|
+
* @param scope - 已注入 sidebarRight 与 sessions 的作用域上下文。
|
|
165
|
+
* @returns 通过返回 null,否则返回一条人类可读的失败原因。
|
|
166
|
+
*/
|
|
167
|
+
function selfCheck(scope) {
|
|
168
|
+
const sidebarRight = scope.sidebarRight;
|
|
169
|
+
const sessions = scope.sessions;
|
|
170
|
+
|
|
171
|
+
if (typeof sidebarRight?.openResource !== "function") {
|
|
172
|
+
return "sidebarRight.openResource 不存在——DSH 可能改了方法名,插件需要适配";
|
|
173
|
+
}
|
|
174
|
+
if (typeof sessions?.list?.getSnapshot !== "function") {
|
|
175
|
+
return "sessions.list.getSnapshot 不存在——无法定位会话工作目录";
|
|
176
|
+
}
|
|
177
|
+
if (typeof scope.remote?.session?.openWorkspacePath !== "function") {
|
|
178
|
+
return "remote.session.openWorkspacePath 不存在——宿主没有提供原生打开能力";
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 客户端插件主体:包装 Sidebar 的资源打开入口,并监听修饰键。
|
|
185
|
+
* @param ctx - 客户端根上下文。
|
|
186
|
+
*/
|
|
187
|
+
function apply(ctx) {
|
|
188
|
+
// sessions 与 sidebarRight 用 ctx.inject 等:它们是服务,可能晚于本插件就绪,
|
|
189
|
+
// 也可能被替换;注入回调在依赖变化时会重跑,包装因此始终挂在当前实例上。
|
|
190
|
+
ctx.inject(["sidebarRight", "sessions"], (scope) => {
|
|
191
|
+
const failure = selfCheck(scope);
|
|
192
|
+
if (failure !== null) {
|
|
193
|
+
// 用 error 而不是 warn:这是插件完全失效,不是降级。
|
|
194
|
+
console.error(
|
|
195
|
+
`[${NS}] 启动自检失败,插件未生效:${failure}\n` +
|
|
196
|
+
` 受影响行为:⌘/Ctrl + 点击文件链接不会用系统默认应用打开(仍走右侧预览)。\n` +
|
|
197
|
+
` 处理建议:升级或卸载 dsh-skip-preview,或向插件作者反馈。`
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sidebarRight = scope.sidebarRight;
|
|
203
|
+
const sessions = scope.sessions;
|
|
204
|
+
|
|
205
|
+
const previous = sidebarRight.openResource;
|
|
206
|
+
// 已经是本插件的包装(热重载重复 apply)就不再套一层。
|
|
207
|
+
if (previous[`__${NS}`] === true) return;
|
|
208
|
+
|
|
209
|
+
const original = previous.bind(sidebarRight);
|
|
210
|
+
|
|
211
|
+
const wrapped = function openResource(address, options) {
|
|
212
|
+
// 先判断这一下是不是「⌘ + 会话文件地址」;不是就完全不碰手势状态。
|
|
213
|
+
if (!isGestureHit()) return original(address, options);
|
|
214
|
+
|
|
215
|
+
const file = parseSessionFileAddress(address);
|
|
216
|
+
if (file === null) return original(address, options);
|
|
217
|
+
|
|
218
|
+
const cwd = sessions.list.getSnapshot().byId[file.sessionId]?.cwd;
|
|
219
|
+
const absolute = resolveToAbsolute(cwd, file.path);
|
|
220
|
+
if (absolute === null) {
|
|
221
|
+
console.warn(`[${NS}] 取不到会话工作区目录,退回预览:`, address);
|
|
222
|
+
return original(address, options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 确认要接管之后才消费手势:非文件资源的点击不会白白吃掉按键状态。
|
|
226
|
+
clearGesture();
|
|
227
|
+
|
|
228
|
+
openWithDefaultApp(ctx, absolute).then(
|
|
229
|
+
() => {},
|
|
230
|
+
(error) => {
|
|
231
|
+
// 失败时退回预览,而不是静默什么都不发生:用户至少能看到文件内容,
|
|
232
|
+
// 或者从预览自己的报错里知道「文件不存在 / 不在工作区」。
|
|
233
|
+
console.warn(`[${NS}] 原生打开失败,退回预览:`, absolute, error);
|
|
234
|
+
original(address, options);
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
return undefined;
|
|
238
|
+
};
|
|
239
|
+
wrapped[`__${NS}`] = true;
|
|
240
|
+
|
|
241
|
+
sidebarRight.openResource = wrapped;
|
|
242
|
+
console.info(`[${NS}] 已接管:⌘/Ctrl + 左键点文件链接 = 系统默认应用打开`);
|
|
243
|
+
|
|
244
|
+
return () => {
|
|
245
|
+
// 只还原自己那一层,别踩掉别人后来的包装。
|
|
246
|
+
if (sidebarRight.openResource === wrapped) sidebarRight.openResource = previous;
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
ctx.effect(() => {
|
|
251
|
+
const record = (event) => recordGesture(event);
|
|
252
|
+
const clear = () => {
|
|
253
|
+
gesture.at = 0;
|
|
254
|
+
};
|
|
255
|
+
// 捕获阶段早于 React 根容器的冒泡监听,读到的是这一下真实的按键状态。
|
|
256
|
+
document.addEventListener("pointerdown", record, true);
|
|
257
|
+
document.addEventListener("click", record, true);
|
|
258
|
+
document.addEventListener("keyup", clear, true);
|
|
259
|
+
document.addEventListener("pointercancel", clear, true);
|
|
260
|
+
return () => {
|
|
261
|
+
document.removeEventListener("pointerdown", record, true);
|
|
262
|
+
document.removeEventListener("click", record, true);
|
|
263
|
+
document.removeEventListener("keyup", clear, true);
|
|
264
|
+
document.removeEventListener("pointercancel", clear, true);
|
|
265
|
+
};
|
|
266
|
+
}, `${NS}: modifier snapshot`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exports.apply = apply;
|
|
270
|
+
exports.inject = inject;
|
|
271
|
+
return module.exports;
|
|
272
|
+
}
|
|
273
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-skip-preview — 宿主半区。
|
|
3
|
+
*
|
|
4
|
+
* 这个插件要改的行为全在浏览器端(按住 ⌘ 点文件链接那一下),宿主侧不需要注册
|
|
5
|
+
* 任何工具、路由或服务:用系统默认应用打开走的是已有的 `session.openWorkspacePath`
|
|
6
|
+
* Remote。所以这里只提供一个合法的空插件,让 bundle 能被宿主加载。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = "dsh-skip-preview";
|
|
10
|
+
|
|
11
|
+
/** 宿主侧不依赖任何服务。 */
|
|
12
|
+
export const inject = [];
|
|
13
|
+
|
|
14
|
+
/** 宿主侧无操作。 */
|
|
15
|
+
export function apply() {}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skip-preview",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "⌘/Ctrl + 左键点击 DSH 会话里的文件链接,跳过右侧预览、直接用系统默认应用打开 | ⌘/Ctrl+click a file link in DSH chat to skip the sidebar preview and open it with your system default app",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"deepseek-harness-plugin",
|
|
10
|
+
"file",
|
|
11
|
+
"preview",
|
|
12
|
+
"open",
|
|
13
|
+
"default-app",
|
|
14
|
+
"sidebar"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./client": {
|
|
23
|
+
"default": "./lib/client.js"
|
|
24
|
+
},
|
|
25
|
+
"./locale/*": "./locale/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"locale",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/finalrgy/dsh-skip-preview.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/finalrgy/dsh-skip-preview#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/finalrgy/dsh-skip-preview/issues"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"engines": {
|
|
49
|
+
"dsh": ">=0.2.1-rc.5"
|
|
50
|
+
},
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
},
|
|
54
|
+
"client": {
|
|
55
|
+
"inject": [
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
58
|
+
],
|
|
59
|
+
"platform": "web"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"author": "finalrgy"
|
|
63
|
+
}
|