dsh-bots 0.2.26 → 0.2.28

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/lib/client.js CHANGED
@@ -289,7 +289,8 @@
289
289
  row on hover and shrink it on leave — width/height jitter the user read
290
290
  as flicker. Fixed 30px row + always-laid-out actions = zero geometry
291
291
  change; the same trick the native headerActions uses (max-width/opacity). */
292
- .dbs-secHead{position:relative;cursor:pointer;border-radius:8px;height:30px}
292
+ .dbs-secHead{position:relative;cursor:pointer;border-radius:8px;height:30px;margin-bottom:4px}
293
+ .dbs-secGroup+.dbs-secGroup{margin-top:10px}
293
294
  .dbs-secHead:hover{background:var(--dsw-alias-interactive-bg-hover)}
294
295
  .dbs-secHead .dbs-rowActions{display:inline-flex;visibility:hidden;opacity:0;pointer-events:none;transition:opacity .12s ease-out}
295
296
  .dbs-secHead:hover .dbs-rowActions,.dbs-secHead:focus-within .dbs-rowActions,.dbs-secHead[data-menu="true"] .dbs-rowActions{visibility:visible;opacity:1;pointer-events:auto}
@@ -483,7 +484,7 @@
483
484
  'chat.stop.noop': '当前没有进行中的生成',
484
485
  'media.openFailed': '打开失败,文件可能已移动或被删除',
485
486
  'chat.members.manage': '管理成员',
486
- 'chat.members.cap': '已达群成员上限 {cap} 人——可先在下方调大「成员上限」再添加',
487
+ 'chat.members.cap': '已达群成员上限 {cap} 人——可在会话设置里调高',
487
488
  'chat.settings': '设置',
488
489
  'chat.settings.title': '会话设置',
489
490
  'chat.settings.name': '名称',
@@ -494,9 +495,11 @@
494
495
  'chat.settings.paths': '额外可写路径(逗号分隔)',
495
496
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
496
497
  'chat.settings.hint': '写入被沙盒限制在工作区(含额外路径)内,读取不受限;同 slug 的 Bot 共享同一目录。',
498
+ 'chat.settings.cap': '成员上限(1–16)',
499
+ 'chat.settings.capHint': '保存在该群的配置里;调低会立即截断超出部分。',
500
+ 'chat.settings.capBad': '成员上限需为 1–16 的整数',
497
501
  'modal.members.title': '管理群成员',
498
502
  'modal.members.hint': '勾选的 Bot 为群成员;保存后立即生效(可随时再改)。',
499
- 'modal.members.cap': '成员上限(1–16)',
500
503
  'action.delete': '删除',
501
504
  'delete.title.bot': '删除 Bot',
502
505
  'delete.title.group': '删除群聊',
@@ -599,7 +602,7 @@
599
602
  'chat.stop.noop': 'No generation in progress',
600
603
  'media.openFailed': 'Open failed — the file may have moved or been deleted',
601
604
  'chat.members.manage': 'Manage members',
602
- 'chat.members.cap': 'Group cap is {cap} members — raise the cap below first',
605
+ 'chat.members.cap': 'Group cap is {cap} members — raise it in session settings',
603
606
  'chat.settings': 'Settings',
604
607
  'chat.settings.title': 'Session settings',
605
608
  'chat.settings.name': 'Name',
@@ -610,9 +613,11 @@
610
613
  'chat.settings.paths': 'Extra writable paths (comma-separated)',
611
614
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
612
615
  'chat.settings.hint': 'Writes are sandboxed to the workspace (plus extra paths); reads stay unrestricted. Bots sharing a slug share one directory.',
616
+ 'chat.settings.cap': 'Member cap (1–16)',
617
+ 'chat.settings.capHint': 'Stored in this group\'s config; lowering it drops members beyond the new cap right away.',
618
+ 'chat.settings.capBad': 'The cap must be an integer from 1 to 16',
613
619
  'modal.members.title': 'Manage group members',
614
620
  'modal.members.hint': 'Checked bots are members; changes apply immediately on save (editable again anytime).',
615
- 'modal.members.cap': 'Member cap (1–16)',
616
621
  'action.delete': 'Delete',
617
622
  'delete.title.bot': 'Delete bot',
618
623
  'delete.title.group': 'Delete group chat',
@@ -1175,7 +1180,7 @@
1175
1180
  const toggle = () => {
1176
1181
  patch({ sectionMenu: null, sectionsOpen: { ...(s.sectionsOpen ?? {}), [menuKind]: !isOpen } });
1177
1182
  };
