dsh-session-messaging 0.2.0 → 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,21 @@ 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
+
7
22
  ## [0.2.0] - 2026-10-18
8
23
 
9
24
  ### Changed
package/README.md CHANGED
@@ -40,11 +40,18 @@ Two user-launched sessions used the plugin to coordinate a real fix. One session
40
40
 
41
41
  See [CHANGELOG.md](CHANGELOG.md) for the `0.1.2` entry.
42
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
+
43
49
  ## Features
44
50
 
45
51
  - `session_peers` — list live, same-directory, top-level sessions and their reception state.
46
52
  - `session_send` — relay a coordination message, optionally declaring `writeScopes` and replying to a message.
47
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.
48
55
  - Messages appear inline in the target conversation, attributed to the sending session — no separate inbox UI.
49
56
  - Advisory **write-scope conflict detection**: if a peer declared an overlapping path, the receipt and the message envelope carry a `conflict-warning`.
50
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.
@@ -80,7 +87,7 @@ The envelope always states that the message comes from another session, not the
80
87
  dsh plugin --profile <profile> add dsh-session-messaging
81
88
  ```
82
89
 
83
- The package is published as [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging) (latest `0.2.0`). For library use: `npm install dsh-session-messaging`.
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`.
84
91
 
85
92
  ### From GitHub
86
93
 
package/README.zh-CN.md CHANGED
@@ -36,11 +36,18 @@ 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`。
@@ -76,7 +83,7 @@ macOS 桌面客户端(`0.2.1-rc.2`)真实运行:另一个用户启动的
76
83
  dsh plugin --profile <profile> add dsh-session-messaging
77
84
  ```
78
85
 
79
- 包已发布为 [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging)(最新 `0.2.0`)。库用法:`npm install dsh-session-messaging`。
86
+ 包已发布为 [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging)(最新 `0.3.0`)。库用法:`npm install dsh-session-messaging`。
80
87
 
81
88
  ### 从 GitHub 安装
82
89
 
@@ -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
- /** Restrict peers to the sender's working directory. Off by default: cross-directory peers are allowed. */
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(false),
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.2.0",
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",
@@ -83,6 +85,14 @@
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",