bothouse-dsh 0.1.58 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -7,14 +7,14 @@ BotHouse channels inside DeepSeek Harness (DSH): a Discord-style channels page w
7
7
  ## Install (pick one)
8
8
 
9
9
  1. **Easiest**: tell DSH "install the plugin bothouse-dsh".
10
- 2. **From the UI**: DSH sidebar "Plugins" → "Add plugin", enter `bothouse-dsh`.
10
+ 2. **From the UI**: DSH sidebar "Plugins" → "Add plugin", enter `bothouse-dsh`, click "Install"; then switch bothouse on under "Installed plugins" — "Channels" appears in the sidebar right away, no restart.
11
11
  3. **From a terminal**:
12
12
 
13
13
  ~~~sh
14
14
  dsh plugin --profile web add bothouse-dsh
15
15
  ~~~
16
16
 
17
- Restart DSH once and click "Channels" in the sidebar. Team desk, bothouse panel and other computers start off; switch the one you want on under "Plugins → bothouse" and it appears in the sidebar right away. #general already has Whale Girl, running on the model you configured in DSH; nothing else to fill in. @ her to get a reply; tell her "hand this to a workspace: …" and she opens a workspace, does the work and reports back. For more characters or models, use "Add member".
17
+ Asked through DSH, "Channels" appears right after the install; installed from the command line while DSH is running, restart DSH once. Then click "Channels" in the sidebar. Team desk, bothouse panel and other computers start off; switch the one you want on under "Plugins → bothouse" and it appears in the sidebar right away. #general already has Whale Girl, running on the model you configured in DSH; nothing else to fill in. @ her to get a reply; tell her "hand this to a workspace: …" and she opens a workspace, does the work and reports back. For more characters or models, use "Add member".
18
18
 
19
19
  If the terminal install says `pnpm was not found`, run `npm i -g pnpm` first.
20
20
 
@@ -23,6 +23,7 @@ If the terminal install says `pnpm was not found`, run `npm i -g pnpm` first.
23
23
  - DSH 0.2.x (from `0.2.1-rc.2`, see `dsh --version`) and Node.js 22.12 or later. Tested on 0.2.1-rc.2 only; no compatibility promise for the DSH plugin API was found and the page is prebuilt against the DSH 0.2 client packages, so the range stops before 0.3.
24
24
  - Model members: configure the model and its key in DSH settings first.
25
25
  - CLI members: install and sign in to Claude Code, Codex or the Gemini CLI, with the command on PATH.
26
+ The Claude Code and Codex adapters are not installed with the plugin; they download the first time you add such a member (Claude Code about 125 MB).
26
27
  - Channel messages are stored in `~/.conest-collab/collab.sqlite3`; set `CONEST_COLLAB_DATA` before starting DSH to use another directory.
27
28
 
28
29
  ## Compared with full BotHouse
package/README.md CHANGED
@@ -7,14 +7,14 @@
7
7
  ## 安装(三选一)
8
8
 
9
9
  1. **最省事**:在 DSH 里对它说「帮我装插件 bothouse-dsh」。
10
- 2. **点界面**:DSH 侧栏「插件」→「添加插件」,填 `bothouse-dsh`。
10
+ 2. **点界面**:DSH 侧栏「插件」→「添加插件」,填 `bothouse-dsh`,点「安装」;装完在「已安装插件」里把 bothouse 的开关打开,左侧马上出现「频道」,不用重启。
11
11
  3. **命令行**:
12
12
 
13
13
  ~~~sh
14
14
  dsh plugin --profile web add bothouse-dsh
15
15
  ~~~
16
16
 
