dsh-workspace-plus 0.1.1 → 0.2.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/README.md +59 -10
- package/lib/client.js +245 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# dsh-workspace-plus
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness Web GUI
|
|
3
|
+
DeepSeek Harness Web GUI 的插件。一个功能,三个入口:
|
|
4
4
|
|
|
5
5
|
| 入口 | 说明 |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| **工作区行上的目录按钮**(弹窗) | 一个工作区登记 N 个目录,每个目录带标签;标签在对话里可直接指代 |
|
|
8
|
+
| **对话输入框里的 `@`** | 打 `@` 就能看到本工作区的标签,选中即插入一个指向该目录绝对路径的引用 |
|
|
8
9
|
| **设置 → `workspace+`** | 一个开关:是否对**默认工作区**启用本功能(**默认关闭**) |
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
它解决的问题:一个工作区(DSH 里 = 一个目录)装不下真实项目。前后端分在两个仓库、文档在第三个目录时,
|
|
11
12
|
每次都要在对话里手写绝对路径,而且目录名相似时 agent 容易改错文件。本插件让工作区带上
|
|
12
13
|
「标签 → 绝对路径」映射,并**在每个请求里把这张表注入 agent 上下文**,于是:
|
|
13
14
|
|
|
@@ -27,7 +28,8 @@ dsh-workspace-plus/
|
|
|
27
28
|
│ ├── index.js # 宿主半:store + 设置 + 上下文注入 + workspace_dirs 工具 + HTTP 路由
|
|
28
29
|
│ └── client.js # 浏览器半:设置页 + 目录弹窗 + 工作区行按钮(无需构建)
|
|
29
30
|
├── icon.svg # 插件管理页显示的图标
|
|
30
|
-
├──
|
|
31
|
+
├── LICENSE # MIT
|
|
32
|
+
├── tools/verify.mjs # 离线自检(329 项断言,不需要 DSH 在跑)
|
|
31
33
|
└── README.md
|
|
32
34
|
```
|
|
33
35
|
|
|
@@ -112,6 +114,8 @@ dsh-workspace-plus/
|
|
|
112
114
|
|
|
113
115
|
### 界面:一个弹窗,两个触发器
|
|
114
116
|
|
|
117
|
+
(第三个入口 —— 对话输入框里的 `@` —— 见下面单独一节。)
|
|
118
|
+
|
|
115
119
|
弹窗本身注册在 **`shell.overlay`** —— 和壳层自己的「重命名工作区 / 删除工作区」对话框同一个层
|
|
116
120
|
(那两个是 `workspace.session-rename`、`workspace.session-archive`),所以观感和层级天然一致。
|
|
117
121
|
|
|
@@ -158,6 +162,32 @@ dsh-workspace-plus/
|
|
|
158
162
|
> 唯一的例外是兜底入口:它是全局的、没有工作区上下文,所以那条路径下弹窗会退回显示选择器。
|
|
159
163
|
> 正常使用中不会看到它。
|
|
160
164
|
|
|
165
|
+
## 对话输入框里的 `@`
|
|
166
|
+
|
|
167
|
+
在会话里打 `@` 会多出一组「工作区目录」候选 —— 就是已登记的标签,可以按标签、说明或路径过滤。
|
|
168
|
+
选中后插入一个引用 chip:**chip 显示标签,提交给模型的是那个目录的绝对路径**。
|
|
169
|
+
|
|
170
|
+
这样一来,即使注入的标签表因为上下文压缩而不在了,模型拿到的仍然是路径本身,指代不会失焦:
|
|
171
|
+
|
|
172
|
+
> `@backend 加个健康检查` → 模型收到 `D:\proj\backend 加个健康检查`
|
|
173
|
+
|
|
174
|
+
候选来自**所有已启用的工作区**,按工作区标题分节;**当前会话所在工作区排在最前**(能读到它的 cwd 时)。
|
|
175
|
+
会话查找只是排序优化,不是前置条件 —— 早先按「只会话工作区」出候选的写法有个致命缺点:只要 cwd 读不到,
|
|
176
|
+
整组就静默变空,而空菜单和坏 bundle 从外面看一模一样,无法证伪。
|
|
177
|
+
|
|
178
|
+
实现要点(都在 [lib/client.js](lib/client.js) 里,用壳层的 input trigger 管线):
|
|
179
|
+
|
|
180
|
+
| 事项 | 做法 |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| 注册 | `ctx.inject(['inputTriggers'], …)` → `registerSource({ trigger: '@', name: 'workspace-plus', … })`;`(trigger, name)` 必须唯一,`@reference` 是官方占用的 |
|
|
183
|
+
| 候选 | 所有 `enabled !== false` 且登记了目录的工作区;默认工作区开关关着时自然被排除 |
|
|
184
|
+
| 排序 | 从管道给的 `{ sessionId }` 反查 cwd(`sessions.list` 投影,与 `ui-reference`/`ui-session` 同一处),命中的工作区提到最前;读不到就按原序 |
|
|
185
|
+
| **必须是 async** | 管线是 `source.candidates(...).then(...)` 直接调用 —— 同步返回数组会在它自己的循环里抛错 |
|
|
186
|
+
| 分组标题 | `showGroupTitle: false` + 每行自带 `section`(工作区标题),否则标题会是管道字典里查不到的 key |
|
|
187
|
+
| chip | `appearance: 'folder'` → 文件夹图标、且不会被标成可点开;点击时因为本 source 没有 `openReference` 而是安全的空操作 |
|
|
188
|
+
| 序列化 | 本 source 自带 `codec.serialize(ref) => ref`;**chip 的 serializer 是按 source 名路由的,没有 codec 的 source 会让整次提交被拒** |
|
|
189
|
+
| 自检 | 设置 → `workspace+` 里有一行「输入框 `@` 候选」,直接告诉你候选源是否已注册、共有多少标签 |
|
|
190
|
+
|
|
161
191
|
## ⚠ 沙箱:一个会话只有一个可写根
|
|
162
192
|
|
|
163
193
|
DSH 的沙箱策略把每个会话的可写范围限定在 `SessionHeader.cwd`(也就是工作区目录);
|
|
@@ -230,14 +260,15 @@ dsh plugin --profile desktop add <本地 checkout 目录>
|
|
|
230
260
|
|
|
231
261
|
`lib/` 是仓库里已提交的产物、包内没有 `prepare` 构建脚本,所以上面三种方式都不需要
|
|
232
262
|
pnpm 的 `allowBuilds` 构建授权,装到的就是可直接加载的代码。想锁定版本可以用
|
|
233
|
-
`github:Sen70s/dsh-workspace-plus#v0.
|
|
263
|
+
`github:Sen70s/dsh-workspace-plus#v0.2.0`。要求 DSH `>=0.1.7-rc.2`(桌面版当前就是
|
|
234
264
|
`@deepseek-ai/dsh-desktop 0.1.7-rc.2`;npm 上是 `next` 通道)。
|
|
235
265
|
|
|
236
266
|
> 0.1.0 曾以包名 `dsh-workspaceplus` 发布;自 0.1.1 起更名为 `dsh-workspace-plus`,旧包已废弃,
|
|
237
267
|
> 请勿再安装。
|
|
238
268
|
|
|
239
269
|
装完后**重启 `dsh`** 并刷新页面。悬停任一工作区那一行,操作区会多出一个目录图标,点它打开弹窗;
|
|
240
|
-
设置面板里会多出一个 `workspace+`
|
|
270
|
+
设置面板里会多出一个 `workspace+` 分区(默认工作区默认关闭,见下一节);
|
|
271
|
+
对话输入框里打 `@` 会多出一组「工作区目录」候选。
|
|
241
272
|
|
|
242
273
|
卸载:
|
|
243
274
|
|
|
@@ -247,12 +278,13 @@ dsh plugin --profile desktop remove dsh-workspace-plus
|
|
|
247
278
|
|
|
248
279
|
## 使用
|
|
249
280
|
|
|
250
|
-
界面上:悬停工作区行 → 点目录图标 → 填「标签 / 绝对路径 / 说明」→
|
|
251
|
-
|
|
252
|
-
|
|
281
|
+
界面上:悬停工作区行 → 点目录图标 → 填「标签 / 绝对路径 / 说明」→ 添加(路径可以点「浏览…」用系统目录选择器)。
|
|
282
|
+
弹窗已经锁定在这个工作区上,不需要(也不能)再选一次。默认工作区要先在 设置 → `workspace+` 里打开开关,
|
|
283
|
+
它的行上才会出现那个图标;此时也可以直接在设置页里管理,或让 agent 用 `workspace_dirs`。
|
|
253
284
|
|
|
254
285
|
对话里(工作区 = `D:\proj`):
|
|
255
286
|
|
|
287
|
+
- 直接打 `@` → 选 `backend` → 输入框里出现一个 `backend` chip,提交后模型收到 `D:\proj\backend`
|
|
256
288
|
- 「把 `D:\proj\backend` 加为 backend 标签,说明写后端 API」→ agent 调 `workspace_dirs add`
|
|
257
289
|
- 「现在有哪些标签?」→ `workspace_dirs list`
|
|
258
290
|
- 「把 backend 改指向 `D:\proj\backend-v2`」→ `workspace_dirs update`
|
|
@@ -267,11 +299,11 @@ dsh plugin --profile desktop remove dsh-workspace-plus
|
|
|
267
299
|
node tools/verify.mjs
|
|
268
300
|
```
|
|
269
301
|
|
|
270
|
-
它会(当前 `
|
|
302
|
+
它会(当前 `329/329 checks passed`,另有 1 项按环境跳过):
|
|
271
303
|
|
|
272
304
|
- 校验 manifest、patch 层、`dsh.client.inject` 的取值;
|
|
273
305
|
- 用临时 `DSH_HOME` 运行宿主半:标签/路径校验、增删改、去重、大小写折叠、损坏文件容错、原子写;
|
|
274
|
-
- 用假的 Cordis ctx
|
|
306
|
+
- 用假的 Cordis ctx 验证各处注册,并**真实执行** `workspace_dirs` 的 list/add/remove;
|
|
275
307
|
- 用假的 `node:http` req/res 真实调用 HTTP 路由的 GET/POST/405/401 分支;
|
|
276
308
|
- 在 `node:vm` 里加载浏览器半,用**最小 DOM 替身**(只实现本插件用到的那几个选择器)真实驱动
|
|
277
309
|
工作区行按钮:挂载前不误报、按行注入、未分组行跳过、重复对账幂等、行移除后回收、
|
|
@@ -281,11 +313,28 @@ node tools/verify.mjs
|
|
|
281
313
|
校验**默认工作区闸门**(宿主快照标 disabled 后行按钮消失、开关打开后回来、探测失败会重试),
|
|
282
314
|
校验**目录选择器**(标签猜测始终满足宿主语法、取消不改变任何字段、失败被捕获并给出原因、
|
|
283
315
|
没有挂载 ui-workspace 时按钮自动置灰且不会调用),
|
|
316
|
+
- 校验 **`@` source**(触发器的唯一性、`candidates` 返回 thenable、按标签/说明/路径过滤、
|
|
317
|
+
默认工作区关闭时不出候选、pick 产出的 chip 与它序列化出的绝对路径一致、坏 payload 不插入),
|
|
284
318
|
并用最小 React + Button/Switch/Modal 桩**真实渲染**设置页(含拨动开关 → POST 一次 settings op →
|
|
285
319
|
共享快照更新)与弹窗/面板的各状态文案。
|
|
286
320
|
|
|
287
321
|
## 故障排查
|
|
288
322
|
|
|
323
|
+
### 输入框打 `@` 有「文件与文件夹」,但没有「工作区目录」
|
|
324
|
+
|
|
325
|
+
**先看 设置 → `workspace+` 里的「输入框 `@` 候选」那一行**,它把三种情况分开了:
|
|
326
|
+
|
|
327
|
+
| 那一行显示 | 含义与处理 |
|
|
328
|
+
| --- | --- |
|
|
329
|
+
| `候选源未注册` | 当前页面跑的还是旧 bundle。**重启 `dsh` 并强制刷新页面**(Ctrl+Shift+R) |
|
|
330
|
+
| `候选源已注册,但还没有可用的标签` | 代码是新的,只是没有可登记目录 —— 去工作区行里加标签;或者这个工作区是默认工作区而开关关着 |
|
|
331
|
+
| `候选源已注册,共 N 个标签` | 代码和数据都就位。若菜单里仍看不到,多半是这一组的行被 `@reference` 的文件/会话组挤到可视区之外,滚动菜单看看 |
|
|
332
|
+
|
|
333
|
+
**为什么必须重启而不能只刷新**:客户端 bundle 的 URL 带一个由 `mtime/ctime/size` 派生的 `rev`,
|
|
334
|
+
而 rev 只在宿主重新组合模块图时才更新(`dsh-client-modules` 的 `rebuilt()`)。文件内容是在**首次 GET**
|
|
335
|
+
时读取并缓存的,所以 rev 不变,刷新只会拿回缓存里的旧 bundle。宿主侧的重载驱动是 `dsh-hmr`,
|
|
336
|
+
你 profile 里它的 `root` 是空的 —— 也就是不监听这个插件目录。
|
|
337
|
+
|
|
289
338
|
### 工作区行上没有出现目录按钮
|
|
290
339
|
|
|
291
340
|
1. 先确认宿主半已加载(见下一条的判据)。
|
package/lib/client.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* into the agent's context on every request — so "改 backend" in conversation
|
|
15
15
|
* resolves to an absolute path instead of a guess.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Five seats cooperate:
|
|
18
18
|
*
|
|
19
19
|
* `settings.section` the `workspace+` settings page. It owns one preference:
|
|
20
20
|
* whether the feature runs for the default (first-use)
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
* panel holds the key and the glyph stops the click from
|
|
46
46
|
* ever reaching it.
|
|
47
47
|
*
|
|
48
|
+
* `@` trigger source the composer side. Typing `@` in a session offers that
|
|
49
|
+
* workspace's labels as their own group; a pick inserts a
|
|
50
|
+
* reference chip that serializes to the directory's
|
|
51
|
+
* absolute path, so the model receives the path even when
|
|
52
|
+
* the injected table is not in context.
|
|
53
|
+
*
|
|
48
54
|
* Only the shell's frozen platform module table may be `require`d here (react,
|
|
49
55
|
* react-dom, react/jsx-runtime, @deepseek-ai/cordis,
|
|
50
56
|
* @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-slots,
|
|
@@ -462,6 +468,188 @@ window.__ModuleLoader__.load({
|
|
|
462
468
|
}
|
|
463
469
|
}
|
|
464
470
|
|
|
471
|
+
// -----------------------------------------------------------------------
|
|
472
|
+
// The composer's `@` source
|
|
473
|
+
// -----------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Source name of the `@` group that offers this workspace's labels. It must be
|
|
477
|
+
* unique for its trigger (`ui-reference` owns `@reference`): the input pipeline
|
|
478
|
+
* routes a chip's serializer by source name, and a chip whose source has no
|
|
479
|
+
* registered codec rejects the whole submit.
|
|
480
|
+
*/
|
|
481
|
+
const SOURCE_NAME = 'workspace-plus'
|
|
482
|
+
|
|
483
|
+
/** Section heading of the group's rows, standing in for the pipeline's group title. */
|
|
484
|
+
const SOURCE_SECTION = '工作区目录'
|
|
485
|
+
|
|
486
|
+
/** The client session list, captured for the `@` source's cwd lookup. */
|
|
487
|
+
let sessionsService = null
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Compare two directory paths the way the host does — separator- and
|
|
491
|
+
* case-insensitively on Windows.
|
|
492
|
+
*
|
|
493
|
+
* @param left - one path.
|
|
494
|
+
* @param right - the other path.
|
|
495
|
+
* @returns whether they name the same directory.
|
|
496
|
+
*/
|
|
497
|
+
function samePath(left, right) {
|
|
498
|
+
const fold = (value) =>
|
|
499
|
+
String(value)
|
|
500
|
+
.replace(/[\\/]+$/, '')
|
|
501
|
+
.replace(/\//g, '\\')
|
|
502
|
+
.toLowerCase()
|
|
503
|
+
return fold(left) === fold(right)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The session's working directory, which is the workspace key. `sessions.list`
|
|
508
|
+
* is the same projection `ui-reference` and `ui-session` read; a composition
|
|
509
|
+
* without it simply yields no candidates.
|
|
510
|
+
*
|
|
511
|
+
* @param sessionId - the id from the trigger pipeline's session handle.
|
|
512
|
+
* @returns the cwd, or null.
|
|
513
|
+
*/
|
|
514
|
+
function sessionCwd(sessionId) {
|
|
515
|
+
if (sessionsService === null || sessionId === undefined || sessionId === null) return null
|
|
516
|
+
try {
|
|
517
|
+
const cwd = sessionsService.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd
|
|
518
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : null
|
|
519
|
+
} catch {
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The workspace a session belongs to, or null when it cannot be resolved. Used
|
|
526
|
+
* to order the `@` menu, never to decide whether it has anything to offer.
|
|
527
|
+
*
|
|
528
|
+
* @param session - the pipeline's `{ sessionId }` handle.
|
|
529
|
+
* @returns the host snapshot's workspace entry, or null.
|
|
530
|
+
*/
|
|
531
|
+
function workspaceForSession(session) {
|
|
532
|
+
if (!hostState.known) return null
|
|
533
|
+
const cwd = sessionCwd(session?.sessionId)
|
|
534
|
+
if (cwd === null) return null
|
|
535
|
+
return hostState.workspaces.find((candidate) => samePath(candidate.path, cwd)) ?? null
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read back one candidate's opaque payload.
|
|
540
|
+
*
|
|
541
|
+
* @param value - the candidate's `value` string.
|
|
542
|
+
* @returns `{ label, path }`, or undefined when the payload is unusable.
|
|
543
|
+
*/
|
|
544
|
+
function parseLabelValue(value) {
|
|
545
|
+
if (typeof value !== 'string') return undefined
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(value)
|
|
548
|
+
if (parsed === null || typeof parsed !== 'object') return undefined
|
|
549
|
+
if (typeof parsed.label !== 'string' || typeof parsed.path !== 'string') return undefined
|
|
550
|
+
return { label: parsed.label, path: parsed.path }
|
|
551
|
+
} catch {
|
|
552
|
+
return undefined
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Build the `@` source that offers registered directory labels.
|
|
558
|
+
*
|
|
559
|
+
* A pick inserts a reference chip whose `ref` is the directory's ABSOLUTE PATH,
|
|
560
|
+
* so the submitted prompt carries the path itself rather than a name the model
|
|
561
|
+
* must resolve — the label routing then holds even if the injected table was
|
|
562
|
+
* compacted away. The chip shows the label, and `appearance: 'folder'` draws
|
|
563
|
+
* the shell's folder glyph without marking the chip openable.
|
|
564
|
+
*
|
|
565
|
+
* Candidates come from EVERY enabled workspace, grouped by workspace title,
|
|
566
|
+
* with the calling session's workspace first when its cwd can be read. The
|
|
567
|
+
* session lookup is an ordering refinement, never a requirement: keying the
|
|
568
|
+
* menu on it meant one unresolved cwd turned the whole group into a silent
|
|
569
|
+
* empty list, which is unfalsifiable from the outside.
|
|
570
|
+
*
|
|
571
|
+
* @returns the trigger source.
|
|
572
|
+
*/
|
|
573
|
+
function createLabelSource() {
|
|
574
|
+
return {
|
|
575
|
+
trigger: '@',
|
|
576
|
+
name: SOURCE_NAME,
|
|
577
|
+
// The pipeline's group title would be an untranslated key; each row
|
|
578
|
+
// carries its own section heading instead.
|
|
579
|
+
showGroupTitle: false,
|
|
580
|
+
// MUST be async: the pipeline calls `source.candidates(...).then(...)`, so
|
|
581
|
+
// a synchronous array return throws inside its per-hit fan-out.
|
|
582
|
+
async candidates(session, { query, signal }) {
|
|
583
|
+
if (signal !== undefined && signal.aborted) return []
|
|
584
|
+
const offered = hostState.workspaces.filter(
|
|
585
|
+
(workspace) => workspace.enabled !== false && Array.isArray(workspace.dirs) && workspace.dirs.length > 0,
|
|
586
|
+
)
|
|
587
|
+
if (offered.length === 0) return []
|
|
588
|
+
const cwd = sessionCwd(session?.sessionId)
|
|
589
|
+
const currentIndex =
|
|
590
|
+
cwd === null ? -1 : offered.findIndex((workspace) => samePath(workspace.path, cwd))
|
|
591
|
+
const ordered = currentIndex <= 0 ? offered : [offered[currentIndex], ...offered.filter((_, index) => index !== currentIndex)]
|
|
592
|
+
const needle = String(query ?? '').trim().toLowerCase()
|
|
593
|
+
const matches = (dir) =>
|
|
594
|
+
needle === '' ||
|
|
595
|
+
dir.label.toLowerCase().includes(needle) ||
|
|
596
|
+
(dir.note ?? '').toLowerCase().includes(needle) ||
|
|
597
|
+
dir.path.toLowerCase().includes(needle)
|
|
598
|
+
return ordered.flatMap((workspace) =>
|
|
599
|
+
workspace.dirs.filter(matches).map((dir) => ({
|
|
600
|
+
name: dir.label,
|
|
601
|
+
label: dir.label,
|
|
602
|
+
description: dir.note === undefined ? dir.path : dir.note + ' · ' + dir.path,
|
|
603
|
+
icon: 'folder',
|
|
604
|
+
section: workspace.title === undefined || workspace.title === '' ? workspace.path : workspace.title,
|
|
605
|
+
value: JSON.stringify({ label: dir.label, path: dir.path }),
|
|
606
|
+
})),
|
|
607
|
+
)
|
|
608
|
+
},
|
|
609
|
+
onPick({ candidate }) {
|
|
610
|
+
const value = parseLabelValue(candidate?.value)
|
|
611
|
+
if (value === undefined) return undefined
|
|
612
|
+
return {
|
|
613
|
+
insert: {
|
|
614
|
+
source: SOURCE_NAME,
|
|
615
|
+
ref: value.path,
|
|
616
|
+
label: value.label,
|
|
617
|
+
appearance: 'folder',
|
|
618
|
+
clipboardText: value.path,
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
codec: {
|
|
623
|
+
clipboardText: (ref) => ref,
|
|
624
|
+
// The model-facing form IS the absolute path.
|
|
625
|
+
serialize: (ref) => Promise.resolve(ref),
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Whether the `@` source is currently registered, and how many labels it would
|
|
632
|
+
* offer. The settings page shows this so "the menu has no group" has a visible
|
|
633
|
+
* cause instead of being unfalsifiable.
|
|
634
|
+
*/
|
|
635
|
+
let labelSourceRegistered = false
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Summarize the `@` source for the settings page.
|
|
639
|
+
*
|
|
640
|
+
* @returns `{ registered, workspaces, labels }`.
|
|
641
|
+
*/
|
|
642
|
+
function labelSourceStatus() {
|
|
643
|
+
const offered = hostState.workspaces.filter(
|
|
644
|
+
(workspace) => workspace.enabled !== false && Array.isArray(workspace.dirs) && workspace.dirs.length > 0,
|
|
645
|
+
)
|
|
646
|
+
return {
|
|
647
|
+
registered: labelSourceRegistered,
|
|
648
|
+
workspaces: offered.length,
|
|
649
|
+
labels: offered.reduce((total, workspace) => total + workspace.dirs.length, 0),
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
465
653
|
/**
|
|
466
654
|
* Pick the workspace the dialog shows: the requested id when it still exists,
|
|
467
655
|
* otherwise whatever was selected, otherwise the first one.
|
|
@@ -966,7 +1154,7 @@ window.__ModuleLoader__.load({
|
|
|
966
1154
|
* @param props - optional initial state for tests.
|
|
967
1155
|
* @returns the settings section element.
|
|
968
1156
|
*/
|
|
969
|
-
function
|
|
1157
|
+
function WorkspacePlusSettings(props) {
|
|
970
1158
|
const host = React.useSyncExternalStore(subscribeHostState, readHostState)
|
|
971
1159
|
const [state, setState] = React.useState(props?.initial ?? { status: 'loading', message: '', ok: null, busy: false })
|
|
972
1160
|
|
|
@@ -1077,11 +1265,28 @@ window.__ModuleLoader__.load({
|
|
|
1077
1265
|
)
|
|
1078
1266
|
})()
|
|
1079
1267
|
|
|
1268
|
+
const sourceStatus = labelSourceStatus()
|
|
1269
|
+
const atLine = React.createElement(
|
|
1270
|
+
'div',
|
|
1271
|
+
{ className: 'dsh-workspace-plus-settings-description' },
|
|
1272
|
+
'输入框 `@` 候选:',
|
|
1273
|
+
sourceStatus.registered
|
|
1274
|
+
? sourceStatus.labels === 0
|
|
1275
|
+
? '候选源已注册,但还没有可用的标签 —— 先在工作区行里登记目录。'
|
|
1276
|
+
: '候选源已注册,共 ' +
|
|
1277
|
+
String(sourceStatus.labels) +
|
|
1278
|
+
' 个标签(分布在 ' +
|
|
1279
|
+
String(sourceStatus.workspaces) +
|
|
1280
|
+
' 个工作区)。'
|
|
1281
|
+
: '候选源未注册 —— 当前页面可能还是旧的插件 bundle,重启 dsh 并强制刷新页面。',
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1080
1284
|
return React.createElement(
|
|
1081
1285
|
'div',
|
|
1082
1286
|
{ className: 'dsh-workspace-plus-settings' },
|
|
1083
1287
|
row,
|
|
1084
1288
|
target,
|
|
1289
|
+
atLine,
|
|
1085
1290
|
status,
|
|
1086
1291
|
React.createElement(
|
|
1087
1292
|
'div',
|
|
@@ -1264,7 +1469,7 @@ window.__ModuleLoader__.load({
|
|
|
1264
1469
|
order: SETTINGS_SECTION_ORDER,
|
|
1265
1470
|
label: SETTINGS_SECTION_LABEL,
|
|
1266
1471
|
},
|
|
1267
|
-
|
|
1472
|
+
WorkspacePlusSettings,
|
|
1268
1473
|
),
|
|
1269
1474
|
),
|
|
1270
1475
|
'dsh-workspace-plus: settings section',
|
|
@@ -1304,6 +1509,34 @@ window.__ModuleLoader__.load({
|
|
|
1304
1509
|
'dsh-workspace-plus: directory picker',
|
|
1305
1510
|
)
|
|
1306
1511
|
|
|
1512
|
+
// The `@` source and the session list it keys off. Both are injected
|
|
1513
|
+
// optionally: without them the composer simply offers no label group.
|
|
1514
|
+
ctx.inject(['sessions'], (sessionScope) => {
|
|
1515
|
+
sessionScope.effect(
|
|
1516
|
+
() => {
|
|
1517
|
+
sessionsService = sessionScope.sessions
|
|
1518
|
+
return () => {
|
|
1519
|
+
sessionsService = null
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
'dsh-workspace-plus: session list',
|
|
1523
|
+
)
|
|
1524
|
+
})
|
|
1525
|
+
|
|
1526
|
+
ctx.inject(['inputTriggers'], (triggerScope) => {
|
|
1527
|
+
triggerScope.effect(
|
|
1528
|
+
() => {
|
|
1529
|
+
const dispose = triggerScope.inputTriggers.registerSource(createLabelSource())
|
|
1530
|
+
labelSourceRegistered = true
|
|
1531
|
+
return () => {
|
|
1532
|
+
labelSourceRegistered = false
|
|
1533
|
+
dispose()
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
'dsh-workspace-plus: @ label source',
|
|
1537
|
+
)
|
|
1538
|
+
})
|
|
1539
|
+
|
|
1307
1540
|
let registerEntry = null
|
|
1308
1541
|
let entryDispose = null
|
|
1309
1542
|
let rowEntryAvailable = false
|
|
@@ -1393,6 +1626,14 @@ window.__ModuleLoader__.load({
|
|
|
1393
1626
|
exports.subscribeHostState = subscribeHostState
|
|
1394
1627
|
exports.publishHostPayload = publishHostPayload
|
|
1395
1628
|
exports.isEnabledWorkspace = isEnabledWorkspace
|
|
1629
|
+
exports.SOURCE_NAME = SOURCE_NAME
|
|
1630
|
+
exports.SOURCE_SECTION = SOURCE_SECTION
|
|
1631
|
+
exports.samePath = samePath
|
|
1632
|
+
exports.sessionCwd = sessionCwd
|
|
1633
|
+
exports.workspaceForSession = workspaceForSession
|
|
1634
|
+
exports.parseLabelValue = parseLabelValue
|
|
1635
|
+
exports.createLabelSource = createLabelSource
|
|
1636
|
+
exports.labelSourceStatus = labelSourceStatus
|
|
1396
1637
|
exports.selectWorkspace = selectWorkspace
|
|
1397
1638
|
exports.isOutside = isOutside
|
|
1398
1639
|
exports.basenameOf = basenameOf
|
|
@@ -1404,7 +1645,7 @@ window.__ModuleLoader__.load({
|
|
|
1404
1645
|
exports.WorkdirsEntryIcon = WorkdirsEntryIcon
|
|
1405
1646
|
exports.WorkdirsDialog = WorkdirsDialog
|
|
1406
1647
|
exports.WorkdirsHintPanel = WorkdirsHintPanel
|
|
1407
|
-
exports.
|
|
1648
|
+
exports.WorkspacePlusSettings = WorkspacePlusSettings
|
|
1408
1649
|
exports.apply = apply
|
|
1409
1650
|
exports.inject = inject
|
|
1410
1651
|
return module.exports
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-plus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness plugin:
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin: label several directories inside one workspace and reference them by absolute path. Type @ in the composer to pick a label, manage them from the workspace row or the workspace_dirs tool, and the label table is injected into every request.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|