dsh-session-messaging 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0] - 2026-10-18
8
+
9
+ ### Added
10
+
11
+ - A client-side sidebar panel, “Session Messaging”, with:
12
+ - a cross-directory toggle persisted through the dsh settings form
13
+ (`sameDirectoryOnly` is now a volatile config field);
14
+ - an npm update check against the published package;
15
+ - links to npm, GitHub, and the author's blog.
16
+
17
+ ### Changed
18
+
19
+ - The `sameDirectoryOnly` config field is volatile, so the panel can edit it
20
+ at runtime without editing the profile patch by hand.
21
+
22
+ ## [0.2.0] - 2026-10-18
23
+
24
+ ### Changed
25
+
26
+ - **Cross-directory peers are allowed by default.** `session_peers` and
27
+ `session_send` now accept sessions whose working directory differs from the
28
+ sender's. Set `sameDirectoryOnly: true` in the plugin configuration to
29
+ restore the previous path restriction.
30
+ - The change is a minor release because it changes default delivery behavior:
31
+ review the peer-content trust boundary before enabling it in shared hosts.
32
+
33
+ ### Tests
34
+
35
+ - Integration coverage now exercises default cross-directory discovery and
36
+ delivery, plus an explicit `sameDirectoryOnly: true` policy test.
37
+
7
38
  ## [0.1.2] - 2026-10-18
8
39
 
9
40
  ### Fixed
package/README.md CHANGED
@@ -3,11 +3,12 @@
3
3
  Cross-session messaging for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh): let **user-launched sessions in the same workspace message each other** to coordinate tasks, declared file-write scopes, and conflicts — without turning them into parent/child subagents.
4
4
 
