dsh-multi-folder 0.1.2 → 0.1.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 CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
  [![Node.js >= 20](https://img.shields.io/badge/Node.js-%3E%3D20-brightgreen)](https://nodejs.org/)
9
+ [![npm version](https://img.shields.io/npm/v/dsh-multi-folder)](https://www.npmjs.com/package/dsh-multi-folder)
10
+ [![GitHub issues](https://img.shields.io/github/issues/AngelosZou/dsh-multi-folder)](https://github.com/AngelosZou/dsh-multi-folder/issues)
11
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
12
 
10
13
  A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin bundle that gives one project (workspace) a set of **secondary working directories**:
11
14
 
@@ -13,8 +16,7 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin bun
13
16
  - Under **Workspace Write** mode the agent gains the **same read / write / edit / execute permissions** on the configured secondary directories as on the primary workspace — enforced by re-rooting the session's own sandbox policy, so every mode keeps its semantics (`read-only` still denies, `workspace-write` allows, `danger-full-access` allows).
14
17
  - The directory list is **injected into the system prompt** and re-rendered per session assembly.
15
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**.
16
- - Configurable **before the session starts**: the session-creation page (new-session screen) offers a Multi-folder entry (「多工作目录」 in the Chinese UI) that reads and edits the same per-workspace configuration through a **sessionless remote API** (`multiFolder/*` endpoints) — no session id required.
17
- - **Localized UI.** The button, panel, and creation-page entries follow the DSH locale (the browser language or the Language setting in Settings): "Multi-folder" in English, 「多工作目录」 in Chinese.
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.
18
20
  - **No new tools.** Everything is a framework-level change (tool-pipeline interception) plus a UI-level change (a session-scoped header entry).
19
21
 
20
22
  ## Requirements
@@ -34,7 +36,7 @@ Then **restart the DSH backend** (host composition loads at process start) and *
34
36
 
35
37
  ## Usage
36
38
 
37
- A Multi-folder button (「多工作目录」 in the Chinese UI) appears in the session header, and a second entry appears on the **session-creation page** (fixed launcher in the bottom-right corner while the new-session screen is shown; an inline chip beside the workspace picker once the upstream `conversation.hero.workspaceExtras` slot is available). The panel lets you:
39
+ A Multi-folder button appears in the session header, and a second entry appears on the **session-creation page** (fixed launcher in the bottom-right corner while the new-session screen is shown; an inline chip beside the workspace picker once the upstream `conversation.hero.workspaceExtras` slot is available). The panel lets you:
38
40
 
39
41
  | Action | Behavior |
40
42
  | ------ | -------- |
package/README.zh.md CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
  [![Node.js >= 20](https://img.shields.io/badge/Node.js-%3E%3D20-brightgreen)](https://nodejs.org/)
9
+ [![npm version](https://img.shields.io/npm/v/dsh-multi-folder)](https://www.npmjs.com/package/dsh-multi-folder)
10
+ [![GitHub issues](https://img.shields.io/github/issues/AngelosZou/dsh-multi-folder)](https://github.com/AngelosZou/dsh-multi-folder/issues)
11
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
12
 
10
13
  一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件 bundle,为一个 Project(工作区)提供一组**副工作目录**:
11
14
 
@@ -14,7 +17,6 @@
14
17
  - 目录列表**注入系统提示词**,每次组装按会话求值;
15
18
  - 配置变更通过**不打断的消息队列**通知 Agent——在下一次消息边界(用户发送或工具调用结束)送达,且**仅在目录集合实际变化时**发送;
16
19
  - **会话开始前即可配置**:会话创建页(新会话界面)提供「多工作目录」入口(英文界面显示 "Multi-folder"),通过**无会话远程 API**(`multiFolder/*` 端点)读写同一份 per-workspace 配置——无需 session id;
17
- - **界面本地化**:按钮、面板与创建页入口跟随 DSH 的语言设置(浏览器语言或设置中的 Language 选项):英文界面显示 "Multi-folder",中文界面显示「多工作目录」。
18
20
  - **不新增任何工具**:改动全部位于框架级(工具流水线拦截)与 UI 级(会话级头部入口)。
19
21
 
20
22
  ## 环境要求
package/docs/design.md CHANGED
@@ -35,13 +35,19 @@ A listener on the `tools/execute` around-dispatch waterfall handles `write`, `ed
35
35
  4. If the canonical path is inside a configured secondary directory, execute the
36
36
  operation directly with `{ ...standingPolicy, workspaceRoot: <secondary dir> }`:
37
37
  - `write`/`edit` → `fs.writeText` / `fs.editText`;
38
- - `pwsh`/`bash` → `shell.resolve({ command, workdir, dshEnv, sandboxPolicy })` +
39
- `shell.run`, with the canonical workdir so the confinement root and the process
40
- cwd agree exactly.
38
+ - `pwsh`/`bash`, foreground → `shell.resolve({ command, workdir, dshEnv,
39
+ sandboxPolicy })` + `shell.run`, with the canonical workdir so the confinement
40
+ root and the process cwd agree exactly;
41
+ - `pwsh`/`bash`, background (`run_in_background: true`) → the same re-rooted
42
+ request registered through the generic jobs runtime (`ctx.jobs`) exactly like
43
+ the shipped shell tools (`kind` = tool name, `owner` = calling agent, streamed
44
+ reads shaped for `job_output` with sandbox markers, terminal outcome in the
45
+ `completed`/`killed` vocabulary). A caller-aborted call falls through to the
46
+ default pipeline, which raises the canonical abort error.
41
47
  The result carries the same canonical value/content shapes as the shipped tools, so
42
48
  downstream presentation keeps working.
43
49
  5. Anything else — unknown tools, paths outside every secondary directory, escalation
44
- arguments (`sandbox_permissions`), `run_in_background`, missing optional services,
50
+ arguments (`sandbox_permissions`), missing optional services (`shell`, `jobs`),
45
51
  or any error — falls through to `next()` and the default pipeline.
46
52
 
47
53
  **Why mode parity is free:** the mode field of the standing policy is never touched.
@@ -206,8 +212,17 @@ window.__ModuleLoader__.load({
206
212
  observation domain.
207
213
  - `presentationMeta` is not computed on the short-circuit path; tool cards fall back to
208
214
  their default presentation.
209
- - `run_in_background` and `sandbox_permissions` escalation on `pwsh`/`bash` calls in
210
- secondary directories are passed through to the default pipeline.
215
+ - `sandbox_permissions` escalation on `pwsh`/`bash` calls in secondary directories is
216
+ passed through to the default pipeline, which re-roots the escalated run at the
217
+ PRIMARY workspace — escalation never widens a secondary root. (Background runs are
218
+ NOT passed through: they register with `ctx.jobs` under the same re-rooted policy
219
+ as foreground runs.)
220
+ - The interceptor registers a background `pwsh`/`bash` job whenever `ctx.jobs` is
221
+ available; it cannot read the shipped shell tools' per-tool
222
+ `enableRunInBackground: false` config, so a deployment that disables background
223
+ execution would still serve secondary-dir background jobs. Deployments that
224
+ disable background execution should also disable this plugin's shell interception
225
+ or accept that exception.
211
226
  - The `/multi-folder` command lifecycle rows (`command/run`, `command/done`) are
212
227
  visible in the conversation UI by framework design; they are log-only and never
213
228
  reach the model. Workspace-mode (session-creation page) operations avoid them
package/lib/index.js CHANGED
@@ -16,7 +16,11 @@
16
16
  * here with the session's standing sandbox policy re-rooted to that
17
17
  * directory — identical semantics to the primary workspace in every mode
18
18
  * (read-only denies, workspace-write allows, danger-full-access allows).
19
- * Reads (read/glob/grep) are unfenced and already work.
19
+ * Background shell runs (`run_in_background: true`) register with the
20
+ * generic jobs runtime (`ctx.jobs`) under the same re-rooted policy,
21
+ * mirroring the shipped pwsh/bash tools so `job_output` / `job_kill` and
22
+ * finish notices keep working. Reads (read/glob/grep) are unfenced and
23
+ * already work.
20
24
  * 3. Prompt injection: one ordered system-prompt section rendered per
21
25
  * assembly from the configured directories of the assembling session.
22
26
  * 4. Non-interrupting change notification: configuration changes made via
@@ -333,7 +337,8 @@ export function apply(ctx) {
333
337
  'Secondary working directories are available in this session (dsh-multi-folder plugin):\n' +
334
338
  dirs.map((d) => '- ' + d).join('\n') +
335
339
  '\nYou have the SAME read/write/edit and command-execution permissions on these directories as on the primary workspace under the current sandbox mode. ' +
336
- 'Use absolute paths inside them (or pass `workdir` to shell tools). The primary workspace remains the default working directory.'
340
+ 'For shell tools, pass `workdir` pointing inside one of these directories foreground and background (`run_in_background`) runs alike. ' +
341
+ 'Reads from these directories work without `workdir`. The primary workspace remains the default working directory.'
337
342
  )
338
343
  },
339
344
  })
@@ -383,6 +388,49 @@ export function apply(ctx) {
383
388
  return text
384
389
  }
385
390
 
391
+ /** Terminal outcome for a background process, in the jobs-registry vocabulary. */
392
+ const processOutcome = (proc) => {
393
+ if (proc.status === 'killed') {
394
+ return {
395
+ status: 'killed',
396
+ detail: proc.signal !== null && proc.signal !== undefined ? 'signal: ' + proc.signal : 'killed before exit',
397
+ }
398
+ }
399
+ return {
400
+ status: 'completed',
401
+ detail: 'exit code: ' + (proc.exitCode === undefined || proc.exitCode === null ? 0 : proc.exitCode),
402
+ }
403
+ }
404
+
405
+ /**
406
+ * One consuming background read, shaped for `job_output`: the raw delta plus
407
+ * loss/spill notices and sandbox markers, mirroring the shipped pwsh/bash
408
+ * tools' background rendering. No escalation hint is appended — escalation
409
+ * calls stay on the default pipeline, which re-roots at the primary
410
+ * workspace, so this job can never receive a wider policy.
411
+ */
412
+ const renderProcessRead = (read, sandbox) => {
413
+ const notices = []
414
+ if (read.lossy) {
415
+ const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== undefined)
416
+ notices.push(
417
+ '[some output was dropped from memory; full output: ' +
418
+ (paths.length > 0 ? paths.join(', ') : '(unavailable)') +
419
+ ']',
420
+ )
421
+ }
422
+ if (sandbox && sandbox.runnerFailed) {
423
+ notices.push(
424
+ '[sandbox: the sandbox runner itself failed under ' + sandbox.mode +
425
+ ' mode — the command did not run; this is a sandbox problem, not a command failure]',
426
+ )
427
+ } else if (sandbox && sandbox.denied) {
428
+ notices.push('[sandbox: file access denied under ' + sandbox.mode + ' mode]')
429
+ }
430
+ if (notices.length === 0) return read.delta
431
+ return read.delta + (read.delta.length > 0 && !read.delta.endsWith('\n') ? '\n' : '') + notices.join('\n')
432
+ }
433
+
386
434
  ctx.on('tools/execute', async (exec, next) => {
387
435
  if (exec.agent && exec.agent.session && exec.agent.session.header) {
388
436
  hydrate(exec.agent.session.header.cwd)
@@ -459,7 +507,6 @@ export function apply(ctx) {
459
507
  if (exec.name === 'pwsh' || exec.name === 'bash') {
460
508
  const shell = ctx.get('shell')
461
509
  if (shell === undefined) return next()
462
- if (args && args.run_in_background === true) return next()
463
510
  const dirs = dirsForSync(primary)
464
511
  if (dirs === null) return next()
465
512
  const rawWorkdir = args && typeof args.workdir === 'string' ? args.workdir : null
@@ -483,6 +530,39 @@ export function apply(ctx) {
483
530
  ...(shellEnv !== undefined ? { dshEnv: shellEnv.collect(exec) } : {}),
484
531
  sandboxPolicy: policy,
485
532
  }
533
+
534
+ // Background runs get the SAME re-rooted policy as foreground runs.
535
+ // They register with the generic jobs runtime (`ctx.jobs`) exactly
536
+ // like the shipped pwsh/bash tools do, so `job_output` / `job_kill`
537
+ // and the finish notice keep working for the intercepted job. A
538
+ // background process outlives the tool call, so no caller signal is
539
+ // forwarded; `shell.start` ignores `timeoutMs` by design.
540
+ if (args && args.run_in_background === true) {
541
+ // An aborted call belongs to the default pipeline, which raises the
542
+ // canonical abort error before anything starts.
543
+ if (exec.signal && exec.signal.aborted) return next()
544
+ const jobs = ctx.get('jobs')
545
+ if (jobs === undefined) return next()
546
+ const jobId = jobs.start({
547
+ kind: exec.name,
548
+ label: String(args.command),
549
+ ...(exec.agent ? { owner: exec.agent } : {}),
550
+ run: () => {
551
+ const proc = shell.start(shell.resolve(request))
552
+ return {
553
+ cancel: () => void proc.kill(),
554
+ done: proc.done.then(() => processOutcome(proc)),
555
+ readOutput: () => renderProcessRead(proc.readOutput(), proc.sandbox),
556
+ }
557
+ },
558
+ })
559
+ return {
560
+ isError: false,
561
+ value: { kind: 'background', jobId },
562
+ content: [{ type: 'text', text: 'started background job ' + jobId }],
563
+ }
564
+ }
565
+
486
566
  const result = await shell.run(shell.resolve({ ...request, signal: exec.signal }))
487
567
  if (result.aborted) {
488
568
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-multi-folder",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DeepSeek Harness plugin: secondary working directories for a project. The agent keeps the primary workspace as cwd, gains equal write/exec permissions on configured secondary directories under workspace-write mode, and is notified of configuration changes at the next message boundary. Configurable from the session header AND from the session-creation page (before the first message) through a sessionless multiFolder remote API.",
5
5
  "keywords": [
6
6
  "dsh-plugin",