1178
- return e('div', { key: label }, e('div', {
1183
+ return e('div', { key: label, className: 'dbs-secGroup' }, e('div', {
1179
1184
  className: 'dbs-navBodyErr dbs-secHead', role: 'button', tabIndex: 0, 'aria-expanded': isOpen,
1180
1185
  'data-menu': menuOpen ? 'true' : 'false',
1181
1186
  // Native sectionHeader outdent: label at x=16 (12 sidebar
@@ -2223,7 +2228,6 @@
2223
2228
  const [picked, setPicked] = React.useState(null);
2224
2229
  const [err, setErr] = React.useState(null);
2225
2230
  const [working, setWorking] = React.useState(false);
2226
- const [capText, setCapText] = React.useState('8');
2227
2231
  // Seed once per opened group; a group vanishing mid-edit (deleted by
2228
2232
  // the swarm, say) just renders the modal inert until closed.
2229
2233
  React.useEffect(() => {
@@ -2233,18 +2237,17 @@
2233
2237
  for (const id of group.memberIds ?? [])
2234
2238
  init[id] = true;
2235
2239
  setPicked(init);
2236
- setCapText(String(group.maxMembers ?? 8));
2237
2240
  // eslint-disable-next-line react-hooks/exhaustive-deps
2238
2241
  }, [s.manageMembers]);
2239
2242
  if (group === null || group === undefined)
2240
2243
  return null;
2241
2244
  const singles = s.agents.filter((a) => !a.isGroup && a.isHiddenFromSidebar !== true);
2242
2245
  const pickedIds = picked === null ? [] : Object.keys(picked).filter((k) => picked[k]);
2243
- // Per-group cap (group.json maxMembers, default 8, engine hard max 16):
2244
- // the glue slice(0, maxMembers)-truncates the roster SILENTLY an
2245
- // over-cap save writes successfully and just drops whoever ranked past
2246
- // the cap. Surface it here instead of letting a save pretend to work.
2247
- const MEMBER_CAP = Math.min(16, Math.max(1, Math.floor(Number(capText)) || 8));
2246
+ // Per-group cap (group.json maxMembers, default 8, engine hard max 16;
2247
+ // the host enriches group rows with the real value). The glue slice(0,
2248
+ // maxMembers)-truncates the roster SILENTLY, so an over-cap pick is
2249
+ // blocked here the cap itself is edited in session settings.
2250
+ const MEMBER_CAP = group.maxMembers ?? 8;
2248
2251
  const overCap = pickedIds.length > MEMBER_CAP;
2249
2252
  function toggle(id) {
2250
2253
  if (picked === null)
@@ -2262,7 +2265,7 @@
2262
2265
  setWorking(true);
2263
2266
  setErr(null);
2264
2267
  try {
2265
- await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds, maxMembers: MEMBER_CAP });
2268
+ await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds });
2266
2269
  patch({ manageMembers: null });
2267
2270
  await refreshAgents();
2268
2271
  return;
@@ -2285,10 +2288,7 @@
2285
2288
  disabled: working,
2286
2289
  icon: Ico('IconCloseOutline16', { size: 16 }),
2287
2290
  onClick: () => patch({ manageMembers: null }),
2288
- })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-meta', style: { padding: '0 2px 6px' } }, t('modal.members.hint')), e('div', { className: 'dbs-setrow', style: { padding: '0 2px 8px', gap: 8 } }, e('span', { className: 'dbs-meta' }, t('modal.members.cap')), e(Input, {
2289
- value: capText, disabled: working, style: { width: 72 },
2290
- onChange: (ev) => setCapText(ev.target.value),
2291
- })), picked === null
2291
+ })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-meta', style: { padding: '0 2px 6px' } }, t('modal.members.hint')), picked === null
2292
2292
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.loading'))
2293
2293
  : e('div', { className: 'dbs-modalMembers' }, singles.length === 0
2294
2294
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.empty'))
@@ -2327,11 +2327,13 @@
2327
2327
  const [wsLoaded, setWsLoaded] = React.useState(false);
2328
2328
  const [working, setWorking] = React.useState(false);
2329
2329
  const [err, setErr] = React.useState(null);
2330
+ const [capText, setCapText] = React.useState('8');
2330
2331
  React.useEffect(() => {
2331
2332
  if (agent === null || agent === undefined)
2332
2333
  return;
2333
2334
  setName(String(agent.name ?? ''));
2334
2335
  setDesc(String(agent.description ?? ''));
2336
+ setCapText(String(agent.maxMembers ?? 8));
2335
2337
  if (agent.isGroup === true) {
2336
2338
  setWsLoaded(true);
2337
2339
  return;
@@ -2353,13 +2355,24 @@
2353
2355
  if (agent === null || agent === undefined)
2354
2356
  return null;
2355
2357
  const valid = name.trim() !== '';
2358
+ // Member cap lives here (session settings), not in the roster editor:
2359
+ // it is this group's own config field. Empty/unchanged = keep.
2360
+ const capNum = Math.floor(Number(capText));
2361
+ const capValid = capText.trim() === '' || (Number.isInteger(capNum) && capNum >= 1 && capNum <= 16);
2356
2362
  async function save() {
2357
2363
  if (working || valid === false)
2358
2364
  return;
2365
+ if (capValid === false) {
2366
+ setErr(t('chat.settings.capBad'));
2367
+ return;
2368
+ }
2359
2369
  setWorking(true);
2360
2370
  setErr(null);
2361
2371
  try {
2362
2372
  await botsCall('update', { id: agent.id, profile: { name: name.trim(), description: desc.trim() } });
2373
+ if (agent.isGroup === true && capText.trim() !== '') {
2374
+ await botsCall('setGroupMembers', { id: agent.id, memberIds: agent.memberIds ?? [], maxMembers: capNum });
2375
+ }
2363
2376
  if (agent.isGroup !== true && wsLoaded === true) {
2364
2377
  const list = paths.split(/[,,]/).map((x) => x.trim()).filter((x) => x !== '');
2365
2378
  await botsCall('workspaceSet', {
@@ -2393,7 +2406,12 @@
2393
2406
  })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-setLabel' }, t('chat.settings.name')), e(Input, {
2394
2407
  value: name, autoFocus: true, disabled: working,
2395
2408
  onChange: (ev) => setName(ev.target.value),
2396
- }), isGroup === false
2409
+ }), isGroup === true
2410
+ ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.cap')), e(Input, {
2411
+ value: capText, disabled: working, inputMode: 'numeric', style: { width: 96 },
2412
+ onChange: (ev) => setCapText(ev.target.value),
2413
+ }), e('div', { className: 'dbs-meta', style: { marginTop: 6 } }, t('chat.settings.capHint')))
2414
+ : null, isGroup === false
2397
2415
  ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.desc')), e(Input, {