5
5
  [![CI](https://github.com/XBXyftx/dsh-session-messaging/actions/workflows/ci.yml/badge.svg)](https://github.com/XBXyftx/dsh-session-messaging/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/dsh-session-messaging.svg)](https://www.npmjs.com/package/dsh-session-messaging)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
8
 
8
9
  **English** | [简体中文](README.zh-CN.md)
9
10
 
10
- > Status: early preview (`0.1.0`). Targets dsh `0.2.1-rc.2`. dsh plugin APIs are pre-stable, so expect breaking changes on upstream upgrades.
11
+ > Status: early preview (`0.2.0`). Targets dsh `0.2.1-rc.2`. dsh plugin APIs are pre-stable, so expect breaking changes on upstream upgrades.
11
12
 
12
13
  ## Why
13
14
 
@@ -39,16 +40,23 @@ Two user-launched sessions used the plugin to coordinate a real fix. One session
39
40
 
40
41
  See [CHANGELOG.md](CHANGELOG.md) for the `0.1.2` entry.
41
42
 
43
+ ### Settings panel
44
+
45
+ ![Session Messaging settings panel](docs/assets/panel.webp)
46
+
47
+ The sidebar entry opens a small panel: toggle cross-directory messaging (persisted through the dsh settings form), check the latest npm version, and open npm, GitHub, or the author's blog. After installing or upgrading the bundle, refresh the Web UI once so the client half joins the module graph.
48
+
42
49
  ## Features
43
50
 
44
51
  - `session_peers` — list live, same-directory, top-level sessions and their reception state.
45
52
  - `session_send` — relay a coordination message, optionally declaring `writeScopes` and replying to a message.
46
53
  - `session_reception` — pause/resume receiving peer messages for the current session and declare its write scopes.
54
+ - **Sidebar panel** — a “Session Messaging” entry in the left sidebar with a cross-directory toggle, update check, and author links.
47
55
  - Messages appear inline in the target conversation, attributed to the sending session — no separate inbox UI.
48
56
  - Advisory **write-scope conflict detection**: if a peer declared an overlapping path, the receipt and the message envelope carry a `conflict-warning`.
49
57
  - No new Session event types and no `SESSION_FORMAT_VERSION` change: the relay is a durable `user/message` with a declared `session-message` source.
50
58
  - Bilingual plugin-card metadata and icons (English / 简体中文) for the dsh plugin manager.
51
- - Bounded: message size, per-sender→target rate limit, same-directory scope, top-level-only, live-only, self-send rejected.
59
+ - Bounded: message size, per-sender→target rate limit, top-level-only, live-only, self-send rejected. Cross-directory peers are allowed by default; `sameDirectoryOnly: true` restricts to one working directory.
52
60
 
53
61
  ## How it works
54
62
 
@@ -73,13 +81,21 @@ The envelope always states that the message comes from another session, not the
73
81
 
74
82
  ## Install
75
83
 
84
+ ### From npm (recommended)
85
+
86
+ ```sh
87
+ dsh plugin --profile <profile> add dsh-session-messaging
88
+ ```
89
+
90
+ The package is published as [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging) (latest `0.3.0`). For library use: `npm install dsh-session-messaging`.
91
+
76
92
  ### From GitHub
77
93
 
78
94
  ```sh
79
95
  dsh plugin --profile <profile> add github:XBXyftx/dsh-session-messaging
80
96
  ```
81
97
 
82
- The repository ships the built `lib/` output, so installing from git does **not** run a build. If you prefer a registry, `npm publish` from this repository produces the same package (`dsh-session-messaging`).
98
+ The repository ships the built `lib/` output, so a git install does not run a build.
83
99
 
84
100
  ### From a local checkout
85
101
 
@@ -130,7 +146,7 @@ The bundle row accepts validated configuration; every field has a default:
130
146
  maxMessageChars: 4000
131
147
  maxMessagesPerMinute: 12
132
148
  requireTargetOptIn: false # sessions are open to peers by default
133
- sameDirectoryOnly: true
149
+ sameDirectoryOnly: false
134
150
  allowSelfSend: false
135
151
  ```
136
152
 
@@ -141,7 +157,7 @@ The bundle row accepts validated configuration; every field has a default:
141
157
  - **Same process only.** Discovery uses `ctx.agents`; a session that is not live cannot receive a message in this version. Cold-session delivery and cross-process/cross-host messaging are not implemented.
142
158
  - **Advisory scopes, not locks.** dsh has optimistic filesystem version guards (`FS_STALE_VERSION`) but no file locking. `writeScopes` and `conflict-warning` are coordination aids; Bash, formatters, and code generators bypass them.
143
159
  - **In-memory state.** Reception pauses and declared scopes live for the plugin's lifetime and reset on reload/restart.
144
- - **Same working directory by default.** Set `sameDirectoryOnly: false` only if you understand cross-project implications.
160
+ - **Cross-directory by default.** Sessions in different working directories can message each other. Set `sameDirectoryOnly: true` to restore a path boundary; peer content remains untrusted either way.
145
161
  - **No GUI extension.** Messages render as ordinary user messages; a dedicated title/icon for the `session-message` source would require a client plugin or an upstream `ui-chat` change.
146
162
 
147
163
  ## Development
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](README.md) | **简体中文**
6
6
 
7
- > 状态:早期预览(`0.1.0`),适配 dsh `0.2.1-rc.2`。dsh 插件 API 仍为 pre-stable,上游升级可能带来破坏性变更。
7
+ > 状态:早期预览(`0.2.0`),适配 dsh `0.2.1-rc.2`。dsh 插件 API 仍为 pre-stable,上游升级可能带来破坏性变更。
8
8
 
9
9
  ## 为什么需要
10
10
 
@@ -36,16 +36,23 @@ macOS 桌面客户端(`0.2.1-rc.2`)真实运行:另一个用户启动的
36
36
 
37
37
  `0.1.2` 的变更见 [CHANGELOG.md](CHANGELOG.md)。
38
38
 
39
+ ### 设置面板
40
+
41
+ ![跨会话消息设置面板](docs/assets/panel.webp)
42
+
43
+ 侧边栏入口打开一个设置面板:切换跨目录消息(通过 dsh 设置表单持久化)、检查 npm 最新版本,并打开 npm / GitHub / 作者博客。安装或升级 bundle 后刷新一次 Web UI,客户端模块才会进入模块图。
44
+
39
45
  ## 功能
40
46
 
41
47
  - `session_peers`:列出同目录、live 的顶层会话及其接收状态。
42
48
  - `session_send`:发送协调消息,可附带 `writeScopes` 与 `replyTo`。
43
49
  - `session_reception`:暂停/恢复本会话接收对等消息,并声明写范围。
50
+ - **侧边栏面板**:左侧“跨会话消息”入口,提供跨目录开关、版本更新检查与作者链接。
44
51
  - 消息直接内联显示在目标会话中,来源归属于发送会话,不需要独立收件箱。
45
52
  - 建议性**写范围冲突检测**:与 peer 声明重叠时,回执与消息信封带 `conflict-warning`。
46
53
  - 不新增 Session 事件类型、不改 `SESSION_FORMAT_VERSION`:消息就是带 `session-message` source 的 `user/message`。
47
54
  - 插件管理卡片提供中英双语标题、简介与图标(English / 简体中文)。
48
- - 有边界:长度上限、每 sender→target 限频、同目录、仅顶层、仅 live、拒绝自发送。
55
+ - 有边界:长度上限、每 sender→target 限频、仅顶层、仅 live、拒绝自发送。默认允许跨目录对等会话;`sameDirectoryOnly: true` 可收紧为同工作目录。
49
56
 
50
57
  ## 工作原理
51
58
 
@@ -70,13 +77,21 @@ macOS 桌面客户端(`0.2.1-rc.2`)真实运行:另一个用户启动的
70
77
 
71
78
  ## 安装
72
79
 
80
+ ### 从 npm 安装(推荐)
81
+
82
+ ```sh
83
+ dsh plugin --profile <profile> add dsh-session-messaging
84
+ ```
85
+
86
+ 包已发布为 [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging)(最新 `0.3.0`)。库用法:`npm install dsh-session-messaging`。
87
+
73
88
  ### 从 GitHub 安装
74
89
 
75
90
  ```sh
76
91
  dsh plugin --profile <profile> add github:XBXyftx/dsh-session-messaging
77
92
  ```
78
93
 
79
- 仓库包含构建好的 `lib/`,从 git 安装**不需要**额外构建。发布到 npm 后也可用包名 `dsh-session-messaging` 安装。
94
+ 仓库包含构建好的 `lib/`,从 git 安装不需要额外构建。
80
95
 
81
96
  ### 本地目录安装
82
97
 
@@ -125,7 +140,7 @@ dsh plugin --profile <profile> add /absolute/path/to/dsh-session-messaging
125
140
  maxMessageChars: 4000
126
141
  maxMessagesPerMinute: 12
127
142
  requireTargetOptIn: false # 默认对 peer 开放
128
- sameDirectoryOnly: true
143
+ sameDirectoryOnly: false
129
144
  allowSelfSend: false
130
145
  ```
131
146
 
@@ -136,7 +151,7 @@ dsh plugin --profile <profile> add /absolute/path/to/dsh-session-messaging
136
151
  - **仅同进程**:发现依赖 `ctx.agents`,非 live 会话本版无法接收;冷会话投递与跨进程/跨机器通信未实现。
137
152
  - **写范围只是建议,不是锁**:dsh 只有乐观版本守卫(`FS_STALE_VERSION`),没有文件锁;Bash、格式化器、代码生成器会绕过写范围提示。
138
153
  - **状态在内存**:暂停接收与写范围声明随插件生命周期存在,重载/重启后重置。
139
- - **默认同目录**:除非理解跨项目影响,否则不要关闭 `sameDirectoryOnly`。
154
+ - **默认允许跨目录**:不同工作目录的会话也可以互发消息。设置 `sameDirectoryOnly: true` 可恢复路径边界;无论是否限制,peer 内容都不可信。
140
155
  - **无 GUI 扩展**:消息按普通 user 消息显示;`session-message` 的专属标题/图标需要 client 插件或上游 `ui-chat` 改动。
141
156
 
142
157
  ## 开发
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Browser half: a sidebar entry and its panel for session-messaging settings,
3
+ * update check, and author links. Built as a ModuleLoader factory (see the
4
+ * dsh plugin UI template); the Host half carries the service and tools.
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-session-messaging',
8
+ factory(require) {
9
+ const React = require('react')
10
+ const h = React.createElement
11
+ const NS = 'session-messaging'
12
+ const PANEL_ID = 'session-messaging'
13
+ const PACKAGE_NAME = 'dsh-session-messaging'
14
+ const FALLBACK_VERSION = '0.3.0'
15
+ const NPM_URL = 'https://www.npmjs.com/package/dsh-session-messaging'
16
+ const GITHUB_URL = 'https://github.com/XBXyftx/dsh-session-messaging'
17
+ const BLOG_URL = 'https://xbxyftx.top'
18
+
19
+ const zh = {
20
+ 'panel.label': '跨会话消息',
21
+ 'page.title': '跨会话消息',
22
+ 'page.subtitle': '让同一 Host 内的多个用户会话互相发消息、协调任务与写入范围。',
23
+ 'setting.title': '允许跨工作目录消息',
24
+ 'setting.desc': '开启后,不同工作目录的顶层会话也能互相发现和发送消息。',
25
+ 'setting.unavailable': '设置服务当前不可用,无法在线修改。',
26
+ 'setting.loading': '正在读取配置…',
27
+ 'setting.saveFailed': '保存失败,请重试。',
28
+ 'update.title': '版本更新',
29
+ 'update.checking': '正在检查更新…',
30
+ 'update.upToDate': '已是最新版本',
31
+ 'update.available': '发现新版本',
32
+ 'update.failed': '更新检查失败',
33
+ 'update.button': '检查更新',
34
+ 'author.title': '作者',
35
+ 'author.desc': '由 XBXyftx 开发,MIT 开源。',
36
+ 'links.npm': 'npm',
37
+ 'links.github': 'GitHub',
38
+ 'links.blog': '作者博客',
39
+ }
40
+ const en = {
41
+ 'panel.label': 'Session Messaging',
42
+ 'page.title': 'Session Messaging',
43
+ 'page.subtitle': 'Let user-launched sessions in the same Host message each other, coordinate tasks, and declare write scopes.',
44
+ 'setting.title': 'Allow cross-directory messages',
45
+ 'setting.desc': 'When on, top-level sessions in different working directories can discover and message each other.',
46
+ 'setting.unavailable': 'The settings service is unavailable; this value cannot be edited here.',
47
+ 'setting.loading': 'Reading configuration…',
48
+ 'setting.saveFailed': 'Saving failed. Try again.',
49
+ 'update.title': 'Updates',
50
+ 'update.checking': 'Checking for updates…',
51
+ 'update.upToDate': 'Up to date',
52
+ 'update.available': 'Update available',
53
+ 'update.failed': 'Update check failed',
54
+ 'update.button': 'Check for updates',
55
+ 'author.title': 'Author',
56
+ 'author.desc': 'Built by XBXyftx. MIT licensed.',
57
+ 'links.npm': 'npm',
58
+ 'links.github': 'GitHub',
59
+ 'links.blog': 'Blog',
60
+ }
61
+
62
+ const color = {
63
+ text: 'var(--dsw-alias-label-primary)',
64
+ secondary: 'var(--dsw-alias-label-secondary)',
65
+ border: 'var(--dsw-alias-border-l1)',
66
+ surface: 'var(--dsw-alias-bg-layer-1)',
67
+ accent: 'var(--dsw-alias-brand-primary)',
68
+ error: 'var(--dsw-alias-state-error-primary)',
69
+ }
70
+ const pageStyle = {
71
+ display: 'flex', flexDirection: 'column', gap: 16, width: '100%',
72
+ maxWidth: 760, margin: '0 auto', padding: '28px 32px', boxSizing: 'border-box',
73
+ color: color.text, overflowY: 'auto',
74
+ }
75
+ const cardStyle = {
76
+ display: 'flex', flexDirection: 'column', gap: 14,
77
+ background: color.surface, border: `1px solid ${color.border}`,
78
+ borderRadius: 12, padding: 18,
79
+ }
80
+ const rowStyle = { display: 'flex', alignItems: 'center', gap: 16, justifyContent: 'space-between' }
81
+ const titleStyle = { fontSize: 15, fontWeight: 600, margin: 0, color: color.text }
82
+ const descStyle = { fontSize: 13, lineHeight: 1.5, margin: '6px 0 0', color: color.secondary }
83
+ const linkStyle = { color: color.accent, textDecoration: 'none', fontSize: 13 }
84
+ const buttonStyle = {
85
+ appearance: 'none', border: `1px solid ${color.border}`, borderRadius: 8,
86
+ background: 'transparent', color: color.text, padding: '6px 12px',
87
+ fontSize: 13, cursor: 'pointer',
88
+ }
89
+
90
+ function Icon({ size = 16 }) {
91
+ return h('svg', {
92
+ viewBox: '0 0 24 24', width: size, height: size, fill: 'none',
93
+ stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round',
94
+ strokeLinejoin: 'round', 'aria-hidden': true, style: { display: 'block' },
95
+ },
96
+ h('path', { d: 'M4 6a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H8l-4 3z' }),
97
+ h('path', { d: 'M9 13h9a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-2l-3 2v-2h-4a2 2 0 0 1-2-2z' }))
98
+ }
99
+
100
+ function compareVersions(a, b) {
101
+ const pa = String(a).split('-')[0].split('.').map(Number)
102
+ const pb = String(b).split('-')[0].split('.').map(Number)
103
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
104
+ const x = pa[i] ?? 0
105
+ const y = pb[i] ?? 0
106
+ if (x !== y) return x - y
107
+ }
108
+ return 0
109
+ }
110
+
111
+ function Panel({ t, form, loadCurrentVersion }) {
112
+ const snapshot = React.useSyncExternalStore(
113
+ React.useCallback(listener => form.subscribe(listener), [form]),
114
+ React.useCallback(() => form.getSnapshot(), [form]),
115
+ React.useCallback(() => form.getSnapshot(), [form]),
116
+ )
117
+ const [saving, setSaving] = React.useState(false)
118
+ const [saveFailed, setSaveFailed] = React.useState(false)
119
+ const [currentVersion, setCurrentVersion] = React.useState(FALLBACK_VERSION)
120
+ const [update, setUpdate] = React.useState({ status: 'checking', latest: undefined })
121
+ const ready = snapshot.status === 'ready'
122
+ const enabled = (snapshot.value && snapshot.value.sameDirectoryOnly) === true
123
+
124
+ const check = React.useCallback(async () => {
125
+ setUpdate(previous => ({ ...previous, status: 'checking' }))
126
+ try {
127
+ const response = await fetch('https://registry.npmjs.org/dsh-session-messaging/latest')
128
+ if (!response.ok) throw new Error(String(response.status))
129
+ const data = await response.json()
130
+ setUpdate({ status: 'done', latest: typeof data.version === 'string' ? data.version : undefined })
131
+ } catch (_error) {
132
+ setUpdate({ status: 'failed', latest: undefined })
133
+ }
134
+ }, [])
135
+
136
+ React.useEffect(() => {
137
+ let cancelled = false
138
+ void loadCurrentVersion().then(version => { if (!cancelled) setCurrentVersion(version) })
139
+ void check()
140
+ return () => { cancelled = true }
141
+ }, [check, loadCurrentVersion])
142
+
143
+ const toggle = async () => {
144
+ if (!ready || saving) return
145
+ setSaving(true)
146
+ setSaveFailed(false)
147
+ try {
148
+ const accepted = await form.set('sameDirectoryOnly', !enabled)
149
+ if (!accepted) setSaveFailed(true)
150
+ } catch (_error) {
151
+ setSaveFailed(true)
152
+ } finally {
153
+ setSaving(false)
154
+ }
155
+ }
156
+
157
+ const newer = update.status === 'done' && update.latest !== undefined
158
+ && compareVersions(update.latest, currentVersion) > 0
159
+
160
+ const settingHint = snapshot.status === 'loading'
161
+ ? t('setting.loading')
162
+ : snapshot.status === 'unavailable'
163
+ ? t('setting.unavailable')
164
+ : saveFailed ? t('setting.saveFailed') : t('setting.desc')
165
+
166
+ return h('div', { style: pageStyle },
167
+ h('div', null,
168
+ h('h1', { style: { fontSize: 22, fontWeight: 650, margin: 0 } }, t('page.title')),
169
+ h('p', { style: { ...descStyle, fontSize: 14, marginTop: 6 } }, t('page.subtitle')),
170
+ ),
171
+ h('section', { style: cardStyle },
172
+ h('div', { style: rowStyle },
173
+ h('div', null,
174
+ h('h2', { style: titleStyle }, t('setting.title')),
175
+ h('p', { style: { ...descStyle, color: saveFailed ? color.error : color.secondary } }, settingHint),
176
+ ),
177
+ h('input', {
178
+ type: 'checkbox', checked: enabled, disabled: !ready || saving,
179
+ onChange: () => { void toggle() }, 'aria-label': t('setting.title'),
180
+ style: { width: 18, height: 18, accentColor: color.accent, cursor: ready ? 'pointer' : 'default' },
181
+ }),
182
+ ),
183
+ ),
184
+ h('section', { style: cardStyle },
185
+ h('div', { style: rowStyle },
186
+ h('div', null,
187
+ h('h2', { style: titleStyle }, t('update.title')),
188
+ h('p', { style: descStyle },
189
+ update.status === 'checking' ? t('update.checking')
190
+ : update.status === 'failed' ? t('update.failed')
191
+ : newer ? `${t('update.available')} · v${update.latest}`
192
+ : `${t('update.upToDate')} · v${currentVersion}`),
193
+ ),
194
+ h('button', { type: 'button', style: buttonStyle, onClick: () => { void check() }, disabled: update.status === 'checking' },
195
+ t('update.button')),
196
+ ),
197
+ newer ? h('a', { href: NPM_URL, target: '_blank', rel: 'noreferrer', style: linkStyle }, t('links.npm') + ' · v' + update.latest) : null,
198
+ ),
199
+ h('section', { style: cardStyle },
200
+ h('div', { style: rowStyle },
201
+ h('div', null,
202
+ h('h2', { style: titleStyle }, t('author.title')),
203
+ h('p', { style: descStyle }, t('author.desc')),
204
+ ),
205
+ h('div', { style: { display: 'flex', gap: 14 } },
206
+ h('a', { href: NPM_URL, target: '_blank', rel: 'noreferrer', style: linkStyle }, t('links.npm')),
207
+ h('a', { href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: linkStyle }, t('links.github')),
208
+ h('a', { href: BLOG_URL, target: '_blank', rel: 'noreferrer', style: linkStyle }, t('links.blog')),
209
+ ),
210
+ ),
211
+ ),
212
+ )
213
+ }
214
+
215
+ return {
216
+ inject: ['slots', 'locale', 'configForms', 'remote', 'remote.pluginManager'],
217
+ apply(ctx) {
218
+ const t = ctx.locale.bind(NS)
219
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-session-messaging: locale')
220
+ const form = ctx.configForms.get(PANEL_ID)
221
+
222
+ const currentVersion = () => ctx.remote.pluginManager.listBundles()
223
+ .then(rows => {
224
+ const rowsArray = Array.isArray(rows) ? rows : []
225
+ const own = rowsArray.find(row => row && row.name === PACKAGE_NAME)
226
+ return own && typeof own.version === 'string' ? own.version : FALLBACK_VERSION
227
+ })
228
+ .catch(() => FALLBACK_VERSION)
229
+
230
+ ctx.slots.inject('main', () => ctx.slots.register({
231
+ name: 'main', key: PANEL_ID, locale: NS,
232
+ inject: () => ({ t, form, loadCurrentVersion: currentVersion }),
233
+ }, Panel))
234
+
235
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
236
+ name: 'sidebar.panellist', id: PANEL_ID, order: 5,
237
+ label: () => t('panel.label'), locale: NS,
238
+ }, Icon))
239
+ },
240
+ }
241
+ },
242
+ })
package/lib/config.d.ts CHANGED
@@ -13,10 +13,32 @@ export interface SessionMessagingConfig {
13
13
  readonly maxMessagesPerMinute: number;
14
14
  /** Require the target session to opt in before it can receive peer messages. Off by default: sessions are mutually visible and can message each other unless they pause reception. */
15
15
  readonly requireTargetOptIn: boolean;
16
- /** Only allow peers whose working directory equals the sender's. */
16
+ /**
17
+ * Restrict peers to the sender's working directory. Off by default:
18
+ * cross-directory peers are allowed. Declared volatile so the sidebar panel
19
+ * can edit it through the dsh settings form.
20
+ */
17
21
  readonly sameDirectoryOnly: boolean;
18
22
  /** Allow a session to relay to itself (diagnostics only; off by default). */
19
23
  readonly allowSelfSend: boolean;
20
24
  }
