dsh-multi-folder 0.3.1 → 0.3.3
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 +24 -5
- package/README.zh.md +24 -5
- package/SECURITY.md +21 -19
- package/docs/design.md +184 -29
- package/lib/client.js +290 -32
- package/lib/index.js +654 -30
- package/lib/native-picker.ps1 +290 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin bun
|
|
|
17
17
|
- The directory list is **injected into the system prompt** and re-rendered per session assembly.
|
|
18
18
|
- Configuration changes notify the agent through a **non-interrupting message queue** — delivered at the next message boundary (user send or tool-call end), and **only when the directory set actually changed**.
|
|
19
19
|
- Configurable **before the session starts**: the session-creation page (new-session screen) offers a Multi-folder entry that reads and edits the same per-workspace configuration through a **sessionless remote API** (`multiFolder/*` endpoints) — no session id required.
|
|
20
|
+
- The **`@` file menu finds files in the configured directories**. The shipped `@` menu only ever searches the primary workspace, so this plugin contributes its own `@` group listing the secondary directories — headed by each directory.
|
|
20
21
|
- **No new tools.** Everything is a framework-level change (tool-pipeline interception) plus a UI-level change (a session-scoped header entry).
|
|
21
22
|
|
|
22
23
|
## Requirements
|
|
@@ -49,7 +50,8 @@ A Multi-folder button appears in the session header, and a second entry appears
|
|
|
49
50
|
|
|
50
51
|
| Action | Behavior |
|
|
51
52
|
| ------ | -------- |
|
|
52
|
-
| Add directory |
|
|
53
|
+
| Add directory | Uses the host's native picker, then its OS dialog on failure; a `browse` capability opens the client browser directly (path field, one level of child directories, optional new folder) |
|
|
54
|
+
| Open in file manager | Opens the host's file manager (Explorer / Finder) at a configured directory |
|
|
53
55
|
| Remove / refresh | Applies immediately |
|
|
54
56
|
| Switch session | The panel auto-switches to that session's directories |
|
|
55
57
|
| Reopen panel | Uses the per-session cache — no redundant command rows |
|
|
@@ -65,6 +67,21 @@ Equivalent slash command for the user:
|
|
|
65
67
|
|
|
66
68
|
The agent needs nothing extra: `read` / `glob` / `grep` work everywhere, and `write` / `edit` / `pwsh` / `bash` are intercepted and re-rooted automatically when the target path (or `workdir`) falls inside a configured secondary directory.
|
|
67
69
|
|
|
70
|
+
### Finding secondary files with `@`
|
|
71
|
+
|
|
72
|
+
Typing `@` in the composer offers the usual primary-workspace files **and** a group per configured secondary directory, fed by the plugin's own `multiFolder/listFiles` endpoint:
|
|
73
|
+
|
|
74
|
+
| You type | You get |
|
|
75
|
+
| -------- | ------- |
|
|
76
|
+
| `@` | one row per configured directory — the entry points |
|
|
77
|
+
| `@probe` | any matching file or directory across every configured directory, headed by its directory |
|
|
78
|
+
| `@secondary-spike/src/` | that level, listed (directories first) |
|
|
79
|
+
| `@D:/repos/other/src/` | the same level by its absolute spelling |
|
|
80
|
+
|
|
81
|
+
Picking a row inserts an **absolute-path** mention (`@D:/repos/other/src/main.ts`), because a secondary directory lies outside the primary workspace and no relative path from it can reach one. Directory rows offer the usual `Tab` drill to descend.
|
|
82
|
+
|
|
83
|
+
This is a **companion group, not a change to the shipped menu**: DSH's own `@` provider searches the session workspace only, and it is left exactly as it is. A workspace with no secondary directories configured behaves precisely as before.
|
|
84
|
+
|
|
68
85
|
## Permission model
|
|
69
86
|
|
|
70
87
|
Each confined command runs under **exactly ONE writable root** — the workspace root the call is re-rooted to (the Windows ACL runner grants a single workspace write SID per process tree). Consequences:
|
|
@@ -82,8 +99,9 @@ When a shell run ends in such a denial and references a configured secondary dir
|
|
|
82
99
|
- **Prompt injection** — one ordered `systemPrompt` section with a text provider evaluated per assembly, rendering only for sessions whose workspace has configured directories.
|
|
83
100
|
- **Notifications** — a pending notice armed by the command handler (only on actual change) is consumed at the next boundary by either the `agent/pre-step` waterfall (prepend into the entering message batch) or the `tools/post-execute` waterfall (attach as `additionalContexts`), whichever fires first — the framework's native plugin-sourced `notice` context.
|
|
84
101
|
- **Configuration & security boundary** — per-workspace config lives in a host-owned store outside every agent sandbox root (`<DSH_HOME>/storages/multi-folder/<workspace-key>.json`). Direct `write`/`edit` attempts against the config file are rejected with an explicit message — **the agent can never self-grant directories; configuration is user-managed by design**. See [SECURITY.md](SECURITY.md).
|
|
85
|
-
- **Sessionless remote API** — a `multiFolder` namespace registered through `ctx.typert.register` (hand-written `src-json` descriptors) plus a plain-object service provided as `multiFolder`. Its `list`/`add`/`remove`/`set` methods are keyed by workspace **path** and share one validated core with the `/multi-folder` command, so the creation page can configure directories before any session exists. `browse`/`makeDir`
|
|
86
|
-
- **
|
|
102
|
+
- **Sessionless remote API** — a `multiFolder` namespace registered through `ctx.typert.register` (hand-written `src-json` descriptors) plus a plain-object service provided as `multiFolder`. Its `list`/`add`/`remove`/`set` methods are keyed by workspace **path** and share one validated core with the `/multi-folder` command, so the creation page can configure directories before any session exists. `browse`/`makeDir` serve the plugin's directory browser; `pick`/`reveal` select or open a host directory.
|
|
103
|
+
- **Directory picking** — `multiFolder/pick` uses the host's `native` picker, then the OS dialog if it fails. A `browse` capability opens the client browser directly. Closing the panel cancels the request; cancelling a dialog adds nothing. The Windows helper is `lib/native-picker.ps1`.
|
|
104
|
+
- **`@` discovery** — a second reader of that same `fs` seam: `multiFolder/listFiles` indexes the configured directories (breadth-first, canonical-path deduplicated so a junction cannot re-enter the walk, generated/vendor basenames excluded, capped and cached per workspace with a short TTL) and the client half registers a companion `@` source over it. The shipped single-root provider is not modified, and the shipped files/sessions group is not disturbed: the trigger registry keys sources by `(trigger, name)` and renders one group each.
|
|
87
105
|
- **Client** — a hand-maintained factory bundle (`window.__ModuleLoader__.load`), no build toolchain required. The panel drives the host through two channels: the Remote BFF (`ctx.remote.commands.execute`) for sessions, and the shared `/api` RPC channel (`ctx.connection.rpc.call`) for the sessionless endpoints.
|
|
88
106
|
|
|
89
107
|
## Project layout
|
|
@@ -91,8 +109,9 @@ When a shell run ends in such a denial and references a configured secondary dir
|
|
|
91
109
|
| Path | Purpose |
|
|
92
110
|
| ---- | ------- |
|
|
93
111
|
| `cordis.patch.yml` | Profile patch layer inserting the `dsh-multi-folder` row |
|
|
94
|
-
| `lib/index.js` | Host plugin:
|
|
95
|
-
| `lib/client.js` | Client
|
|
112
|
+
| `lib/index.js` | Host plugin: configuration, tool interception, remote API, directory picker and file manager |
|
|
113
|
+
| `lib/client.js` | Client panel, directory browser and companion `@` source |
|
|
114
|
+
| `lib/native-picker.ps1` | Windows folder-dialog helper |
|
|
96
115
|
| `test/` | Runtime-free behavior tests (see Development) |
|
|
97
116
|
| `docs/` | Design and analysis documents |
|
|
98
117
|
|
package/README.zh.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- 目录列表**注入系统提示词**,每次组装按会话求值;
|
|
18
18
|
- 配置变更通过**不打断的消息队列**通知 Agent——在下一次消息边界(用户发送或工具调用结束)送达,且**仅在目录集合实际变化时**发送;
|
|
19
19
|
- **会话开始前即可配置**:会话创建页(新会话界面)提供「多工作目录」入口(英文界面显示 "Multi-folder"),通过**无会话远程 API**(`multiFolder/*` 端点)读写同一份 per-workspace 配置——无需 session id;
|
|
20
|
+
- **`@` 文件菜单能搜到副目录里的文件**:DSH 自带的 `@` 菜单只在主工作区内检索,因此本插件额外贡献自己的 `@` 分组,按目录成组列出副工作目录中的文件;
|
|
20
21
|
- **不新增任何工具**:改动全部位于框架级(工具流水线拦截)与 UI 级(会话级头部入口)。
|
|
21
22
|
|
|
22
23
|
## 环境要求
|
|
@@ -49,7 +50,8 @@ dsh plugin --profile web add dsh-multi-folder
|
|
|
49
50
|
|
|
50
51
|
| 操作 | 行为 |
|
|
51
52
|
| ---- | ---- |
|
|
52
|
-
| 添加目录 |
|
|
53
|
+
| 添加目录 | `native` 使用系统文件夹对话框,失败后尝试宿主对话框;`browse` 直接打开插件自带浏览器(路径输入框 + 一级子目录列表 + 可新建文件夹) |
|
|
54
|
+
| 在文件管理器中打开 | 在宿主机器的文件管理器(资源管理器 / Finder)中打开某个已配置目录 |
|
|
53
55
|
| 移除 / 刷新 | 立即生效 |
|
|
54
56
|
| 切换会话 | 面板自动切换为该会话的副工作目录 |
|
|
55
57
|
| 重新打开面板 | 使用会话级缓存,不产生冗余命令行 |
|
|
@@ -65,6 +67,21 @@ dsh plugin --profile web add dsh-multi-folder
|
|
|
65
67
|
|
|
66
68
|
Agent 无需任何额外操作:`read` / `glob` / `grep` 随处可用;`write` / `edit` / `pwsh` / `bash` 在路径(或 `workdir`)落入副目录时自动拦截并以该目录为沙箱根执行。
|
|
67
69
|
|
|
70
|
+
### 用 `@` 查找副目录文件
|
|
71
|
+
|
|
72
|
+
在输入框输入 `@`,除主工作区文件外,还会按每个已配置的副工作目录显示一个分组——数据来自插件自己的 `multiFolder/listFiles` 端点:
|
|
73
|
+
|
|
74
|
+
| 输入 | 结果 |
|
|
75
|
+
| ---- | ---- |
|
|
76
|
+
| `@` | 每个已配置目录一行——即入口 |
|
|
77
|
+
| `@probe` | 所有副目录中匹配的文件与目录,按其所在目录分组显示 |
|
|
78
|
+
| `@secondary-spike/src/` | 列出该层级(目录优先) |
|
|
79
|
+
| `@D:/repos/other/src/` | 用绝对路径写出同一层级 |
|
|
80
|
+
|
|
81
|
+
选中一行会插入**绝对路径**引用(`@D:/repos/other/src/main.ts`)——因为副目录位于主工作区之外,从主工作区出发的任何相对路径都无法到达。目录行同样支持 `Tab` 逐层下钻。
|
|
82
|
+
|
|
83
|
+
这是一个**并列分组,而不是对自带菜单的改动**:DSH 自己的 `@` provider 只检索会话工作区,本插件完全不改它。未配置任何副目录的工作区行为与从前完全一致。
|
|
84
|
+
|
|
68
85
|
## 权限模型
|
|
69
86
|
|
|
70
87
|
每条受沙箱约束的命令只拥有**唯一一个可写根**——即本次调用被换根到的那个目录(Windows ACL runner 为每个进程树只授予一个工作区写 SID)。由此:
|
|
@@ -82,8 +99,9 @@ Agent 无需任何额外操作:`read` / `glob` / `grep` 随处可用;`write`
|
|
|
82
99
|
- **提示词注入**——一个有序 `systemPrompt` 段落,text provider 每次组装按会话求值,仅为配置了副目录的会话渲染。
|
|
83
100
|
- **通知**——命令处理器仅在目录集合实际变化时置位 pending notice;`agent/pre-step`(前置注入进入批次)与 `tools/post-execute`(附加为 `additionalContexts`)两个通道中先触发者消费——均使用框架原生的插件来源 `notice` 上下文。
|
|
84
101
|
- **配置与安全边界**——per-workspace 配置存储于 Agent 沙箱之外的宿主自有目录(`<DSH_HOME>/storages/multi-folder/<workspace-key>.json`)。对配置文件的任何直接 `write`/`edit` 都会收到显式拒绝——**Agent 永远无法自我授予目录,配置权仅属于用户**。详见 [SECURITY.md](SECURITY.md)。
|
|
85
|
-
- **无会话远程 API**——经 `ctx.typert.register` 注册 `multiFolder` 命名空间(手写 `src-json` 描述符),并以普通对象服务 `multiFolder` 提供;`list`/`add`/`remove`/`set` 以工作区**路径**为键,与 `/multi-folder` 命令共享同一套校验核心,因此会话尚未建立时创建页也能直接配置。`browse`/`makeDir`
|
|
86
|
-
-
|
|
102
|
+
- **无会话远程 API**——经 `ctx.typert.register` 注册 `multiFolder` 命名空间(手写 `src-json` 描述符),并以普通对象服务 `multiFolder` 提供;`list`/`add`/`remove`/`set` 以工作区**路径**为键,与 `/multi-folder` 命令共享同一套校验核心,因此会话尚未建立时创建页也能直接配置。`browse`/`makeDir` 服务于自带浏览器;`pick`/`reveal` 负责选择或打开宿主目录。
|
|
103
|
+
- **目录选择**——`multiFolder/pick` 先调用宿主的 `native` 选择器,失败后尝试系统对话框;`browse` 能力直接打开客户端浏览器。关闭面板会取消请求;取消系统对话框不会添加目录。Windows 助手为 `lib/native-picker.ps1`。
|
|
104
|
+
- **`@` 发现**——同一 `fs` seam 的第二种用法:`multiFolder/listFiles` 为已配置目录建立索引(广度优先、按规范化路径去重以免 junction 绕回、排除生成物/依赖目录名、按工作区限量并短 TTL 缓存),客户端再据此注册一个并列的 `@` source。自带 provider 不做任何修改,自带分组也不受影响:触发器注册表以 `(trigger, name)` 为键,每个 source 各渲染一个分组。
|
|
87
105
|
- **客户端**——手写维护的 factory bundle(`window.__ModuleLoader__.load`),无需构建工具链;面板经两条通道驱动宿主:会话内走 Remote BFF(`ctx.remote.commands.execute`),无会话端点走共享 `/api` RPC 通道(`ctx.connection.rpc.call`)。
|
|
88
106
|
|
|
89
107
|
## 目录结构
|
|
@@ -91,8 +109,9 @@ Agent 无需任何额外操作:`read` / `glob` / `grep` 随处可用;`write`
|
|
|
91
109
|
| 路径 | 作用 |
|
|
92
110
|
| ---- | ---- |
|
|
93
111
|
| `cordis.patch.yml` | profile patch 层,插入 `dsh-multi-folder` 行 |
|
|
94
|
-
| `lib/index.js` |
|
|
95
|
-
| `lib/client.js` |
|
|
112
|
+
| `lib/index.js` | 宿主插件:配置、工具拦截、远程 API、目录选择器和文件管理器 |
|
|
113
|
+
| `lib/client.js` | 客户端面板、自带浏览器和并列 `@` 来源 |
|
|
114
|
+
| `lib/native-picker.ps1` | Windows 文件夹选择器助手 |
|
|
96
115
|
| `test/` | 免 DSH 运行时的行为测试(见开发) |
|
|
97
116
|
| `docs/` | 设计与分析文档 |
|
|
98
117
|
|
package/SECURITY.md
CHANGED
|
@@ -32,6 +32,27 @@ granted the session. The design enforces four boundaries:
|
|
|
32
32
|
bypasses confinement (as it already does for the primary workspace, by the user's
|
|
33
33
|
explicit choice).
|
|
34
34
|
|
|
35
|
+
### Host-side UI actions (`pick` / `reveal`)
|
|
36
|
+
|
|
37
|
+
Two sessionless endpoints ask the **host machine** to show UI instead of reading or
|
|
38
|
+
writing a path:
|
|
39
|
+
|
|
40
|
+
- `multiFolder/pick` asks the host's composed directory picker first, and only a
|
|
41
|
+
`native` composition (a loopback, attended, non-SSH host — the framework's own
|
|
42
|
+
`directory-picker-auto` decision) may fall back to this plugin's own OS dialog:
|
|
43
|
+
`lib/native-picker.ps1` on Windows, `osascript` on macOS. A `browse` composition
|
|
44
|
+
never reaches a dialog — the client is sent to the browser this plugin draws.
|
|
45
|
+
- `multiFolder/reveal` opens one **existing directory** in the host file manager
|
|
46
|
+
(`explorer.exe` / `open` / `xdg-open`). It refuses anything that is not an existing
|
|
47
|
+
directory, so a stale entry cannot launch a file.
|
|
48
|
+
|
|
49
|
+
Both ride the same trusted browser→host RPC channel as every other `multiFolder/*`
|
|
50
|
+
endpoint (the `connection` Host/Origin fence plus browser authentication) and are
|
|
51
|
+
never exposed as agent tools. They act on the host by design — the host owns the
|
|
52
|
+
filesystem the session is configured against — which is exactly why a remote client
|
|
53
|
+
must never be routed to them: the UI would appear on a display nobody clicked from.
|
|
54
|
+
The `native`-only gate above is what enforces that.
|
|
55
|
+
|
|
35
56
|
### What is deliberately out of scope
|
|
36
57
|
|
|
37
58
|
- In a `danger-full-access` session the agent can already touch the whole filesystem;
|
|
@@ -39,22 +60,3 @@ granted the session. The design enforces four boundaries:
|
|
|
39
60
|
- The agent can *read* the configuration file (reads are not policy-fenced in the DSH
|
|
40
61
|
filesystem backend). Reading reveals nothing the system prompt does not already list
|
|
41
62
|
for that session.
|
|
42
|
-
|
|
43
|
-
## Reporting a vulnerability
|
|
44
|
-
|
|
45
|
-
If you believe you have found a security issue in this plugin, please report it
|
|
46
|
-
privately by opening a GitHub Security Advisory on the repository instead of a public
|
|
47
|
-
issue. Please include:
|
|
48
|
-
|
|
49
|
-
- the affected version,
|
|
50
|
-
- a minimal reproduction,
|
|
51
|
-
- the expected vs. observed behavior.
|
|
52
|
-
|
|
53
|
-
We will acknowledge the report within 7 days and aim to publish a fix (or a
|
|
54
|
-
documented mitigation) before public disclosure.
|
|
55
|
-
|
|
56
|
-
## Supported versions
|
|
57
|
-
|
|
58
|
-
| Version | Supported |
|
|
59
|
-
| ------- | --------- |
|
|
60
|
-
| 0.1.x | ✅ |
|
package/docs/design.md
CHANGED
|
@@ -14,8 +14,8 @@ agent informed. No new tools are added.
|
|
|
14
14
|
|
|
15
15
|
| Half | File | Role |
|
|
16
16
|
| ---- | ---- | ---- |
|
|
17
|
-
| Host | `lib/index.js` | Config store, tool-pipeline interception, prompt section, notifications, `/multi-folder` command, sessionless `multiFolder/*` remote API (configuration
|
|
18
|
-
| Client | `lib/client.js` | Session-header button + overlay panel; session-creation page entry (input-dock chip, upstream hero chip, or fixed fallback launcher — one at a time), all driving the host through the Remote BFF / shared RPC channel; the owned directory browser behind "Add directory" |
|
|
17
|
+
| Host | `lib/index.js` | Config store, tool-pipeline interception, prompt section, notifications, `/multi-folder` command, sessionless `multiFolder/*` remote API (configuration, the browser's `browse`/`makeDir`, and the `@` menu's `listFiles`) |
|
|
18
|
+
| Client | `lib/client.js` | Session-header button + overlay panel; session-creation page entry (input-dock chip, upstream hero chip, or fixed fallback launcher — one at a time), all driving the host through the Remote BFF / shared RPC channel; the owned directory browser behind "Add directory"; the companion `@` source |
|
|
19
19
|
|
|
20
20
|
The package declares both faces: `dsh.bundle.patch` (the host row inserted by
|
|
21
21
|
`cordis.patch.yml`) and `dsh.client` (the web bundle at `exports["./client"]`).
|
|
@@ -36,13 +36,15 @@ A listener on the `tools/execute` around-dispatch waterfall handles `write`, `ed
|
|
|
36
36
|
operation directly with `{ ...standingPolicy, workspaceRoot: <secondary dir> }`:
|
|
37
37
|
- `write`/`edit` → `fs.writeText` / `fs.editText`;
|
|
38
38
|
- `pwsh`/`bash`, foreground → `shell.resolve({ command, workdir, dshEnv,
|
|
39
|
-
sandboxPolicy })` +
|
|
40
|
-
|
|
39
|
+
sandboxPolicy })` + the seam's foreground call — `shell.run(spec)` up to
|
|
40
|
+
0.1.6-alpha.1, `(await shell.execute(spec)).result()` from 0.1.7-alpha.1 on —
|
|
41
|
+
with the canonical workdir so the confinement root and the process cwd agree
|
|
42
|
+
exactly;
|
|
41
43
|
- `pwsh`/`bash`, background (`run_in_background: true`) → the same re-rooted
|
|
42
44
|
request registered through the generic jobs runtime (`ctx.jobs`) exactly like
|
|
43
|
-
the shipped shell tools (`kind` = tool name, `owner` = calling
|
|
45
|
+
the shipped shell tools (`kind` = tool name, `owner` = the calling session's id, streamed
|
|
44
46
|
reads shaped for `job_output` with sandbox markers, terminal outcome in the
|
|
45
|
-
`completed`/`killed`/`failed` vocabulary).
|
|
47
|
+
`completed`/`killed`/`failed` vocabulary). The launch is **async** (it
|
|
46
48
|
publishes the handle only once launch preparation — Windows ACL grants
|
|
47
49
|
included — succeeded, and rejects when preparation is cancelled or fails), so
|
|
48
50
|
the launch is adapted to the jobs runtime's synchronous `run(): JobHooks`
|
|
@@ -52,6 +54,24 @@ A listener on the `tools/execute` around-dispatch waterfall handles `write`, `ed
|
|
|
52
54
|
preparation settles the job as `failed` with the real cause, and a read before
|
|
53
55
|
publication is empty. A caller-aborted call falls through to the
|
|
54
56
|
default pipeline, which raises the canonical abort error.
|
|
57
|
+
- Both shell paths ride one version-adaptive seam (`shellUsesExecute`). DSH
|
|
58
|
+
**0.1.7-alpha.1** (commit `d6bebc5783`, "converge on execute()") deleted
|
|
59
|
+
`ShellExecutor.run` and `ShellExecutor.start` in favour of a single
|
|
60
|
+
`execute(spec): Promise<ShellExecution>`, and turned `JobSpec.owner` from the
|
|
61
|
+
calling `Agent` into its `SessionId` — `jobs-local` resolves that id through
|
|
62
|
+
`agents.get(id)`, so an Agent object throws
|
|
63
|
+
`session "[object Object]" has no live agent`. The retired calls map onto the
|
|
64
|
+
new seam exactly: `await shell.run(spec)` becomes
|
|
65
|
+
`await (await shell.execute(spec)).result()`, and `await shell.start(spec)`
|
|
66
|
+
becomes `await shell.execute({ ...spec, onExpiry: 'none' })` — the retired
|
|
67
|
+
`start` armed no deadline at all, while `resolve()` defaults `onExpiry` to
|
|
68
|
+
`'kill'`, so a background run that inherited the default would be killed at
|
|
69
|
+
the executor's timeout. One probe on the **presence of `execute`** selects the
|
|
70
|
+
shape (never the absence of `run`, so a release that keeps the retired methods
|
|
71
|
+
as shims still takes the modern path), which is what keeps every release from
|
|
72
|
+
0.1.2-alpha through 0.2.0-rc.1+ served by the same code. A `ShellExecution`
|
|
73
|
+
*is* the `ShellProcess` the background path already consumed, so the jobs
|
|
74
|
+
adaptation itself needed no change.
|
|
55
75
|
The result carries the same canonical value/content shapes as the shipped tools, so
|
|
56
76
|
downstream presentation keeps working.
|
|
57
77
|
5. Anything else — unknown tools, paths outside every secondary directory, missing
|
|
@@ -120,7 +140,7 @@ opens its own **sessionless** endpoints on the shared `/api` RPC channel:
|
|
|
120
140
|
`ctx.inject(['typert'], (t) => t.typert.register(REMOTE_CONTRIBUTION))` —
|
|
121
141
|
the sanctioned manual path documented by `dsh-typert-loader` ("Manual
|
|
122
142
|
`ctx.typert.register()` remains available for contributions that do not use
|
|
123
|
-
a `./typert` artifact"). All
|
|
143
|
+
a `./typert` artifact"). All nine descriptors use `src-json` codecs (no zod
|
|
124
144
|
schemas needed) with `invocation: { kind: 'direct' }`:
|
|
125
145
|
|
|
126
146
|
| Endpoint | Parameters (wire) | Result |
|
|
@@ -131,9 +151,14 @@ opens its own **sessionless** endpoints on the shared `/api` RPC channel:
|
|
|
131
151
|
| `multiFolder/set` | `workspace`, `dirs` | `{ workspace, dirs, changed }` |
|
|
132
152
|
| `multiFolder/browse` | `path` | `{ path, parent, home, entries, truncated }` |
|
|
133
153
|
| `multiFolder/makeDir` | `parent`, `name` | `{ path, parent }` |
|
|
154
|
+
| `multiFolder/listFiles` | `workspace`, `query` | `{ workspace, dirs, candidates, truncated }` |
|
|
155
|
+
| `multiFolder/pick` | `cwd` (plus transport cancellation) | `{ path, via }` |
|
|
156
|
+
| `multiFolder/reveal` | `path` | `{ path, via }` |
|
|
134
157
|
|
|
135
|
-
The four configuration endpoints are keyed by workspace;
|
|
136
|
-
the plugin's own directory browser and are keyed by path instead
|
|
158
|
+
The four configuration endpoints are keyed by workspace; `browse`/`makeDir`
|
|
159
|
+
serve the plugin's own directory browser and are keyed by path instead;
|
|
160
|
+
`listFiles` serves the `@` menu (see its own section below) and is keyed by
|
|
161
|
+
workspace plus the live query.
|
|
137
162
|
|
|
138
163
|
The workspace argument is a **path**, not a session id; the client derives
|
|
139
164
|
it from the workspaces store (`WorkspaceView.path`). Business errors throw
|
|
@@ -155,6 +180,85 @@ opens its own **sessionless** endpoints on the shared `/api` RPC channel:
|
|
|
155
180
|
does (type checks, absolute-path requirement, canonicalization, sanitization,
|
|
156
181
|
primary-workspace exclusion).
|
|
157
182
|
|
|
183
|
+
## Host: `@` discovery for secondary directories
|
|
184
|
+
|
|
185
|
+
### Why this exists
|
|
186
|
+
|
|
187
|
+
DSH's `@` file menu is **single-root by construction**, not by omission. Its
|
|
188
|
+
provider (`@deepseek-ai/dsh-file-reference-local`) builds exactly one
|
|
189
|
+
`WorkspaceFileSearch` per agent from `agent.session.header.cwd`, and that
|
|
190
|
+
searcher refuses every candidate outside its root — a directory query resolves
|
|
191
|
+
against the root and answers `undefined` for a path that escapes it (`..`
|
|
192
|
+
check). Browser-side, `@deepseek-ai/dsh-client-ui-reference` registers the only
|
|
193
|
+
`@` source and forwards each query to `remote.fileReferences.list`. A configured
|
|
194
|
+
secondary directory is by definition outside the primary workspace, so no
|
|
195
|
+
amount of typing can make the shipped menu offer one.
|
|
196
|
+
|
|
197
|
+
The plugin therefore does not touch that provider at all. It publishes its own
|
|
198
|
+
discovery endpoint over the files it already has read access to and contributes
|
|
199
|
+
a companion menu group, which keeps both failure modes separate: a broken
|
|
200
|
+
discovery path degrades to an empty group and can never affect the shipped
|
|
201
|
+
files/sessions group.
|
|
202
|
+
|
|
203
|
+
### Index and query rules
|
|
204
|
+
|
|
205
|
+
`multiFolder/listFiles(workspace, query)` rides the same sessionless remote
|
|
206
|
+
namespace as the browser endpoints and the same `fs` seam (`fs.resolve` +
|
|
207
|
+
`fs.listDir`), so it needs no session and no new capability:
|
|
208
|
+
|
|
209
|
+
| Rule | Why |
|
|
210
|
+
| ---- | --- |
|
|
211
|
+
| Breadth-first, bounded by `MAX_INDEX_ENTRIES` across all directories | one workspace's index cannot grow without limit |
|
|
212
|
+
| Canonical-path deduplication (`fs.resolve` + `fs.processPath` per level) | a junction/symlink pointing back up the tree cannot re-enter the walk |
|
|
213
|
+
| `INDEX_EXCLUDED` basenames never traversed | mirrors the shipped provider's defaults (`.git`, `node_modules`, build output); `lib` is deliberately absent there and therefore here |
|
|
214
|
+
| Hidden entries indexed but invisible to a global query | parity with the shipped provider: `.foo` needs an explicit `.` query |
|
|
215
|
+
| Per-workspace index cached with `FILE_INDEX_TTL_MS` (4 s) and a directory signature | config changes invalidate immediately; on-disk changes are caught within the TTL. Autocomplete is advisory, so staleness is invisible while rebuild cost stays bounded to one traversal per window |
|
|
216
|
+
| An empty query yields the configured directories themselves | the menu's entry points; `@` alone stays cheap and does not list 30 files |
|
|
217
|
+
|
|
218
|
+
Ranking mirrors the shipped provider (name beat path, directories win ties,
|
|
219
|
+
then shorter paths, then name order) with **one deliberate deviation**: the
|
|
220
|
+
path and subsequence rules read the *in-directory relative* path, never the
|
|
221
|
+
absolute one. Every absolute path on a host shares its prefix (`D:/…`), so
|
|
222
|
+
scoring it would make a one-character query match literally every candidate.
|
|
223
|
+
The configured directory's own basename stays searchable at the lowest rank,
|
|
224
|
+
which is what lets `@secondary-spike` narrow to that directory.
|
|
225
|
+
|
|
226
|
+
A query carrying `/` lists a level instead of ranking one, and accepts two
|
|
227
|
+
spellings — the absolute path a drill inserted, and a leading basename segment
|
|
228
|
+
(`@secondary-spike/src/`). An ambiguous basename (two configured directories
|
|
229
|
+
sharing one) resolves to nothing rather than to a guess. The decoded
|
|
230
|
+
in-directory path is normalized (redundant separators and `.` segments drop)
|
|
231
|
+
but **any `..` segment is refused**: without that, `@secondary/../../etc/` would
|
|
232
|
+
list a directory outside every configured root while still claiming the
|
|
233
|
+
configured directory as the candidates' owner — a listing that escaped the set
|
|
234
|
+
the user granted, wearing a name that lies about it. The shipped provider
|
|
235
|
+
refuses the same escape for the same reason.
|
|
236
|
+
|
|
237
|
+
### Mention semantics
|
|
238
|
+
|
|
239
|
+
A secondary directory lies outside the workspace root, so no relative path from
|
|
240
|
+
that root can reach one: candidates are **absolute** paths (forward-slashed,
|
|
241
|
+
which is the spelling the mention grammar and the prompt guidance already use
|
|
242
|
+
for host paths). `formatMention` in the client half re-implements the shipped
|
|
243
|
+
grammar — whitespace quotes the path, a quoted directory keeps its quote open
|
|
244
|
+
so completion can descend, and control characters or an embedded quote make a
|
|
245
|
+
path unrepresentable, so that row is dropped rather than inserted broken. The
|
|
246
|
+
bundle is standalone (no build step) and cannot import that package's module,
|
|
247
|
+
hence the re-implementation.
|
|
248
|
+
|
|
249
|
+
### Client contribution
|
|
250
|
+
|
|
251
|
+
| Decision | Reason |
|
|
252
|
+
| -------- | ------ |
|
|
253
|
+
| A **companion source** (`trigger: '@'`, `name: 'multi-folder'`), not a replacement | the registry keys sources by `(trigger, name)` and throws on a duplicate; the shipped `reference` source keeps answering untouched |
|
|
254
|
+
| `order: 10` | the shipped group declares no order and defaults to `0`, so the secondary group sits below it |
|
|
255
|
+
| `showGroupTitle: false` | the menu derives a group's title by looking its **source name** up in its own dictionary, so a visible title would read `multi-folder` — and an empty result would render that heading over an empty list |
|
|
256
|
+
| A `section` on every row = the directory | rows are headed by the directory they came from, and the abbreviation of a path under the host home (`~`) keeps that heading readable |
|
|
257
|
+
| `ctx.inject(['inputTriggers'], …)`, not a hard `inject` entry | a shell composing no trigger service must keep every panel surface; this contribution then simply never activates |
|
|
258
|
+
| Own `codec` (identity) | `serializeReference` resolves the owner by **source name** and rejects a codec-less owner, so the source must own one even though the mention IS the model form |
|
|
259
|
+
| Failures resolve to `[]` | discovery is advisory: it must never break the composer or the groups beside it |
|
|
260
|
+
| Queries are per keystroke, like the shipped source | the host index makes each call an in-memory rank |
|
|
261
|
+
|
|
158
262
|
## Host: prompt injection and notifications
|
|
159
263
|
|
|
160
264
|
- One global `systemPrompt.section` (`multi-folder:secondary-dirs`, order 160) whose
|
|
@@ -192,7 +296,11 @@ window.__ModuleLoader__.load({
|
|
|
192
296
|
- `inject: ['remote', 'remote.commands', 'slots', 'workspaces', 'connection', 'sessions', 'locale']`; the package's
|
|
193
297
|
`dsh.client.inject` lists the packages providing them
|
|
194
298
|
(`@deepseek-ai/dsh-api-gateway`, `@deepseek-ai/dsh-api-remotes`,
|
|
195
|
-
`@deepseek-ai/dsh-client-connection`, `@deepseek-ai/dsh-client-locale
|
|
299
|
+
`@deepseek-ai/dsh-client-connection`, `@deepseek-ai/dsh-client-locale`,
|
|
300
|
+
`@deepseek-ai/dsh-client-ui-input-trigger`). The trigger registry is
|
|
301
|
+
deliberately **not** in that hard list: the `@` source is contributed through
|
|
302
|
+
`ctx.inject(['inputTriggers'], …)`, so shells without that service keep every
|
|
303
|
+
panel surface (see the `@` discovery section).
|
|
196
304
|
- UI registrations: `conversation.session.header.actions` (session-scoped button),
|
|
197
305
|
`shell.overlay` panel, `conversation.input.dock` chip row (session-scoped
|
|
198
306
|
list entry above the composer card — the session-creation page's shipped
|
|
@@ -224,25 +332,33 @@ window.__ModuleLoader__.load({
|
|
|
224
332
|
'multiFolder/<op>', { args })` against the sessionless remote endpoints.
|
|
225
333
|
The panel runs in either mode according to how it was opened; mutations
|
|
226
334
|
and refreshes route per mode, and both modes share the same row/error UI.
|
|
227
|
-
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
335
|
+
- Directory picking: `multiFolder/pick` calls the host's `native` picker with
|
|
336
|
+
the RPC abort signal, then tries a host OS dialog if that picker fails.
|
|
337
|
+
A `browse` capability returns `unavailable` immediately, so remote clients
|
|
338
|
+
open the plugin's browser instead of a dialog on the host. A completed dialog
|
|
339
|
+
returns a path or `null` on cancellation; closing the panel aborts the request.
|
|
340
|
+
- The owned browser's listing rides the **`fs` seam** (`fs.resolve` +
|
|
341
|
+
`fs.listDir`), which every composition provides; only directories are
|
|
342
|
+
returned, hidden entries are flagged, the level is capped at 1000 with a
|
|
343
|
+
`truncated` flag, and paths must be fully qualified. Creation mirrors the
|
|
344
|
+
shipped browse backend (`dsh-host-directory-picker-browse`) by calling Node's
|
|
345
|
+
`mkdir` on a validated single segment, because the `fs` seam exposes no
|
|
346
|
+
creation primitive. Neither endpoint touches the configuration store: choosing
|
|
347
|
+
a level still commits through the mode's own channel (`/multi-folder add` in a
|
|
348
|
+
session, `multiFolder/add` on the creation page).
|
|
349
|
+
- On Windows, `lib/native-picker.ps1` uses `IFileOpenDialog` on an STA thread,
|
|
350
|
+
sets per-monitor DPI awareness (a build without that thread API still shows the
|
|
351
|
+
dialog), assists it to the foreground for a few seconds, and closes an
|
|
352
|
+
unanswered dialog on a deadline. It exits 0 for a selection or dismissal and
|
|
353
|
+
nonzero when `Show()` fails. `multiFolder/reveal` checks `fs.stat` before
|
|
354
|
+
opening an existing directory with the host file manager.
|
|
355
|
+
- `@` source: registered through `ctx.inject(['inputTriggers'], …)` (see the
|
|
356
|
+
`@` discovery section for the full decision table). It resolves the addressed
|
|
357
|
+
session's workspace from the `sessions` snapshot (`byId[sessionId].cwd`), calls
|
|
358
|
+
`multiFolder/listFiles` over the shared RPC channel, and projects each
|
|
359
|
+
candidate onto a row — a `section` naming its directory, an in-directory
|
|
360
|
+
`description`, and a `value` carrying the mention the pick inserts. Directory
|
|
361
|
+
rows set `drill`, so the shipped drill gesture descends a level.
|
|
246
362
|
- Session switch: a `React.useEffect` on `sessionId` re-points the open panel
|
|
247
363
|
to the current session (reusing the per-session cache) — this also folds a
|
|
248
364
|
workspace-mode panel back into session mode once the first message creates
|
|
@@ -375,6 +491,34 @@ window.__ModuleLoader__.load({
|
|
|
375
491
|
business validation server-side. DSH versions that change the Typert
|
|
376
492
|
registry contract would need this contribution revisited (the tests assert
|
|
377
493
|
the descriptor shape).
|
|
494
|
+
- `@` discovery is a **companion group, never a merged list**. The shipped
|
|
495
|
+
provider stays single-root, so a secondary file appears under the plugin's
|
|
496
|
+
own group and its mention is an ABSOLUTE path — a relative one could not
|
|
497
|
+
reach outside the workspace root. Consequences worth knowing: the workspace
|
|
498
|
+
file list and the secondary list are ranked separately (the shipped group's
|
|
499
|
+
relevance order never mixes with ours, and only the first 30 secondary
|
|
500
|
+
candidates of a query are offered), a session whose workspace has no
|
|
501
|
+
configured directories gains an empty group that renders nothing, and the
|
|
502
|
+
index is advisory by design — it is rebuilt lazily (4 s TTL) rather than
|
|
503
|
+
invalidated on every tool result, so a file created in a secondary directory
|
|
504
|
+
can take a few seconds to appear in the menu.
|
|
505
|
+
- A **symlinked or junctioned level inside a configured directory is indexed and
|
|
506
|
+
listed through the link**, where the shipped provider skips symlinked
|
|
507
|
+
directories. The fs seam's `listDir` reports only an entry's `type`, so a link
|
|
508
|
+
is not distinguishable from a directory without a second canonicalization per
|
|
509
|
+
child — and rejecting on that basis would also reject junctions that are
|
|
510
|
+
ordinary project structure (and a secondary directory reached through a
|
|
511
|
+
junctioned ancestor). The walk stays bounded regardless: canonical-path
|
|
512
|
+
deduplication prevents a link from re-entering it, and `MAX_INDEX_ENTRIES`
|
|
513
|
+
caps it. Reads are unfenced in DSH, so this grants no access the plugin's own
|
|
514
|
+
directory browser does not already have.
|
|
515
|
+
- The `@` group depends on the trigger registry's source contract
|
|
516
|
+
(`trigger`/`name`/`order`/`showGroupTitle`, `candidates`/`onPick`/`codec`,
|
|
517
|
+
candidate `section`/`icon`/`drill`, and the `(trigger, name)` uniqueness key).
|
|
518
|
+
A DSH release that changes that contract would need this contribution
|
|
519
|
+
revisited; because the registration rides `ctx.inject`, a release that drops
|
|
520
|
+
the service entirely degrades to "no `@` group" instead of breaking the
|
|
521
|
+
plugin's panel.
|
|
378
522
|
|
|
379
523
|
## Tests
|
|
380
524
|
|
|
@@ -397,3 +541,14 @@ shape, in-flow row, hero-only visibility, RPC routing, anchored popover), the
|
|
|
397
541
|
upstream hero chip taking over the moment its slot is declared, and the fixed
|
|
398
542
|
launcher returning once both declarations collapse — asserting at each step that
|
|
399
543
|
the other two surfaces stand down.
|
|
544
|
+
|
|
545
|
+
`@` discovery is covered on both halves against a fake secondary tree behind the
|
|
546
|
+
`fs` seam: the host test asserts the empty-query entry points, bare-fragment
|
|
547
|
+
ranking, directory-basename narrowing, level listing by both spellings,
|
|
548
|
+
recursion, hidden-entry visibility, `node_modules` exclusion, index caching
|
|
549
|
+
inside the TTL, the workspace fence, config-store isolation, and that clearing
|
|
550
|
+
the configuration withdraws the surface; the client test asserts the source's
|
|
551
|
+
identity/order/`showGroupTitle`, its RPC routing and keying, row projection
|
|
552
|
+
(section heading, `~` abbreviation, in-directory description, drill flag,
|
|
553
|
+
folder icon), quote handling, the pick inserts, the identity codec, and that
|
|
554
|
+
both an unrepresentable path and an RPC failure degrade quietly.
|