17
- 装完重启一次 DSH,点左侧「频道」。团队工作台、bothouse 面板、其他电脑默认不在左侧,要用就到「插件 → bothouse」里打开对应那一项,左侧马上出现。#general 里已经有鲸鱼娘,用的是你在 DSH 里配好的那个模型,不用再填任何东西。@鲸鱼娘 就回话;对她说「交给工作区做:……」,她会开工作区干完再交回。想要多角色、多模型,再点「添加成员」。
17
+ 对 DSH 说的装法装完左侧直接出现「频道」;命令行装时 DSH 若正开着,重启一次。然后点左侧「频道」。团队工作台、bothouse 面板、其他电脑默认不在左侧,要用就到「插件 → bothouse」里打开对应那一项,左侧马上出现。#general 里已经有鲸鱼娘,用的是你在 DSH 里配好的那个模型,不用再填任何东西。@鲸鱼娘 就回话;对她说「交给工作区做:……」,她会开工作区干完再交回。想要多角色、多模型,再点「添加成员」。
18
18
 
19
19
  命令行装时提示 `pnpm was not found`:先 `npm i -g pnpm` 再装。
20
20
 
@@ -23,6 +23,7 @@ dsh plugin --profile web add bothouse-dsh
23
23
  - DSH 0.2.x(`0.2.1-rc.2` 起,`dsh --version`)和 Node.js 22.12 以上。只在 0.2.1-rc.2 上实测过;DSH 插件接口有没有兼容承诺没查到,页面又是按 DSH 0.2 的前端包预编译的,所以先只放到 0.2.x,0.3 出来要重新验。
24
24
  - 用模型当成员:在 DSH 设置里先配好模型和 Key。
25
25
  - 用 CLI 当成员:本机装好并登录 Claude Code、Codex 或 Gemini CLI,命令在 PATH 里。
26
+ Claude Code、Codex 的接入适配器不随插件安装,第一次把它们加成成员时自动下载(Claude Code 约 125 MB),之后不再下。
26
27
  - 频道消息存在 `~/.conest-collab/collab.sqlite3`;启动 DSH 前设置 `CONEST_COLLAB_DATA` 可换目录。
27
28
 
28
29
  ## 和完整版 BotHouse 的区别