21
25
  /** Schemastery schema for the plugin row's `config` block. */
22
- export declare const Config: Schema<SessionMessagingConfig>;
26
+ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
27
+ maxMessageChars: Schema<number, number, "defined">;
28
+ maxMessagesPerMinute: Schema<number, number, "defined">;
29
+ requireTargetOptIn: Schema<boolean, boolean, "defined">;
30
+ sameDirectoryOnly: Schema<boolean, boolean, "volatile-defined">;
31
+ allowSelfSend: Schema<boolean, boolean, "defined">;
32
+ }>>, Schemastery.ObjectT<NoInfer<{
33
+ maxMessageChars: Schema<number, number, "defined">;
34
+ maxMessagesPerMinute: Schema<number, number, "defined">;
35
+ requireTargetOptIn: Schema<boolean, boolean, "defined">;
36
+ sameDirectoryOnly: Schema<boolean, boolean, "volatile-defined">;
37
+ allowSelfSend: Schema<boolean, boolean, "defined">;
38
+ }>>, "plain">;
39
+ /** Plugin runtime config: volatile fields arrive as stable references. */
40
+ export interface ResolvedSessionMessagingConfig extends Omit<SessionMessagingConfig, 'sameDirectoryOnly'> {
41
+ readonly sameDirectoryOnly: {
42
+ get(): boolean;
43
+ };
44
+ }
package/lib/config.js CHANGED
@@ -11,6 +11,6 @@ export const Config = Schema.object({
11
11
  maxMessageChars: Schema.number().min(1).default(DEFAULT_MAX_MESSAGE_CHARS),
12
12
  maxMessagesPerMinute: Schema.number().min(1).default(DEFAULT_MAX_MESSAGES_PER_MINUTE),
13
13
  requireTargetOptIn: Schema.boolean().default(false),
14
- sameDirectoryOnly: Schema.boolean().default(true),
14
+ sameDirectoryOnly: Schema.boolean().default(false).volatile(),
15
15
  allowSelfSend: Schema.boolean().default(false),
16
16
  });
