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.
Files changed (3) hide show
  1. package/README.md +59 -10
  2. package/lib/client.js +245 -4
  3. 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
- 功能 2 解决的问题:一个工作区(DSH 里 = 一个目录)装不下真实项目。前后端分在两个仓库、文档在第三个目录时,
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
- ├── tools/verify.mjs # 离线自检(258 项断言,不需要 DSH 在跑)
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.1.1`。要求 DSH `>=0.1.7-rc.2`(桌面版当前就是
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
- 不需要(也不能)再选一次。默认工作区要先在 设置 → `workspace+` 里打开开关,它的行上才会出现那个图标;
252
- 此时也可以直接在设置页里管理,或让 agent 用 `workspace_dirs`。
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
- 它会(当前 `283/283 checks passed`,另有 1 项按环境跳过):
302
+ 它会(当前 `329/329 checks passed`,另有 1 项按环境跳过):
271
303
 
272
304
  - 校验 manifest、patch 层、`dsh.client.inject` 的取值;
273
305
  - 用临时 `DSH_HOME` 运行宿主半:标签/路径校验、增删改、去重、大小写折叠、损坏文件容错、原子写;
274
- - 用假的 Cordis ctx 验证三处注册,并**真实执行** `workspace_dirs` 的 list/add/remove;
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
- * Four seats cooperate:
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 WorkspaceplusSettings(props) {
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
- WorkspaceplusSettings,
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.WorkspaceplusSettings = WorkspaceplusSettings
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.1.1",
4
- "description": "DeepSeek Harness plugin: give a workspace a label -> absolute-path directory map, injected into every request, and manageable from the workspace row or the workspace_dirs tool.",
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": {