dsh-side-branch 0.1.1

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.
@@ -0,0 +1,88 @@
1
+ # DSH 临时会话 (Side Ask)
2
+
3
+ [English](../README.md) | 简体中文
4
+
5
+ 在主会话里选中一段文字,右栏即可弹出一条只读的分支子会话。它继承主会话已有的上下文,逐词流式回答,**且答案绝不会污染主会话的模型上下文**。
6
+
7
+ 核心场景:在不干扰主会话连贯性的前提下,针对当前对话内容进行延伸追问、查词或探索。
8
+
9
+ * **包名 (底层实现):** `dsh-side-branch`
10
+ * **UI 名称 (界面显示):** 临时会话 (Side Ask)
11
+ * **前置要求:** Node.js ≥ 20,且本机 `PATH` 环境变量中已安装 `pnpm`
12
+ * **已验证 DSH 版本:** `0.1.5-rc.2` 与 `0.1.7-rc.2`
13
+
14
+ <p align="center">
15
+ <img src="../assets/sidebar-guide.png" alt="右侧栏引导页里的「临时会话」入口" width="600">
16
+ </p>
17
+
18
+ <p align="center">
19
+ <img src="../assets/selection-entry.png" alt="划选文本入口" width="600">
20
+ </p>
21
+
22
+ <p align="center">
23
+ <img src="../assets/panel-answering.png" alt="面板流式回答" width="600">
24
+ </p>
25
+
26
+ ## 💡 为什么造这个轮子?(设计初衷)
27
+
28
+ 在开发本项目前(2026年9月),我调研了 DSH 生态中的 14 个侧边会话/气泡类插件,发现它们**没有一款能同时满足以下所有硬需求**:
29
+
30
+ 1. **真继承上下文**(不是简单截断摘要)。
31
+ 2. **支持同一段内多轮追问**(不是一次性气泡)。
32
+ 3. **逐词流式输出**(包含推理过程)。
33
+ 4. **轻量稳定**(只使用官方公开 API,无臃肿依赖)。
34
+ 5. **执行层只读,且绝不破坏前缀缓存**。
35
+
36
+ 当时最接近的几款方案都有各自的妥协:`@michengai/dsh-btw` 是一次性的且无流式;`dsh-side-chat-plus` 默认不继承历史;`dsh-sidenote` 功能全但极其沉重(需引入 14.7 MB 的前置依赖);`@lukeknow0/dsh-side-chat` 架构优秀但已停更。
37
+
38
+ 最大的分歧点在于“只读”的实现方式:同类插件普遍采用 `toolFilter` 强行删掉提示词中的工具来实现只读,但这会导致分支的提示词前缀与主会话分叉,**使得父会话积累的前缀缓存全部失效**。
39
+ 本项目坚决选择将“只读”拦截放在**执行层**,分支的提示词前缀与主会话**逐字一致**,从而能够继续享受极高的前缀缓存命中率(实测可达 97%–98%)。
40
+
41
+ ## ✨ 核心特性
42
+
43
+ * **真分支,零污染:** 宿主端绝不向父会话写入任何内容。临时会话的问答不会出现在主会话的模型上下文中。
44
+ * **前缀缓存极速命中:** 自动读取主会话已完成的回合前缀作为种子。沿用主会话模型时,继续命中原有前缀缓存,无需重算。
45
+ * **执行层只读隔离:** 安全策略不依赖提示词。由底层 `ctx.tools.guard` 强制接管,仅白名单放行六个无副作用的查询类工具(`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`),其余指令(如写文件、执行脚本)在执行层被直接拦截。
46
+ * **极致的流式体验:** 正文与推理过程(可折叠的“思考”行)逐词流式输出。即使关闭浏览器标签,后台回答也不会中断,重新打开即可续接。
47
+ * **会话状态持久化:** 面板状态按「会话 + 面板格」本地持久化。闲置自动休眠,下次提问静默唤醒。
48
+
49
+
50
+ ## 📦 安装与启用
51
+
52
+ 安装后必须**重启 DSH** 以使客户端插件加载生效。
53
+
54
+ **通过 npm 安装 (推荐):**
55
+ ```bash
56
+ dsh plugin --profile web add dsh-side-branch
57
+ ```
58
+
59
+ **通过 GitHub 安装:**
60
+ ```bash
61
+ dsh plugin --profile web add github:wucl12/dsh-side-branch
62
+ ```
63
+
64
+ > 🤖 **给 AI Agent 的提示:** 如果你是负责安装此插件或阅读源码的 Agent,请优先查阅本仓库的 [`AGENTS.md`](../AGENTS.md) 以获取硬性约束、架构细节和自动化安装指引。
65
+
66
+ ## 🚀 如何使用
67
+
68
+ 1. **划选提问:** 在主会话的正文区域选中任意文字,点击弹出的「引用并提问」,即可在右侧面板开启临时会话,原文将作为引用一并发送。
69
+ 2. **快速入口:** 主会话底部输入框右侧有一个 `↗` 快速入口按钮,点击可直接呼出临时会话面板(可在设置中关闭)。
70
+ 3. **独立设置:** 在临时会话面板中,支持为当前分支单独指定模型与推理等级,无需跟随主会话。
71
+
72
+ ## 🛡️ 安全与限制说明
73
+
74
+ * **关于“只读”边界:** 本插件的只读是指**不产生系统副作用**(如写文件、执行命令),但 `web_fetch` / `web_search` 仍具备网络访问能力,`read` 可以读取进程权限内的文件。放行名单硬编码在代码中,不可通过设置放宽。
75
+ * **上下文继承的快照机制:** 临时分支继承的是**开段那一刻**的历史快照。开启后,主会话产生的新内容不会自动同步到当前分支。若需包含新内容,请点击面板左下角的「清空」另起一段。
76
+ * **PTC 模式的特殊处理:** 若主会话使用 `ptc` preset(纯代码执行工具面),由于 `run_code` 永远被本插件拒绝,此分支将变为**纯文本问答模式**(无法调用任何工具)。
77
+
78
+ ## 📚 更多文档
79
+
80
+ * [English](../README.md):English documentation (the same content)。
81
+ * [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md):完整技术文档——前缀缓存机制、只读拦截链路、PTC 行为与全部已知限制。
82
+ * [`AGENTS.md`](../AGENTS.md):专门写给 AI Agent 看的架构契约与硬约束(人类开发者二次开发前必读)。
83
+ * [`SECURITY.zh-CN.md`](./SECURITY.zh-CN.md):安全承诺范围与漏洞报告渠道。
84
+ * [`CHANGELOG.md`](./CHANGELOG.md):版本更新历史。
85
+
86
+ ## 📄 许可证
87
+
88
+ [MIT](../LICENSE)
@@ -0,0 +1,36 @@
1
+ # 安全说明
2
+
3
+ [English](../SECURITY.md) · 简体中文 · [README](./README.zh-CN.md)
4
+
5
+ 本插件的核心承诺是**执行层只读**:侧枝会话可以调用六个只读工具,其余工具在**执行层**被拒绝
6
+ (`ctx.tools.guard()`,判据是插件级常量 `ALLOWED_TOOLS` 的名字白名单,失配方向 fail-closed)。
7
+
8
+ ## 报告安全问题
9
+
10
+ 用 GitHub 私有安全通告提交:<https://github.com/wucl12/dsh-side-branch/security/advisories/new>
11
+
12
+ 请不要为**绕过只读**类问题开公开 issue。会在 7 天内回应。
13
+
14
+ ## 算安全问题的
15
+
16
+ - **guard 绕过**:任何能让侧枝会话执行白名单外工具(写文件、跑命令、开孙子会话、改宿主)
17
+ 的路径,包括 `run_code`(PTC)与 `mcp__*` 工具。
18
+ - **答案回流**:任何能让侧枝会话的回答进入主会话模型上下文的路径。
19
+ - **宿主路由绕过**:不经过 `ctx.connection.requestRejection` 就访问 `/side-branch/*`,
20
+ 或客户端能让宿主执行未校验输入(问题长度、模型标识、`conversationId` 归属)。
21
+ - **客户端 XSS / 注入**:渲染答案或引用原文时执行脚本。
22
+
23
+ ## 不算安全问题的(设计上就是这样)
24
+
25
+ 这些在 README 的「只读不等于无害」和 [`AGENTS.md`](../AGENTS.md) 的「为什么这样做」里都写了:
26
+
27
+ - 放行的六个工具本身的能力:`web_fetch` / `web_search` 是**网络出口**(分支里的敏感内容可以被发出去)、
28
+ `lsp` 会**起语言服务器进程**、`read` / `glob` / `grep` 能读**进程权限范围内的任意路径** ——
29
+ 没有按分支划分的沙箱。
30
+ - 引用原文与工具取回的外部内容里的提示词注入。插件做了反注入声明,但那是**提示词层**的约束,
31
+ 不是执行层的。
32
+ - 放行名单不可配置(要收窄只能改代码)。这是刻意的取舍,理由同上。
33
+
34
+ ## 支持的版本
35
+
36
+ 只对最新发布版本提供修复。插件在 README 声明的 DSH 版本上验证过。
@@ -0,0 +1,225 @@
1
+ # DSH Side Branch — Side Ask
2
+
3
+ **English** · [简体中文](./TECHNICAL.zh-CN.md) · [README](../README.md)
4
+
5
+ Select text in the main session and a **read-only branch session** opens in the right sidebar. The branch inherits the main session's context and streams its answer token by token — and **its answers never enter the main session's model context**.
6
+
7
+ Typical use: ask a follow-up question, look up a term, or explore an idea *alongside* the current conversation without breaking its flow.
8
+
9
+ | | |
10
+ | --- | --- |
11
+ | **Package name** (implementation) | `dsh-side-branch` |
12
+ | **UI name** (what you see) | **Side Ask** · 中文「临时会话」 |
13
+ | **Verified DSH version** | `0.1.5-rc.2` and `0.1.7-rc.2` (see "Archived sessions" below) |
14
+ | **Prerequisites** | Node.js ≥ 20 and `pnpm` on your `PATH` |
15
+ | **Runtime dependencies** | none — no build step, no third-party packages |
16
+ | **License** | MIT |
17
+
18
+ > **Two names, on purpose.** The package is `dsh-side-branch` ("branch session"); the feature is displayed in the UI as **Side Ask** (Chinese: 「临时会话」) — the sidebar tab title, panel hints and button tooltips all use that name. The package name describes the implementation, the UI name describes the job ("ask one temporary question"). Renaming one should not rename the other. The rest of this page says "branch" when it means the implementation.
19
+
20
+ <p align="center">
21
+ <img src="../assets/sidebar-guide.png" alt="The Side Ask entry in the right sidebar guide" width="720"><br>
22
+ <img src="../assets/selection-entry.png" alt="Selecting text in the main session, with the ask entry floating above the selection" width="720"><br>
23
+ <img src="../assets/panel-answering.png" alt="The right-hand panel answering: quote block, collapsible thinking row, usage row" width="720">
24
+ </p>
25
+
26
+ ## 💡 Why another side-chat plugin?
27
+
28
+ When this project was designed (Mar 2026) I surveyed 14 side-chat / bubble plugins in the DSH ecosystem and found **no single one that met all of these requirements at once**:
29
+
30
+ 1. **True context inheritance** — not a truncated summary.
31
+ 2. **Multi-turn follow-ups inside one branch** — not a one-shot bubble.
32
+ 3. **Token-by-token streaming**, including the reasoning process.
33
+ 4. **Lightweight and stable** — public APIs only, no bloat.
34
+ 5. **Read-only at the execution layer, and never destroying the prefix cache.**
35
+
36
+ The closest alternatives each traded something away. For example:
37
+
38
+ - [`@michengai/dsh-btw`](https://github.com/MichengAI/dsh-btw) is a one-shot side ask: each question is answered independently, with no follow-up within the same thread, and answers are not restored after a page refresh.
39
+ - [`dsh-side-chat-plus`](https://github.com/heartmove/dsh-side-chat) inherits the main conversation's toolset and permission preset by default, so its side chat is **read-write** — an agentic second chat rather than a read-only aside.
40
+ - [`dsh-sidenote`](https://github.com/g-yixuan/dsh-sidenote) is a full-featured side chat with one-click "reflow into the main session"; by design its conclusions are meant to be written back into the main conversation.
41
+ - [`@lukeknow0/dsh-side-chat`](https://github.com/Lukeknow0/dsh-side-chat) has an excellent architecture and a comparable read-only model, but its README states it is an MVP.
42
+
43
+ The sharpest split is **how "read-only" is implemented**. Comparable plugins usually enforce it in the *prompt layer* — removing the tools from the prompt via `toolFilter`, or restricting them with `tools.restrict`. Functionally that does produce read-only. But it also makes the branch's prompt prefix diverge from the main session's, **which invalidates every prefix-cache entry the parent session has accumulated**; the whole context is then recomputed on every turn.
44
+
45
+ This plugin enforces read-only in the **execution layer** instead (`ctx.tools.guard`, the public `dsh-tools` contract). The tools stay in the prompt; the model may attempt to call them, but execution is denied — so the prompt is byte-identical to the main session and the cache keeps hitting (measured at 97%–98%).
46
+
47
+ ## ✨ Core features
48
+
49
+ - **True branch, zero pollution.** The host half never writes anything back to the parent session. Whatever you ask — and whatever the branch answers — never appears in the main session's model context.
50
+ - **Prefix-cache hits stay intact.** The branch seeds itself from the main session's *completed-turn* prefix. Reusing the main session's model keeps hitting the existing prefix cache instead of rebuilding the prompt.
51
+ - **Read-only at the execution layer.** The security boundary does not depend on the prompt. `ctx.tools.guard` allows exactly six side-effect-free query tools (`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`); everything else — writing files, running commands, spawning child sessions — is denied at execution time.
52
+ - **Streaming that survives the tab.** Body text and reasoning stream token by token (reasoning lives in a collapsible "Thinking" row). Closing the browser tab does not interrupt the answer in the background; reopen and the result is there.
53
+ - **Persistent panel state.** Panel state is kept locally per *session + panel pane*. Idle side sessions go to sleep, release their live instance, keep their record, and wake silently on the next question.
54
+
55
+ ## 📦 Install
56
+
57
+ Either path works. **Restart DSH afterwards** — the client half is loaded at startup, so refreshing the browser is not enough.
58
+
59
+ From GitHub:
60
+
61
+ ```powershell
62
+ dsh plugin --profile web add github:wucl12/dsh-side-branch
63
+ ```
64
+
65
+ From npm:
66
+
67
+ ```powershell
68
+ dsh plugin --profile web add dsh-side-branch
69
+ ```
70
+
71
+ From a local directory (`link:` dependency — this installs a *link to that directory*, not a snapshot):
72
+
73
+ ```powershell
74
+ dsh plugin --profile web add C:\path\to\dsh-side-branch
75
+ ```
76
+
77
+ > After a local-directory install, **re-run `add` and restart DSH whenever you change the code** (`lib/client.js` and friends are only loaded at startup; a browser refresh is not enough). If you don't want to restart, install from GitHub or npm instead.
78
+
79
+ **If the path contains a space** (e.g. the repo lives under `Documents\`), `dsh plugin add` splits the path into two arguments and creates a broken link. Two ways out:
80
+
81
+ - move the repo to a path without spaces (e.g. `C:\dev\dsh-side-branch`) and run `add`; or
82
+ - **skip the command line**: write the dependency into the profile's `package.json` by hand, then run `install` (a path inside a file is not split):
83
+
84
+ ```jsonc
85
+ // <DSH_HOME>\profiles\web\package.json
86
+ "dependencies": { "dsh-side-branch": "link:C:/dev/dsh-side-branch" }
87
+ ```
88
+
89
+ ```powershell
90
+ dsh plugin --profile web install # this also adds it to dsh.profile.bundles
91
+ ```
92
+
93
+ Replace `web` with the profile you actually use. If the profile is not in the default location, set `DSH_HOME` first.
94
+
95
+ ### Let an agent install it for you
96
+
97
+ Paste the following into any agent that can run commands on your machine:
98
+
99
+ ```text
100
+ Install the DSH plugin dsh-side-branch into my web profile:
101
+ 1. Make sure pnpm is on PATH (dsh plugin is a forwarder to pnpm); install it first if missing.
102
+ 2. Make sure the repo's absolute path contains no spaces. If it does, use the "not through the command
103
+ line" route instead: add "dsh-side-branch": "link:<absolute repo path with forward slashes>" to the
104
+ dependencies in <DSH_HOME>\profiles\web\package.json, then run dsh plugin --profile web install.
105
+ 3. Otherwise: dsh plugin --profile web add <absolute repo path>
106
+ 4. Run dsh --profile web --dump-config and confirm dsh-side-branch appears in the output.
107
+ 5. Tell me the command to restart DSH, then stop and wait for me to restart it.
108
+ ```
109
+
110
+ ## 🚀 Usage
111
+
112
+ Select text in the main session's body: an ask entry appears above the selection. Click it, the right-hand panel opens, type your question and send. The selected text travels with the question as a quote.
113
+
114
+ There is a second entry point: a button on the right of the main session's composer opens the panel directly (it can be turned off in settings).
115
+
116
+ Inside the panel:
117
+
118
+ | Where | What it does |
119
+ | --- | --- |
120
+ | Composer | Ask; keep asking follow-ups in the same branch — that branch owns its own history |
121
+ | "Clear" (bottom left) | End the current branch; the next question starts a new one |
122
+ | Gear (bottom left) | Settings: the main-session quick-entry toggle |
123
+ | Model row | Pick a model and reasoning effort for this branch alone; defaults to following the main session |
124
+
125
+ Closing the browser tab does not interrupt an in-flight answer; come back to the panel and the result is still there.
126
+
127
+ ## 🧭 Behaviour notes
128
+
129
+ **The branch is a real branch.** At birth it reads the main session's completed-turn prefix as a seed, and inherits the main session's agent preset, tools and persona. The system prompt this branch sees is byte-identical to the main session's.
130
+
131
+ **Answers never flow back.** The host half never writes anything to the parent session. Nothing you ask, and nothing it answers, appears in the main session's model context.
132
+
133
+ **Prefix-cache hits continue.** Because the prompt prefix matches the main session's word for word, reusing the main session's model keeps hitting the existing prefix cache instead of recomputing it every time. A branch on a different model is a fresh prefix billed at full price; the plugin marks that in the usage row.
134
+
135
+ **Streaming, body and reasoning.** Answers are pushed token by token; reasoning sits in a collapsible "Thinking" row. A disconnecting client does not abort the background answer.
136
+
137
+ **Panel state is remembered.** Kept per *session + panel pane*, and survives a page refresh. After a side session has been idle for a while the plugin releases its live instance but keeps the record, and wakes it automatically on your next question.
138
+
139
+ **Settings live in the browser.** This plugin's namespace does not appear in the official settings page, and that is a deliberate trade-off: going through the official settings service would require the platform-internal `schemastery` package, which is not resolvable from the plugin workspace. Settings are therefore persisted in client `localStorage`, with the host only validating them.
140
+
141
+ **Archived sessions, and why the branch is un-archived before every turn.** A side session is a real DSH session, so without help it would show up as an extra row in the sidebar. To keep the list clean the plugin *archives* it the moment it is created — archiving hides a session from the grouping surfaces without deleting it.
142
+
143
+ Starting with DSH **`0.1.7-rc.1`** an archived session also becomes *unrunnable*: `dsh-api-session-controller` composes an `ArchivedSessionGate` whose `agent/pre-step` listener rejects any model step proposed for an archived session, and `dsh-agent-loop` ends that turn as `turn/end { reason: 'blocked' }` **without sending a model request**. A plugin that archives its own session would therefore block itself on every single turn.
144
+
145
+ The plugin's answer is a three-step cycle, all of it tolerant of failure:
146
+
147
+ 1. **On creation** — archive, so an idle branch is hidden.
148
+ 2. **Immediately before each turn is delivered** (`followup`) — call `workspaceRegistry.unarchiveSession()`, lifting the gate for that turn.
149
+ 3. **When the turn settles** — archive again, so an idle branch is hidden again. The registry refuses to archive a session that still has activity, so this step waits and retries on a bounded backoff; if it never succeeds the only consequence is that the branch stays visible in the sidebar.
150
+
151
+ `unarchiveSession()` only exists from DSH **`0.1.6-alpha.2`**; the call site is feature-detected, so on `0.1.5-rc.2` (no gate, no method) both extra steps are no-ops and behaviour is unchanged. This is what lets one published version serve both DSH lines.
152
+
153
+ ## 🛡️ How read-only is guaranteed
154
+
155
+ Read-only is enforced by the **execution layer**, not by asking the model nicely in a prompt.
156
+
157
+ Six tools are allow-listed:
158
+
159
+ ```text
160
+ read glob grep web_search web_fetch lsp
161
+ ```
162
+
163
+ The test is an **exact tool-name match, and it fails closed**. `dsh-tools`' `prepareExecution` runs the guard on every single execution, so anything not in that list of six is denied at execution time, including:
164
+
165
+ - `subagent`, `workflow`, `ralph` — they can open grandchild sessions, and a grandchild's context does not carry our guard;
166
+ - `bash`, `pwsh` — arbitrary commands;
167
+ - `write`, `edit`, `report` — writing files;
168
+ - `goal`, `jobs`, `cordis` — changing long-term goals, starting/stopping background jobs, mutating the running host;
169
+ - `run_code` — the channel PTC mode uses to wrap actions in program execution. Every SDK dispatch inside a PTC program goes through the same `prepareExecution`, so it is blocked twice;
170
+ - `mcp__*` — MCP tools register into the same tool table and are rejected by name like anything else.
171
+
172
+ One case has been tested live: the model **actually called** `subagent`, the guard denied it at the execution layer, no grandchild session started, and a decoy file's content and mtime were unchanged. `run_code` and MCP go through the same already-covered pipeline, but they have not each been exercised end to end.
173
+
174
+ If DSH renames a tool in the future, that tool falls into the *denied* side automatically rather than being silently allowed.
175
+
176
+ `session_query` is **not** on the list: it can read other sessions, which would turn "one branch of this session" into cross-session reading.
177
+
178
+ The allow-list is **fixed**. The plugin deliberately offers no "pick your own tools" setting: one less configurable layer is one less place where the read-only guarantee can be broken, and the list the model sees can never diverge from what the execution layer enforces. Narrowing the surface means editing that constant.
179
+
180
+ Note what was deliberately **not** done: removing tools from the prompt. That would fork the prompt prefix away from the main session and invalidate the whole prefix cache — which is exactly why read-only has to be enforced at the execution layer here.
181
+
182
+ ### "Read-only" is not "harmless"
183
+
184
+ "Read-only" means **no side effects**, not "no outside access". Of the six allow-listed tools:
185
+
186
+ - `web_fetch` and `web_search` are **network egress**. If the branch holds sensitive content, the model can send it out. Both the quoted source text and fetched tool output carry an anti-injection declaration (explicitly telling the model not to follow instructions found in them), but that is a prompt-layer constraint.
187
+ - `lsp` **starts language-server processes**.
188
+ - `read`, `glob` and `grep` can read **any path the process can reach**. There is no per-branch sandbox, so their reach is not limited to the current workspace.
189
+
190
+ These surfaces are **fixed**; the plugin has no switches to turn them off one by one. A narrower tool surface means editing the constant in code — which is precisely why it is deliberately non-configurable.
191
+
192
+ ### When the main session uses PTC mode
193
+
194
+ If the main session's agent preset uses `tool-presentation: mode: ptc` (the built-in `ptc` preset does), the tool surface is replaced by a generated SDK and the only entry point the model **can see** is `run_code`. `run_code` is never allow-listed, so such a branch cannot call a single tool and answers from inherited history plus whatever you quote.
195
+
196
+ The plugin detects this tool surface when opening a branch and switches to a guidance preamble stating that the branch offers no tools, so the model does not keep trying. Detection reads the parent's preset name (`ptc`). If the parent uses a **custom preset saved from the PTC template**, the preset name will not match and the guard is the fallback: the first `run_code` attempt is denied, after which the PTC preamble is used — the first round is wasted once.
197
+
198
+ Measured: in a PTC parent session the PTC preamble applied from the first round, the model reported that "the only visible entry in the tool surface is run_code", and `run_code` was actually called and denied at the execution layer, leaving the decoy file untouched.
199
+
200
+ ## ⚠️ Known limitations
201
+
202
+ - **A branch inherits a snapshot from the moment it is opened.** Turns the main session completes afterwards do not enter that branch. To include newer content, use "Clear" and start a new branch.
203
+ - **At most two side branches at once.** This comes from DSH's sidebar pane limit (one instance per tab type per pane, at most two panes), not from this plugin — but you will run into it.
204
+ - **The six allowed tools' own behaviour is not constrained by read-only.** The guard blocks *tools outside the list*; it does not restrict these six tools' network access, process spawning or file reading. See "Read-only is not harmless" above.
205
+ - **"Clear" is irreversible for the panel history.** After clearing, that branch is no longer visible in the panel and cannot be brought back. The archived side-session record is still on disk, but the official API cannot delete it and it never appears in any session list.
206
+ - Settings live in browser `localStorage`: switching browsers or clearing site data resets them. Currently there is only one setting (the main-session quick-entry toggle).
207
+ - The tool allow-list is not configurable. Narrowing it means editing code; there is no per-tool switch.
208
+ - After a plugin reload, the table of "sleeping" sessions is gone with memory, and old session ids report that a new branch was started (the client creates one automatically, so nothing hangs). **Note the two paths**: going idle past the threshold only puts the side session to *sleep* — the record stays and the next question **wakes it silently**, without even a divider; only a **plugin reload** loses the record and starts a new branch. Your question is still answered normally and you do not have to retype it — that branch's history just no longer connects.
209
+ - Each branch has a context ceiling. On reaching it, the plugin refuses to send and shows the actual usage instead of silently dropping the oldest turns.
210
+ - With a `ptc` main session the branch **cannot use a single tool** (the only visible entry, `run_code`, is always denied). A custom preset saved from the PTC template also defeats the opening-time detection, wasting the first round.
211
+ - After being denied by the guard, the model often **retries the same tool**, wasting a round. This is independent of PTC mode and was observed in both.
212
+ - Verified on DSH `0.1.5-rc.2` (before the archive gate) and `0.1.7-rc.2` (with it). `0.1.6-alpha.2` was checked by reading its published packages only — it already has `unarchiveSession` and does **not** yet have `ArchivedSessionGate`, so the feature-detected path keeps it working — but no end-to-end run was done there.
213
+ - The MCP tool conclusion comes from reading the code path, not from a live test (`run_code` was tested live).
214
+ - The effect of the branch preamble on model behaviour has not been quantified. It asks the model to say "this needs the main session" when it would have to read a file to answer; whether that makes the model overly conservative needs human judgement.
215
+
216
+ ## 📚 More documentation
217
+
218
+ - [`README.md`](../README.md) / [`README.zh-CN.md`](./README.zh-CN.md): the short overview, English and Chinese.
219
+ - [`AGENTS.md`](../AGENTS.md) / [`AGENTS.en.md`](./AGENTS.en.md): the architecture contract and hard constraints, written for AI agents — read it before any secondary development (Chinese / English).
220
+ - [`SECURITY.md`](../SECURITY.md) / [`SECURITY.zh-CN.md`](./SECURITY.zh-CN.md): the scope of the read-only commitment and the vulnerability reporting channel (English / Chinese).
221
+ - [`CHANGELOG.md`](./CHANGELOG.md): release history, bilingual.
222
+
223
+ ## 📄 License
224
+
225
+ [MIT](../LICENSE)
@@ -0,0 +1,227 @@
1
+ # DSH 侧枝会话 — 临时会话 (Side Ask)
2
+
3
+ [English](./TECHNICAL.md) · **简体中文** · [README](./README.zh-CN.md)
4
+
5
+ 在主会话里选中一段文字,右栏就会打开一条**只读分支子会话**。它继承主会话已有的上下文,逐词流式回答——而**它的答案绝不会进入主会话的模型上下文**。
6
+
7
+ 典型场景:针对当前对话做延伸追问、查词或发散探索,同时不打断主会话的连贯性。
8
+
9
+ | | |
10
+ | --- | --- |
11
+ | **包名**(实现) | `dsh-side-branch` |
12
+ | **界面显示名** | **临时会话**(英文界面 `Side Ask`) |
13
+ | **已验证的 DSH 版本** | `0.1.5-rc.2` 与 `0.1.7-rc.2`(见下文「归档与解锁」) |
14
+ | **前置要求** | Node.js ≥ 20,且 `PATH` 上有 `pnpm` |
15
+ | **运行时依赖** | 无——零第三方依赖,无构建步骤 |
16
+ | **许可证** | MIT |
17
+
18
+ > **两个名字是刻意的。** 插件叫 `dsh-side-branch`(侧枝会话);这个功能在**界面里**显示为「**临时会话**」(英文界面 `Side Ask`)——侧栏 tab 标题、面板提示、按钮悬停提示都用这个名字。包名描述实现(只读分支),UI 名描述用途(临时问一句),改名不该动用户已经认得的东西。本页其余部分用「分支」这个词描述它的实现语义。
19
+
20
+ <p align="center">
21
+ <img src="../assets/sidebar-guide.png" alt="右侧栏引导页里的「临时会话」入口" width="720"><br>
22
+ <img src="../assets/selection-entry.png" alt="在主会话里选文字后浮出的询问入口" width="720"><br>
23
+ <img src="../assets/panel-answering.png" alt="右栏面板正在回答:引用块、可折叠的「思考」行、用量行都在位" width="720">
24
+ </p>
25
+
26
+ ## 💡 为什么再造一个侧边会话插件?
27
+
28
+ 设计这个项目时(2026 年 3 月),我调研了 DSH 生态里的 14 个侧边会话/气泡类插件,发现**没有一款能同时满足下面全部硬需求**:
29
+
30
+ 1. **真继承上下文**——不是截断后的摘要。
31
+ 2. **同一段内支持多轮追问**——不是一次性气泡。
32
+ 3. **逐词流式输出**,包括推理过程。
33
+ 4. **轻量稳定**——只用官方公开 API,不臃肿。
34
+ 5. **只读拦在执行层,且绝不破坏前缀缓存**。
35
+
36
+ 当时最接近的几款方案各有取舍,例如:
37
+
38
+ - [`@michengai/dsh-btw`](https://github.com/MichengAI/dsh-btw) 是一次性旁问:每个问题独立回答,同一话题内无法追问,刷新页面后答案也不再保留。
39
+ - [`dsh-side-chat-plus`](https://github.com/heartmove/dsh-side-chat) 默认继承主会话的工具集与权限预设,因此它的侧边聊天是**可写**的——是「第二个 agent 会话」,而不是只读旁问。
40
+ - [`dsh-sidenote`](https://github.com/g-yixuan/dsh-sidenote) 是功能完整的侧边聊天,带一键「回流主会话」;按设计它的结论就是要写回主会话的。
41
+ - [`@lukeknow0/dsh-side-chat`](https://github.com/Lukeknow0/dsh-side-chat) 架构优秀、只读模型与本插件相近,但其 README 自述仍是 MVP。
42
+
43
+ 最大的分歧点在**「只读」的实现方式**。同类插件普遍在**提示词层**实现:用 `toolFilter` 把工具从提示词里删掉,或用 `tools.restrict` 限制工具。功能上确实达到只读,但代价是侧会话的提示词前缀与主会话分叉,**父会话已经积累的前缀缓存全部失效**,每轮都要重算整个上下文。
44
+
45
+ 本插件把只读拦在**执行层**(`ctx.tools.guard`,`dsh-tools` 的公开契约):工具仍留在提示词里,模型可以尝试调用,但执行被拒——于是提示词与主会话逐字一致,缓存照旧命中(实测 97%–98%)。
46
+
47
+ ## ✨ 核心特性
48
+
49
+ - **真分支,零污染。** 宿主半绝不向父会话写入任何内容。你在分支里问过什么、得到什么答案,主会话的模型上下文里都不会出现。
50
+ - **继续命中前缀缓存。** 分支出生时读取主会话**已完成回合**的前缀作为种子。沿用主会话模型时,继续命中原有前缀缓存,无需重算提示词。
51
+ - **执行层只读隔离。** 安全边界不依赖提示词。由底层 `ctx.tools.guard` 精确放行六个无副作用的查询类工具(`read`、`glob`、`grep`、`web_search`、`web_fetch`、`lsp`),其余一切——写文件、执行命令、开孙子会话——都在执行层被拒。
52
+ - **关掉标签也不断的流式输出。** 正文与推理过程逐词推进(推理放在可折叠的「思考」行里)。关闭浏览器标签不会中断后台回答,重新打开结果还在。
53
+ - **面板状态会记住。** 按「会话 + 面板格」本地持久化。侧会话闲置后释放活动实例、保留记录,下次提问静默唤醒。
54
+
55
+ ## 📦 安装
56
+
57
+ 任选一种。安装后**必须重启 DSH**——客户端半在启动时装载,只刷新浏览器不够。
58
+
59
+ 从 GitHub 安装:
60
+
61
+ ```powershell
62
+ dsh plugin --profile web add github:wucl12/dsh-side-branch
63
+ ```
64
+
65
+ 从 npm 安装:
66
+
67
+ ```powershell
68
+ dsh plugin --profile web add dsh-side-branch
69
+ ```
70
+
71
+ 从本地目录安装(`link:` 依赖,装的是**指向该目录的链接**,不是快照):
72
+
73
+ ```powershell
74
+ dsh plugin --profile web add C:\path\to\dsh-side-branch
75
+ ```
76
+
77
+ > 本地目录安装后,**改了代码就要重新 `add` 一次并重启 DSH**(`lib/client.js` 这类客户端代码只在启动时装载,
78
+ > 刷新浏览器不够)。不想重启就改用 GitHub 或 npm 安装。
79
+
80
+ **路径含空格时**(例如仓库放在 `Documents\` 下),`dsh plugin add` 会把路径拆成两段、装出错误的链接。两个办法,任选一个:
81
+
82
+ - 把仓库放到不含空格的路径(如 `C:\dev\dsh-side-branch`)再 `add`;或
83
+ - **不经过命令行**:在 profile 的 `package.json` 里手写一条依赖,再跑 `install`(路径写在文件里,不会被拆):
84
+
85
+ ```jsonc
86
+ // <DSH_HOME>\profiles\web\package.json
87
+ "dependencies": { "dsh-side-branch": "link:C:/dev/dsh-side-branch" }
88
+ ```
89
+
90
+ ```powershell
91
+ dsh plugin --profile web install # 装完会自动把它加进 dsh.profile.bundles
92
+ ```
93
+
94
+ 把 `web` 换成你实际使用的 profile 名。若 profile 不在默认位置,先设置 `DSH_HOME`。
95
+
96
+ ### 让 agent 帮你装
97
+
98
+ 把下面这段发给任意能执行本机命令的 agent:
99
+
100
+ ```text
101
+ 请把本机的 DSH 插件 dsh-side-branch 装到 web profile:
102
+ 1. 确认本机 PATH 上有 pnpm(dsh plugin 是 pnpm 的转发器);没有就先装。
103
+ 2. 确认仓库所在目录的绝对路径不含空格。若含空格,改用"不经过命令行"的办法:
104
+ 在 <DSH_HOME>\profiles\web\package.json 的 dependencies 里加一行
105
+ "dsh-side-branch": "link:<仓库绝对路径的正斜杠形式>",然后跑 dsh plugin --profile web install。
106
+ 3. 不含空格时:dsh plugin --profile web add <仓库绝对路径>
107
+ 4. 执行 dsh --profile web --dump-config,确认输出里出现 dsh-side-branch 这一行。
108
+ 5. 告诉我重启 DSH 的命令,然后停在那里等我重启。
109
+ ```
110
+
111
+ ## 🚀 使用
112
+
113
+ 在主会话正文里选中文字,选区上方出现询问入口。点它打开右栏面板,输入问题并发送——选中的原文会作为引用一起交给分支。
114
+
115
+ 主会话输入框右侧另有一个入口按钮,点它可以直接打开面板(可在设置里关掉)。
116
+
117
+ 面板内的操作:
118
+
119
+ | 位置 | 作用 |
120
+ | --- | --- |
121
+ | 输入框 | 提问;同一段内可以继续追问,历史由这段分支自己持有 |
122
+ | 底部左侧「清空」 | 结束当前这一段,下次提问另起一段 |
123
+ | 底部左侧齿轮 | 打开设置:主会话快速入口按钮的开关 |
124
+ | 模型选择行 | 为这一段单独指定模型与推理等级,默认跟随主会话 |
125
+
126
+ 关闭浏览器标签不会中断正在进行的回答;回到面板仍能看到结果。
127
+
128
+ ## 🧭 行为要点
129
+
130
+ **分支是真分支。** 出生时读取主会话已完成的回合前缀作为种子,并继承主会话的 agent preset、工具与 persona。因此这条分支看到的系统提示词与主会话逐字一致。
131
+
132
+ **答案不回流。** 宿主半从不向父会话写入任何内容。你在分支里问过什么、得到什么答案,主会话的模型上下文里都不会出现。
133
+
134
+ **继续命中前缀缓存。** 因为提示词前缀与主会话逐字一致,沿用主会话模型时仍能命中原有的前缀缓存,而不是每次重算。换模型的那一段是全新前缀,按全价计费,插件会在用量行标注。
135
+
136
+ **流式输出正文与推理。** 回答逐词推送;推理过程放在可折叠的「思考」行里。客户端断开不会中止后台回答。
137
+
138
+ **面板状态会记住。** 按「会话 + 面板格」保留,刷新页面不丢。侧会话闲置一段时间后释放活动实例但保留记录,下次提问自动唤醒。
139
+
140
+ **设置存在浏览器本地。** 官方设置页里看不到本插件的命名空间,这是有意的取舍:改为走官方设置服务需要平台内置包 `schemastery`,插件工作区解析不到它,所以设置改由客户端 `localStorage` 持久化,宿主只做校验。
141
+
142
+ **归档与解锁:为什么每一轮开跑前都要取消归档。** 侧会话是一条**真实的 DSH 会话**,不处理的话它会作为一行出现在左侧边栏。为了让列表干净,插件在建段那一刻就把它**归档**——归档会把会话从各个分组界面里藏起来,但不删除它。
143
+
144
+ 从 DSH **`0.1.7-rc.1`** 起,已归档的会话同时变成**不可运行**:`dsh-api-session-controller` 组合的 `ArchivedSessionGate` 会在 `agent/pre-step` 上拒掉已归档会话的任何模型步,`dsh-agent-loop` 随即把那一轮收成 `turn/end { reason: 'blocked' }`,**连模型请求都不会发出**。于是"自己归档自己"的插件会每一轮都把自己锁死。
145
+
146
+ 插件的做法是三步循环,每一步都容忍失败:
147
+
148
+ 1. **建段时**归档 —— 空闲的分支不出现在会话列表里;
149
+ 2. **每轮真的投递之前**(`followup` 之前)调 `workspaceRegistry.unarchiveSession()`,为这一轮解除闸门;
150
+ 3. **该轮结算后**再归档 —— 空闲的分支重新藏起来。注册表会拒绝归档"仍有活动"的会话,所以这一步按有界退避重试;即使始终失败,唯一后果也只是这段分支留在侧栏可见。
151
+
152
+ `unarchiveSession()` 是 DSH **`0.1.6-alpha.2`** 才有的方法,调用点做了**特性探测**:在 `0.1.5-rc.2` 上(既没有这个方法、也没有那道闸)第 2、3 步都是空操作,行为与旧版完全一致。这正是同一份发布版本能同时服务两条 DSH 版本线的支点。
153
+
154
+ ## 🛡️ 只读是怎么保证的
155
+
156
+ 只读由**执行层**保证,不依赖提示词请求模型配合。
157
+
158
+ 白名单放行六个工具:
159
+
160
+ ```text
161
+ read glob grep web_search web_fetch lsp
162
+ ```
163
+
164
+ 判定按**工具名精确匹配,方向是 fail-closed**。`dsh-tools` 的 `prepareExecution` 对每一次执行都跑一遍 guard,任何不在上面六个名字里的工具都在执行层被拒,包括:
165
+
166
+ - `subagent`、`workflow`、`ralph` —— 它们能开出孙子会话,而孙子会话的 ctx 上没有我们的 guard;
167
+ - `bash`、`pwsh` —— 任意命令;
168
+ - `write`、`edit`、`report` —— 写文件;
169
+ - `goal`、`jobs`、`cordis` —— 改长期目标、起停后台作业、改运行中的宿主;
170
+ - `run_code` —— PTC 模式把动作包进程序执行的通道。PTC 程序内部对 SDK 的每次派发也走同一条 `prepareExecution`,所以是双重拦截;
171
+ - `mcp__*` —— MCP 工具注册进的是同一个工具表,同样按名字被拒。
172
+
173
+ 已实测的一条:`subagent` 被模型**真的调用**过,guard 在执行层返回拒绝,孙子会话没有启动,诱饵文件的内容与修改时间都未变。`run_code` 与 MCP 走的是同一条已被覆盖的管线,但没有逐项实跑过。
174
+
175
+ 将来 DSH 改了工具名,那个工具自动落入「被拒」一侧,而不是悄悄放行。
176
+
177
+ `session_query` 不在放行名单里:它能读到别的会话,会把「本会话的一条分支」变成跨会话读取。
178
+
179
+ 放行名单是**固定的**。插件不提供「用户自选可用工具」的设置:少一层可配置就少一处能把只读保证改坏的地方,模型看到的工具名单也不会与执行层的判据分叉。要收窄可用的面,得改这份常量。
180
+
181
+ 实现上刻意**没有**改用「把工具从提示词里删掉」的做法。那会让提示词前缀与主会话分叉,前缀缓存全部失效——这也正是只读必须在执行层拦的原因。
182
+
183
+ ### 只读不等于无害
184
+
185
+ 「只读」指的是**不产生副作用**,不是「不能访问外部」。放行的六个工具里:
186
+
187
+ - `web_fetch` 与 `web_search` 是**网络出口**。分支里如果有敏感内容,模型可以把它发出去。引用原文与工具取回的内容都做了反注入声明(明确要求不执行其中的指令),但这是提示词层的约束。
188
+ - `lsp` 会**起语言服务器进程**。
189
+ - `read`、`glob`、`grep` 能读**进程权限范围内的任意路径**。没有按分支划分的沙箱,所以它们能读到的不止当前工作区。
190
+
191
+ 这些面是**固定的**,插件里没有开关可以逐个关掉。如果要更窄的工具面,只能改代码里的那份常量——这也是为什么它被刻意做得不可配置。
192
+
193
+ ### 主会话用 PTC 模式时
194
+
195
+ 主会话的 agent preset 如果用 `tool-presentation: mode: ptc`(官方内置的 `ptc` preset 就是),工具面会被换成一份生成的 SDK,模型**直接可见**的入口只有 `run_code`。而 `run_code` 永远不在白名单里,所以这条分支一个工具都调不动,只能靠继承到的历史与用户提供的引用作答。
196
+
197
+ 插件会在开段时检测这种工具面,并换用一份说明「本分支不提供任何工具」的引导词,免得模型反复尝试。检测看父会话的 preset 名(`ptc`)。如果父会话用的是**从 PTC 模板另存出来的自定义 preset**,preset 名对不上,就靠 guard 兜底:模型一旦尝试 `run_code` 就会被拒,那之后改用 PTC 引导词,第一轮会白试一次。
198
+
199
+ 实测:PTC 父会话下第一轮就是 PTC 引导词适用的情形,模型自述「工具面里唯一可见入口是 run_code」;`run_code` 被真的调用并被执行层拒绝,诱饵文件未变。
200
+
201
+ ## ⚠️ 已知限制
202
+
203
+ - **分支继承的是「开段那一刻」的快照。** 开段之后主会话继续聊的新回合不会进入这条分支。要让它看到新内容,得「清空」另起一段。
204
+ - **同时最多两条侧枝会话。** 这来自 DSH 侧栏的分栏上限(同一 tab 类型每个分栏最多一颗、最多两个分栏),不是本插件的限制,但用起来会撞到。
205
+ - **`web_fetch`、`web_search`、`lsp`、`read`、`glob`、`grep` 本身的行为不受只读约束。** guard 拦的是「名单外的工具」,不是这六个工具的网络访问、起进程与读文件能力。详见上面「只读不等于无害」。
206
+ - **「清空」对界面历史不可逆。** 清空之后这一段在面板里再也看不到、也回不去。磁盘上的归档侧会话记录仍在,但官方 API 删不掉它,它也不会出现在任何会话列表里。
207
+ - 设置存在浏览器 `localStorage`:换浏览器或清站点数据会回到默认值。目前设置只有一项(主会话快速入口按钮的开关)。
208
+ - 工具放行名单不可配置。想收窄可用工具只能改代码,没有针对单个工具的开关。
209
+ - 插件重载后,「睡着」的会话表随内存消失,旧会话 id 会报告已另起一段(客户端会自动新建,不会卡住)。
210
+ **注意区分两种路径**:闲置到点只是让侧会话「睡觉」,记录还在,下次提问**静默唤醒**、连分隔线都不出现;只有**插件重载**才会丢记录、另起一段,此时问题照常得到回答、不用重新输入,只是那段历史接不上。
211
+ - 每段有上下文上限。到达上限时拒绝发送并显示实际用量,不会静默丢弃最早的轮次。
212
+ - 主会话用 `ptc` preset 时,这条分支**一个工具都用不了**(唯一可见入口 `run_code` 永远被拒)。用 PTC 模板另存出来的自定义 preset 还会让开段判定漏判,第一轮白试一次。
213
+ - 模型被 guard 拒绝后往往会**再试一次**同一个工具,白花一轮。这与主会话是否 PTC 无关,实测两种模式下都会发生。
214
+ - 已在 DSH `0.1.5-rc.2`(尚无归档闸)与 `0.1.7-rc.2`(已有归档闸)上验证。`0.1.6-alpha.2` 只核对了它已发布的包:它已有 `unarchiveSession`、尚未有 `ArchivedSessionGate`,特性探测的分支足以让它照常工作,但**没有在那里跑过端到端**。
215
+ - MCP 工具的拦截结论来自代码路径核对,没有实测(`run_code` 已实测)。
216
+ - 分支引导词对模型行为的影响没有量化。它要求模型在需要读文件才能得出结论时说明「这需要回主会话做」,是否会让模型过度保守需要人工评判。
217
+
218
+ ## 📚 更多文档
219
+
220
+ - [`README.md`](../README.md) / [`README.zh-CN.md`](./README.zh-CN.md):精简版说明(英文 / 中文)。
221
+ - [`AGENTS.md`](../AGENTS.md) / [`AGENTS.en.md`](./AGENTS.en.md):专门写给 AI Agent 看的架构契约与硬约束(人类开发者二次开发前也建议先读;中文 / 英文)。
222
+ - [`SECURITY.md`](../SECURITY.md) / [`SECURITY.zh-CN.md`](./SECURITY.zh-CN.md):只读承诺的范围与安全问题报告渠道(英文 / 中文)。
223
+ - [`CHANGELOG.md`](./CHANGELOG.md):版本更新历史。
224
+
225
+ ## 📄 许可证
226
+
227
+ [MIT](../LICENSE)
package/docs/assets.md ADDED
@@ -0,0 +1,11 @@
1
+ # Screenshots / 截图
2
+
3
+ Referenced by [`README.md`](../README.md) (English) and [`README.zh-CN.md`](./README.zh-CN.md) (中文)。
4
+
5
+ | File | What it shows | 说明 |
6
+ | --- | --- | --- |
7
+ | [`sidebar-guide.png`](../assets/sidebar-guide.png) | The **Side Ask** entry in the right sidebar guide. | 右侧栏引导页里的「临时会话」入口。 |
8
+ | [`selection-entry.png`](../assets/selection-entry.png) | The ask entry that appears above a text selection in the main session. | 在主会话里选中文字后浮出的询问入口。 |
9
+ | [`panel-answering.png`](../assets/panel-answering.png) | The panel mid-answer: quote block, collapsible "Thinking" row and usage row. | 面板正在回答:引用块、可折叠的「思考」行、用量行。 |
10
+
11
+ Files live in `assets/`. To regenerate a screenshot, replace the file in place and keep the filename — the docs reference these paths, and the screenshots ship in the npm tarball (`package.json` → `files` → `assets`).