package/lib/service.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import { Service, type Context } from '@deepseek-ai/cordis';
10
10
  import type { Agent } from '@deepseek-ai/dsh-agent';
11
11
  import { Config } from './config.ts';
12
- import type { SessionMessagingConfig } from './config.ts';
12
+ import type { ResolvedSessionMessagingConfig } from './config.ts';
13
13
  import type { PeerView, SendReceipt, SendRequest } from './protocol.ts';
14
14
  /** One session's receive consent and advisory write scopes. */
15
15
  interface ConsentEntry {
@@ -31,7 +31,19 @@ declare module '@deepseek-ai/cordis' {
31
31
  export declare class SessionMessagingService extends Service {
32
32
  static inject: string[];
33
33
  /** Loader-read configuration schema; defaults apply when the row omits `config`. */
34
- static Config: import("@deepseek-ai/schemastery").default<SessionMessagingConfig>;
34
+ static Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
35
+ maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
36
+ maxMessagesPerMinute: import("@deepseek-ai/schemastery").default<number, number, "defined">;
37
+ requireTargetOptIn: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
38
+ sameDirectoryOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
39
+ allowSelfSend: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
40
+ }>>, Schemastery.ObjectT<NoInfer<{
41
+ maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
42
+ maxMessagesPerMinute: import("@deepseek-ai/schemastery").default<number, number, "defined">;
43
+ requireTargetOptIn: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
44
+ sameDirectoryOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
45
+ allowSelfSend: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
46
+ }>>, "plain">;
35
47
  private readonly config;
36
48
  private readonly limiter;
37
49
  private readonly consent;
@@ -39,7 +51,7 @@ export declare class SessionMessagingService extends Service {
39
51
  * @param ctx - Cordis context with `agents` and `sessions` ready.
40
52
  * @param config - validated plugin configuration.
41
53
  */
42
- constructor(ctx: Context, config: SessionMessagingConfig);
54
+ constructor(ctx: Context, config: ResolvedSessionMessagingConfig);
43
55
  /**
44
56
  * List live user-launched peers the caller may address.
45
57
  * @param caller - the agent whose tool call is inspecting peers.
package/lib/service.js CHANGED
@@ -47,7 +47,7 @@ export class SessionMessagingService extends Service {
47
47
  continue;
48
48
  if (!isTopLevelSessionHeader(candidate.session.header))
49
49
  continue;
50
- if (this.config.sameDirectoryOnly && !sameDirectory(callerDirectory, candidate.session.header.cwd)) {
50
+ if (this.config.sameDirectoryOnly.get() && !sameDirectory(callerDirectory, candidate.session.header.cwd)) {
51
51
  continue;
52
52
  }
53
53
  const consent = this.consent.get(candidate.id);
@@ -137,7 +137,7 @@ export class SessionMessagingService extends Service {
137
137
  if (!isTopLevelSessionHeader(target.session.header)) {
138
138
  throw new MessagingError('target-not-top-level', `session ${targetId} is not a user-launched top-level session`);
139
139
  }
140
- if (this.config.sameDirectoryOnly && !sameDirectory(caller.session.header.cwd, target.session.header.cwd)) {
140
+ if (this.config.sameDirectoryOnly.get() && !sameDirectory(caller.session.header.cwd, target.session.header.cwd)) {
141
141
  throw new MessagingError('cross-directory', 'the target session works in a different directory');
142
142
  }
143
143
  const consent = this.consent.get(targetId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-messaging",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Peer messaging between user-launched sessions in one workspace: discover peers, relay coordination messages, pause reception, and get advisory write-scope conflict warnings.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,6 +15,7 @@
15
15
  "types": "./lib/tools.d.ts",
16
16
  "default": "./lib/tools.js"
17
17
  },
18
+ "./client": "./client/client.js",
18
19
  "./locale/*.json": "./locale/*.json",
19
20
  "./tools/locale/*.json": "./tools/locale/*.json",
20
21
  "./tools/icon": "./assets/tools-icon.svg",
@@ -23,6 +24,7 @@
23
24
  },
24
25
  "files": [
25
26
  "lib",
27
+ "client",
26
28
  "locale",
27
29
  "tools/locale",
28
30
  "assets",
@@ -77,12 +79,20 @@
77
79
  "build": "tsc -p tsconfig.build.json",
78
80
  "typecheck": "tsc -p tsconfig.json",
79
81
  "test": "node --experimental-strip-types --test tests/protocol.test.ts",
80
- "test:integration": "node --experimental-strip-types tests/integration-smoke.ts",
82
+ "test:integration": "node --experimental-strip-types tests/integration-smoke.ts && node --experimental-strip-types tests/directory-policy-smoke.ts",
81
83
  "test:model-e2e": "node --experimental-strip-types tests/model-e2e-smoke.ts"
82
84
  },
83
85
  "dsh": {
84
86
  "bundle": {
85
87
  "patch": "./cordis.patch.yml"
88
+ },
89
+ "client": {
90
+ "platform": "web",
91
+ "immediately": true,
92
+ "inject": [
93
+ "@deepseek-ai/dsh-client-ui-settings",
94
+ "@deepseek-ai/dsh-client-ui-layout"
95
+ ]
86
96
  }
87
97
  },
88
98
  "icon": "./assets/icon.svg",