2398
2416
  value: desc, disabled: working,
2399
2417
  onChange: (ev) => setDesc(ev.target.value),
package/lib/index.js CHANGED
@@ -214,7 +214,22 @@ export class BotsRemote extends TypertRemoteService {
214
214
  * would make the gateway reject the envelope with "unexpected request".
215
215
  */
216
216
  async list(request) {
217
- return normalizeAgents(await callGateway(this.cfg.dataDir, 'listAgents', {}));
217
+ const agents = normalizeAgents(await callGateway(this.cfg.dataDir, 'listAgents', {}));
218
+ // The gateway row omits group.json's maxMembers, so read it here — the
219
+ // settings editor needs the CURRENT cap, not a hardcoded default.
220
+ for (const a of agents) {
221
+ if (a.isGroup !== true)
222
+ continue;
223
+ try {
224
+ const cfg = JSON.parse(readFileSync(join(effectiveDataDir(this.cfg.dataDir), 'agents', a.id, 'group.json'), 'utf-8'));
225
+ const n = typeof cfg.maxMembers === 'number' ? Math.floor(cfg.maxMembers) : Number.NaN;
226
+ a.maxMembers = Number.isFinite(n) ? Math.min(16, Math.max(1, n)) : 8;
227
+ }
228
+ catch {
229
+ a.maxMembers = 8;
230
+ }
231
+ }
232
+ return agents;
218
233
  }
219
234
  async workspaces(request) {
220
235
  const registry = this.ctx.get('workspaceRegistry');
@@ -285,6 +300,7 @@ export class BotsRemote extends TypertRemoteService {
285
300
  const updated = await callGateway(this.cfg.dataDir, 'setGroupMembers', {
286
301
  id: request?.id,
287
302
  memberAgentIds: Array.isArray(request?.memberIds) ? request.memberIds : [],
303
+ ...(typeof request?.maxMembers === 'number' ? { maxMembers: request.maxMembers } : {}),
288
304
  });
289
305
  return trimAgent(updated?.agent ?? updated);
290
306
  }
@@ -91,6 +91,7 @@ export declare class BotsRemote extends TypertRemoteService {
91
91
  setGroupMembers(request: {
92
92
  id?: string;
93
93
  memberIds?: string[];
94
+ maxMembers?: number;
94
95
  } | null): Promise<AgentInfo | null>;
95
96
  /**
96
97
  * Resolve a bot-written path against the media allowlist: only files inside
@@ -20,6 +20,9 @@ export interface AgentInfo {
20
20
  title: string;
21
21
  isGroup: boolean;
22
22
  memberIds: string[];
23
+ /** Per-group roster cap (group.json maxMembers; 1-16, default 8). The
24
+ gateway row omits it, so the host enriches group rows from disk. */
25
+ maxMembers?: number;
23
26
  isRunning: boolean;
24
27
  isActive: boolean;
25
28
  lastMessagePreview: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bots",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Multi-bot workbench for DeepSeek Harness: bridges the sdk-bots orchestration gateway (group chats, single bots) into the dsh web shell with official-styled UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",