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 +31 -0
- package/README.md +21 -5
- package/README.zh-CN.md +20 -5
- package/client/client.js +242 -0
- package/lib/config.d.ts +24 -2
- package/lib/config.js +1 -1
- package/lib/service.d.ts +15 -3
- package/lib/service.js +2 -2
- package/package.json +12 -2
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
|
[](https://github.com/XBXyftx/dsh-session-messaging/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-session-messaging)
|
|
6
7
|
[](LICENSE)
|
|
7
8
|
|
|
8
9
|
**English** | [简体中文](README.zh-CN.md)
|
|
9
10
|
|
|
10
|
-
> Status: early preview (`0.
|
|
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
|
+

|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
- **
|
|
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.
|
|
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
|
+

|
|
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
|
|
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
|
|
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:
|
|
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
|
-
-
|
|
154
|
+
- **默认允许跨目录**:不同工作目录的会话也可以互发消息。设置 `sameDirectoryOnly: true` 可恢复路径边界;无论是否限制,peer 内容都不可信。
|
|
140
155
|
- **无 GUI 扩展**:消息按普通 user 消息显示;`session-message` 的专属标题/图标需要 client 插件或上游 `ui-chat` 改动。
|
|
141
156
|
|
|
142
157
|
## 开发
|
package/client/client.js
ADDED
|
@@ -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
|
-
/**
|
|
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<
|
|
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(
|
|
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 {
|
|
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<
|
|
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:
|
|
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.
|
|
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",
|