@@ -597,7 +597,7 @@ export class DshMembers {
597
597
  const answered = this.approvals.answerByText(msg);
598
598
  const hit = mentions(msg.text, this.hub.routableIn(msg.room_id));
599
599
  const only = this.hub.replyRoute(msg);
600
- const leadID = this.hub.primaryIn(msg.room_id)?.id;
600
+ const leadID = this.hub.defaultReplyIn(msg.room_id)?.id;
601
601
  const leadTarget = msg.author_kind === 'user' && leadID !== undefined && target === undefined && only === null && hit.size === 0;
602
602
  const visible = new Set(this.hub.routableIn(msg.room_id).map(m => m.id));
603
603
  const targets = [...this.config.keys()].filter(id => !this.hub.quota?.cooling(id) && visible.has(id) && id !== msg.author && !answered.has(id) && (!target || target === id)
package/collab/hub.mjs CHANGED
@@ -48,9 +48,8 @@ export class Hub {
48
48
  return member !== undefined && member.kind !== 'human' ? member : undefined;
49
49
  }
50
50
 
51
- /** Channel selection, inherited by workspaces; only unset channels use the legacy global choice. */
51
+ /** Channel primary for delegation; unset channels retain the selected global role. */
52
52
  primaryIn(roomId) {
53
- // #1023: in a private chat the one agent answers every owner message, @ or not.
54
53
  const dm = this.store.dmMember?.(roomId);
55
54
  if (dm !== undefined) return this.routableIn(roomId).find(m => m.id === dm);
56
55
  const id = this.store.leadId(roomId);
@@ -58,7 +57,18 @@ export class Hub {
58
57
  return member && this.routableIn(roomId).some(m => m.id === member.id) ? member : undefined;
59
58
  }
60
59
 
61
- /** UI-only fallback; callers that route messages must use selectedPrimaryMember(). */
60
+ /** Default replies are channel-local and do not change delegation authority. */
61
+ defaultReplyIn(roomId) {
62
+ // #1023: in a private chat the one agent answers every owner message, @ or not.
63
+ const dm = this.store.dmMember?.(roomId);
64
+ if (dm !== undefined) return this.routableIn(roomId).find(m => m.id === dm);
65
+ const id = this.store.leadId(roomId);
66
+ const members = this.routableIn(roomId);
67
+ if (id === null) return members.length === 1 ? members[0] : undefined;
68
+ return members.find(member => member.id === id);
69
+ }
70
+
71
+ /** UI-only fallback; message routing uses defaultReplyIn(), delegation uses primaryIn(). */
62
72
  primaryMember() {
63
73
  const fallback = this.fallbackLead === '' ? undefined : this.members.get(this.fallbackLead);
64
74
  return this.selectedPrimaryMember() ?? (fallback?.kind !== 'human' ? fallback : undefined);
@@ -68,6 +78,8 @@ export class Hub {
68
78
  if (member.avatar?.startsWith('upload:')) member.avatarVersion = Math.random().toString(36).slice(2);
69
79
  this.members.set(member.id, member);
70
80
  this.emit({ type: 'members', members: this.memberView() });
81
+ // Membership determines an unset channel's effective primary, including its workspaces.
82
+ this.emit({ type: 'rooms', rooms: this.store.roomsView() });
71
83
  }
72
84
 
73
85
  removeMember(id) {
@@ -76,6 +88,7 @@ export class Hub {
76
88
  for (const room of this.thinking.keys()) this.setThinking(room, id, false);
77
89
  for (const room of [...this.queued.keys()]) this.setQueue(room, id, []);
78
90
  this.emit({ type: 'members', members: this.memberView() });
91
+ this.emit({ type: 'rooms', rooms: this.store.roomsView() });
79
92
  }
80
93
 
81
94
  setOnline(id, online) {
@@ -283,7 +296,7 @@ export class Hub {
283
296
 
284
297
  emit(event) {
285
298
  if (event.type === 'rooms') event = { ...event, rooms: event.rooms.map(room => ({ ...room,
286
- lead_id: this.store.leadId(room.id), effective_lead_id: this.primaryIn(room.id)?.id ?? null,
299
+ lead_id: this.store.leadId(room.id), effective_lead_id: this.defaultReplyIn(room.id)?.id ?? null,
287
300
  workspace_path: this.store.workspacePath(room.id, this.workspacePath),
288
301
  })) };
289
302
  for (const s of this.subscribers) {
package/collab/page.html CHANGED
@@ -1026,6 +1026,26 @@ function messageNode(m) {
1026
1026
  }
1027
1027
  card.append(bar);
1028
1028
  }
1029
+ // 与原生频道页共用重试接口;后端把已受理的失败卡更新成 retried。
1030
+ if (meta.state === 'failed') {
1031
+ const bar = el('div', 'actions');
1032
+ const retry = el('button', '', '重试');
1033
+ retry.type = 'button';
1034
+ retry.onclick = async () => {
1035
+ retry.disabled = true;
1036
+ retry.textContent = '重试中…';
1037
+ try {
1038
+ await api(`${ROOT}/collab/api/messages/${m.id}/retry`, { method: 'POST' });
1039
+ retry.textContent = '已重试';
1040
+ } catch (err) {
1041
+ retry.disabled = false;
1042
+ retry.textContent = '重试';
1043
+ alert('重试失败:' + err.message);
1044
+ }
1045
+ };
1046
+ bar.append(retry);
1047
+ card.append(bar);
1048
+ }
1029
1049
  col.append(card);
1030
1050
  } else if (m.text) col.append(renderBody(m.text, 'body'));
1031
1051
  if (Array.isArray(m.attachments) && m.attachments.length) col.append(attachmentList(m.attachments));
@@ -1151,7 +1171,43 @@ function showState(kind, title, detail, retry) {
1151
1171
  return box;
1152
1172
  }
1153
1173
 
1174
+ // 来源:openRoom 原有每次读取 100 条;向上分页沿用同一页大小。
1175
+ const HISTORY_LIMIT = 100;
1176
+ let historyPage;
1177
+
1178
+ async function loadOlder() {
1179
+ const page = historyPage;
1180
+ if (!page || page.loading || !page.more) return;
1181
+ page.loading = true;
1182
+ const log = $('log');
1183
+ log.querySelector('.history-error')?.remove();
1184
+ try {
1185
+ const { messages } = await api(`${ROOT}/collab/api/rooms/${page.room}/messages?before=${page.before}&limit=${HISTORY_LIMIT}`);
1186
+ if (historyPage !== page) return;
1187
+ const oldHeight = log.scrollHeight;
1188
+ const oldTop = log.scrollTop;
1189
+ messages.forEach(upsert);
1190
+ if (messages.length) page.before = messages[0].id;
1191
+ page.more = messages.length === HISTORY_LIMIT;
1192
+ log.scrollTop = oldTop + log.scrollHeight - oldHeight;
1193
+ } catch (err) {
1194
+ if (historyPage !== page) return;
1195
+ const retry = el('button', 'btn history-error', '历史加载失败,点击重试');
1196
+ retry.type = 'button';
1197
+ retry.title = err.message;
1198
+ retry.onclick = () => loadOlder();
1199
+ log.prepend(retry);
1200
+ } finally {
1201
+ page.loading = false;
1202
+ }
1203
+ }
1204
+
1205
+ $('log').addEventListener('scroll', () => {
1206
+ if ($('log').scrollTop === 0) void loadOlder();
1207
+ }, { passive: true });
1208
+
1154
1209
  async function openRoom(id) {
1210
+ const page = historyPage = { room: id, before: undefined, more: false, loading: true };
1155
1211
  state.room = id; state.synced = 0;
1156
1212
  $('log').replaceChildren(showState('loading', '正在加载消息…'));
1157
1213
  $('log').setAttribute('aria-busy', 'true');
@@ -1161,11 +1217,14 @@ async function openRoom(id) {
1161
1217
  const setting = await api(`${ROOT}/collab/api/rooms/${id}/mode`);
1162
1218
  state.mode = setting.dispatch_mode;
1163
1219
  renderRooms(); renderMembers();
1164
- const { messages } = await api(`${ROOT}/collab/api/rooms/${id}/messages?limit=100`);
1165
- if (state.room !== id) return;
1220
+ const { messages } = await api(`${ROOT}/collab/api/rooms/${id}/messages?limit=${HISTORY_LIMIT}`);
1221
+ if (historyPage !== page) return;
1166
1222
  const log = $('log');
1167
1223
  log.querySelector('.state')?.remove();
1168
1224
  messages.forEach(upsert);
1225
+ page.before = messages[0]?.id;
1226
+ page.more = messages.length === HISTORY_LIMIT;
1227
+ page.loading = false;
1169
1228
  state.synced = messages.length ? messages[messages.length - 1].id : 0;
1170
1229
  if (!log.querySelector('.msg')) log.append(showState('empty', '还没有消息', ''));
1171
1230
  $('log').setAttribute('aria-busy', 'false');
@@ -1173,7 +1232,7 @@ async function openRoom(id) {
1173
1232
  stuck = true;
1174
1233
  $('log').scrollTop = $('log').scrollHeight;
1175
1234
  } catch (err) {
1176
- if (state.room !== id) return;
1235
+ if (historyPage !== page) return;
1177
1236
  $('log').replaceChildren(showState('error', '加载失败', err.message, () => openRoom(id)));
1178
1237
  $('log').setAttribute('aria-busy', 'false');
1179
1238
  }
package/collab/plugin.mjs CHANGED
@@ -346,7 +346,7 @@ export async function apply(ctx, rawConfig) {
346
346
  if (path === '/collab/api/state' && req.method === 'GET') {
347
347
  return json(res, 200, { me: OWNER, lead_id: hub.selectedPrimaryMember()?.id ?? null,
348
348
  effective_lead_id: hub.primaryMember()?.id ?? null,
349
- rooms: store.roomsView().map(r => ({ ...r, lead_id: store.leadId(r.id), effective_lead_id: hub.primaryIn(r.id)?.id ?? null,
349
+ rooms: store.roomsView().map(r => ({ ...r, lead_id: store.leadId(r.id), effective_lead_id: hub.defaultReplyIn(r.id)?.id ?? null,
350
350
  workspace_path: store.workspacePath(r.id, config.workspacePath) })), members: hub.memberView(),
351
351
  thinking: Object.fromEntries([...hub.thinking].map(([k, v]) => [k, [...v]])),
352
352
  queued: Object.fromEntries([...hub.queued.keys()].map(r => [r, hub.queueView(r)])),
@@ -573,7 +573,7 @@ export function roomSettingsRoute(req, res, path, { store, hub, dsh, workspacePa
573
573
  if (match === null) return undefined;
574
574
  const roomId = Number(match[1]);
575
575
  if (store.room(roomId) === undefined) return Promise.resolve(json(res, 404, { error: '频道不存在' }));
576
- const view = () => ({ lead_id: store.leadId(roomId), effective_lead_id: hub.primaryIn(roomId)?.id ?? null,
576
+ const view = () => ({ lead_id: store.leadId(roomId), effective_lead_id: hub.defaultReplyIn(roomId)?.id ?? null,
577
577
  workspace_path: store.workspacePath(roomId, workspacePath), max_relay: store.relaySetting(roomId).max_relay });
578
578
  if (match[2] === 'renew' && req.method === 'POST') {
579
579
  return withBody(req, res, async () => ({ members: await dsh.renewRoom(roomId) }), false);
@@ -108,7 +108,7 @@ export class QuotaCooling {
108
108
  if (!quota.cooling_until && !state.until) continue;
109
109
  if (!this.cooling(member.id)) {
110
110
  for (const [room, lead] of Object.entries(state.leads ?? {})) {
111
- if (this.hub.primaryIn(Number(room))?.id === lead.to) {
111
+ if (this.hub.defaultReplyIn(Number(room))?.id === lead.to) {
112
112
  this.hub.store.setLead(Number(room), member.id);
113
113
  this.notice(Number(room), `${member.id}-${state.until}-restore-${room}`, `${member.name} 额度恢复,主角色已切回。`);
114
114
  }
@@ -124,7 +124,7 @@ export class QuotaCooling {
124
124
  const key = `${member.id}-${state.until}`;
125
125
  const reserved = new Set(Object.values(state.transfers).filter(t => !t.woken).map(t => t.to));
126
126
  for (const room of this.hub.store.rooms().filter(r => r.kind !== 'workspace')) {
127
- if (state.leads[room.id] || this.hub.primaryIn(room.id)?.id !== member.id) continue;
127
+ if (state.leads[room.id] || this.hub.defaultReplyIn(room.id)?.id !== member.id) continue;
128
128
  const target = this.candidate(room.id, '', member.id, reserved);
129
129
  if (!target) continue;
130
130
  state.leads[room.id] = { to: target.id, woken: false };
@@ -135,8 +135,8 @@ export class QuotaCooling {
135
135
  for (const [room, lead] of Object.entries(state.leads)) {
136
136
  if (lead.woken || this.cooling(lead.to)) continue;
137
137
  // Replay a crash between persisting intent and changing the selection.
138
- if (this.hub.primaryIn(Number(room))?.id === member.id) this.hub.store.setLead(Number(room), lead.to);
139
- if (this.hub.primaryIn(Number(room))?.id !== lead.to) continue;
138
+ if (this.hub.defaultReplyIn(Number(room))?.id === member.id) this.hub.store.setLead(Number(room), lead.to);
139
+ if (this.hub.defaultReplyIn(Number(room))?.id !== lead.to) continue;
140
140
  const msg = this.notice(Number(room), `${key}-lead-${room}`, `${member.name} 额度冷却,主角色交给 ${this.tasks.nameOf(lead.to)}。请接着推进未完成任务。`);
141
141
  if (this.wake(lead.to, msg)) { lead.woken = true; this.save(member.id, state, true); }
142
142
  }
package/collab/roster.mjs CHANGED
@@ -208,7 +208,7 @@ export class Roster {
208
208
  this.avatars?.remove(id);
209
209
  this.store.removeMember(id);
210
210
  // #176: a dead id must not linger as a channel's primary role.
211
- for (const roomId of this.store.clearLeadOf(id)) this.hub.emit({ type: 'room-settings', room_id: this.store.settingsRoom(roomId), lead_id: null, effective_lead_id: this.hub.primaryIn(roomId)?.id ?? null });
211
+ for (const roomId of this.store.clearLeadOf(id)) this.hub.emit({ type: 'room-settings', room_id: this.store.settingsRoom(roomId), lead_id: null, effective_lead_id: this.hub.defaultReplyIn(roomId)?.id ?? null });
212
212
  this.dsh.dropMember(id);
213
213
  }
214
214
 
@@ -252,7 +252,8 @@ export class Roster {
252
252
  throw new CollabError('invalid', 'rooms must contain existing chat channel ids');
253
253
  }
254
254
  const clash = this.hub.memberList().find(m => m.id !== selfId && (!rooms.length || !m.rooms?.length || rooms.some(id => m.rooms.includes(id))) && (m.name.toLowerCase() === name.toLowerCase() || m.id.toLowerCase() === name.toLowerCase()));
255
- if (clash !== undefined) throw new CollabError('conflict', `name ${name} is already used by ${clash.name}`);
255
+ // #366: 这条会原样显示在成员对话框的错误条里,和同页的成员名校验一样说中文。
256
+ if (clash !== undefined) throw new CollabError('conflict', `名字 ${name} 已被 ${clash.name} 使用`);
256
257
  const persona = String(input?.persona ?? '');
257
258
  if (persona.length > MAX_PERSONA) throw new CollabError('invalid', `persona exceeds ${MAX_PERSONA} characters`);
258
259
  let avatar;
package/collab/store.mjs CHANGED
@@ -393,19 +393,21 @@ export class Store {
393
393
  * workspace is addressed, so a duplicate or a workspace-shaped name is refused with 'conflict'.
394
394
  */
395
395
  checkRoomName(name, selfId) {
396
- if (new RegExp(`^${WORKSPACE_PREFIX}\\d+$`).test(name)) throw new CollabError('conflict', `channel name ${name} is reserved for workspaces`);
396
+ // #366: 这两句会原样显示在客户端改名/新建对话框的错误条里,因此和同页其它校验一样说中文。
397
+ if (new RegExp(`^${WORKSPACE_PREFIX}\\d+$`).test(name)) throw new CollabError('conflict', `${name} 是工作区保留名,换个名字`);
397
398
  // A private chat is named after its member and listed with its avatar, so it does not clash.
398
399
  const clash = this.rooms().find(r => r.id !== selfId && r.kind === 'chat' && r.dm_member == null && r.name.toLowerCase() === name.toLowerCase());
399
- if (clash !== undefined) throw new CollabError('conflict', `channel name ${name} is already in use`);
400
+ if (clash !== undefined) throw new CollabError('conflict', `频道名 ${name} 已被占用`);
400
401
  }
401
402
  /** Rename the channel without changing any room, message or session identity. */
402
403
  renameRoom(id, name) {
403
404
  if (typeof name !== 'string' || !name || name.length > 32 || /\s/.test(name)) {
404
- throw new CollabError('invalid', 'room name must be 1-32 characters without spaces');
405
+ throw new CollabError('invalid', '频道名 1–32 个字符,且不能有空格');
405
406
  }
406
407
  const room = this.room(id);
407
- if (room === undefined) throw new CollabError('room-missing', `room ${id} does not exist`);
408
- if (room.kind === 'workspace') throw new CollabError('invalid', 'workspace names cannot be changed');
408
+ if (room === undefined) throw new CollabError('room-missing', `频道 ${id} 不存在`);
409
+ // 工作区名是系统分配的(工作区<n>),改名会和一个地址撞名,所以服务端直接拒绝。
410
+ if (room.kind === 'workspace') throw new CollabError('invalid', '工作区名称由系统分配,不能修改');
409
411
  this.checkRoomName(name, id);
410
412
  return this.db.prepare('UPDATE rooms SET name = ? WHERE id = ? RETURNING id, name, kind, parent_id').get(name, id);
411
413
  }
@@ -1,4 +1,4 @@
1
- // dsh-client-source-sha256: bf3c01b19e0ed8f9d15a02ad7d7e9e9684db4fcbf32f43ba251b220ff57aca56
1
+ // dsh-client-source-sha256: def898ca7c3b5cbc9806630df8269a00d441300d83397256c948bcbf8313e7f7
2
2
  // dsh-style-sha256: f6a6de6373a2ead4c3a965e58c158c16626e49a16b91d2d568b35b19fcfdb95a
3
3
  window.__ModuleLoader__.load({
4
4
  id: "bothouse-dsh-channels",
@@ -20541,13 +20541,13 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
20541
20541
  const [name, setName] = (0, react.useState)(room.name);
20542
20542
  const [mode, setMode] = (0, react.useState)(room.dispatch_mode ?? "mixed");
20543
20543
  const [relay, setRelay] = (0, react.useState)(String(room.max_relay ?? RELAY_CAP_MIN));
20544
- const [lead, setLeadDraft] = (0, react.useState)(effectiveLead);
20544
+ const [lead, setLeadDraft] = (0, react.useState)(room.lead_id === void 0 ? effectiveLead : room.lead_id ?? "");
20545
20545
  const [directory, setDirectory] = (0, react.useState)(room.workspace_path ?? "");
20546
20546
  const [saved, setSaved] = (0, react.useState)({
20547
20547
  name: room.name,
20548
20548
  mode: room.dispatch_mode ?? "mixed",
20549
20549
  relay: String(room.max_relay ?? RELAY_CAP_MIN),
20550
- lead: effectiveLead,
20550
+ lead: room.lead_id === void 0 ? effectiveLead : room.lead_id ?? "",
20551
20551
  directory: room.workspace_path ?? ""
20552
20552
  });
20553
20553
  const [saving, setSaving] = (0, react.useState)({});
@@ -20776,7 +20776,6 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
20776
20776
  },
20777
20777
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
20778
20778
  value: "",
20779
- disabled: true,
20780
20779
  children: t("leadNone")
20781
20780
  }), members.filter((member) => member.kind !== "human").map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
20782
20781
  value: member.id,
@@ -24431,7 +24430,7 @@ const zh = {
24431
24430
  dispatchTeam: "完全团队 — 交给团队成员协作",
24432
24431
  dispatchMixed: "混合 — 按任务选择模型与成员",
24433
24432
  lead: "主角色",
24434
- leadNone: "未选择(按原配置)",
24433
+ leadNone: "未选择(单成员自动接话,多成员不接话)",
24435
24434
  durSeconds: "{n} 秒",
24436
24435
  durMinutes: "{n} 分钟",
24437
24436
  durHours: "{h} 小时 {m} 分",
@@ -24715,7 +24714,7 @@ const en = {
24715
24714
  taskReviewReason: "Reason for rework (required)",
24716
24715
  taskReviewSubmit: "Submit rework",
24717
24716
  lead: "Primary role",
24718
- leadNone: "Not selected (use the default)",
24717
+ leadNone: "No selection (one member replies; multiple stay quiet)",
24719
24718
  durSeconds: "{n}s",
24720
24719
  durMinutes: "{n} min",
24721
24720
  durHours: "{h} h {m} min",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bothouse-dsh",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
5
5
  "type": "module",
6
6
  "main": "./collab/plugin.mjs",
@@ -51,20 +51,17 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@agentclientprotocol/sdk": "1.4.0",
54
- "@zed-industries/claude-code-acp": "^0.16.2",
55
54
  "bothouse-dsh-channels": "0.1.0",
56
55
  "bothouse-dsh-desk": "0.1.0",
57
56
  "bothouse-dsh-devices": "0.1.0",
58
57
  "bothouse-dsh-panel": "0.1.0",
59
- "ws": "8.22.1"
58
+ "ws": "8.22.1",
59
+ "zod": "^4.0.0"
60
60
  },
61
61
  "bundleDependencies": [
62
62
  "bothouse-dsh-channels",
63
63
  "bothouse-dsh-desk",
64
64
  "bothouse-dsh-panel",
65
65
  "bothouse-dsh-devices"
66
- ],
67
- "optionalDependencies": {
68
- "@agentclientprotocol/codex-acp": "^2.2.2"
69
- }
66
+ ]
70
67
  }