dsh-bots 0.2.27 → 0.2.29

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
@@ -320,6 +320,7 @@
320
320
  .dbs-avatar{width:20px;height:20px;border-radius:6px;flex:none;display:grid;place-items:center;font-size:11px;line-height:1;font-weight:600;color:#fff;overflow:hidden;user-select:none}
321
321
  .dbs-avatar.dbs-group{border-radius:999px}
322
322
  .dbs-avatar img{width:100%;height:100%;object-fit:cover;display:block}
323
+ .dbs-avatarRound{border-radius:999px}
323
324
  .dbs-badge{min-width:16px;height:16px;padding:0 5px;border-radius:999px;background:var(--dsw-alias-state-business-primary,#1a6dff);color:#fff;font-size:11px;line-height:16px;text-align:center;flex:none;font-variant-numeric:tabular-nums}
324
325
  .dbs-railBtn{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0}
325
326
  .dbs-railBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -484,7 +485,7 @@
484
485
  'chat.stop.noop': '当前没有进行中的生成',
485
486
  'media.openFailed': '打开失败,文件可能已移动或被删除',
486
487
  'chat.members.manage': '管理成员',
487
- 'chat.members.cap': '已达群成员上限 {cap} 人——可先在下方调大「成员上限」再添加',
488
+ 'chat.members.cap': '已达群成员上限 {cap} 人——可在会话设置里调高',
488
489
  'chat.settings': '设置',
489
490
  'chat.settings.title': '会话设置',
490
491
  'chat.settings.name': '名称',
@@ -495,9 +496,11 @@
495
496
  'chat.settings.paths': '额外可写路径(逗号分隔)',
496
497
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
497
498
  'chat.settings.hint': '写入被沙盒限制在工作区(含额外路径)内,读取不受限;同 slug 的 Bot 共享同一目录。',
499
+ 'chat.settings.cap': '成员上限(1–16)',
500
+ 'chat.settings.capHint': '保存在该群的配置里;调低会立即截断超出部分。',
501
+ 'chat.settings.capBad': '成员上限需为 1–16 的整数',
498
502
  'modal.members.title': '管理群成员',
499
503
  'modal.members.hint': '勾选的 Bot 为群成员;保存后立即生效(可随时再改)。',
500
- 'modal.members.cap': '成员上限(1–16)',
501
504
  'action.delete': '删除',
502
505
  'delete.title.bot': '删除 Bot',
503
506
  'delete.title.group': '删除群聊',
@@ -600,7 +603,7 @@
600
603
  'chat.stop.noop': 'No generation in progress',
601
604
  'media.openFailed': 'Open failed — the file may have moved or been deleted',
602
605
  'chat.members.manage': 'Manage members',
603
- 'chat.members.cap': 'Group cap is {cap} members — raise the cap below first',
606
+ 'chat.members.cap': 'Group cap is {cap} members — raise it in session settings',
604
607
  'chat.settings': 'Settings',
605
608
  'chat.settings.title': 'Session settings',
606
609
  'chat.settings.name': 'Name',
@@ -611,9 +614,11 @@
611
614
  'chat.settings.paths': 'Extra writable paths (comma-separated)',
612
615
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
613
616
  'chat.settings.hint': 'Writes are sandboxed to the workspace (plus extra paths); reads stay unrestricted. Bots sharing a slug share one directory.',
617
+ 'chat.settings.cap': 'Member cap (1–16)',
618
+ 'chat.settings.capHint': 'Stored in this group\'s config; lowering it drops members beyond the new cap right away.',
619
+ 'chat.settings.capBad': 'The cap must be an integer from 1 to 16',
614
620
  'modal.members.title': 'Manage group members',
615
621
  'modal.members.hint': 'Checked bots are members; changes apply immediately on save (editable again anytime).',
616
- 'modal.members.cap': 'Member cap (1–16)',
617
622
  'action.delete': 'Delete',
618
623
  'delete.title.bot': 'Delete bot',
619
624
  'delete.title.group': 'Delete group chat',
@@ -1087,8 +1092,11 @@
1087
1092
  const a = p.agent;
1088
1093
  const size = p.size ?? 20;
1089
1094
  const style = { width: size, height: size };
1095
+ // Conversation avatars are round (native chat look); the sidebar keeps
1096
+ // the native squircle via the base class.
1097
+ const shape = p.round === true || a.isGroup === true ? ' dbs-avatarRound' : (a.isGroup ? ' dbs-group' : '');
1090
1098
  if (typeof a.avatarDataUrl === 'string' && a.avatarDataUrl !== '') {
1091
- return e('span', { className: 'dbs-avatar' + (a.isGroup ? ' dbs-group' : ''), style }, e('img', { src: a.avatarDataUrl, alt: '' }));
1099
+ return e('span', { className: 'dbs-avatar' + shape, style }, e('img', { src: a.avatarDataUrl, alt: '' }));
1092
1100
  }
1093
1101
  style.background = typeof a.avatarColor === 'string' && a.avatarColor !== ''
1094
1102
  ? a.avatarColor
@@ -1097,7 +1105,7 @@
1097
1105
  style.fontSize = '13px';
1098
1106
  const initial = (a.name ?? '').trim().slice(0, 1) || '·';
1099
1107
  return e('span', {
1100
- className: 'dbs-avatar' + (a.isGroup ? ' dbs-group' : ''), style, 'aria-hidden': true,
1108
+ className: 'dbs-avatar' + shape, style, 'aria-hidden': true,
1101
1109
  }, initial);
1102
1110
  }
1103
1111
  /** Chevron matching the shipped project row (fills in if the icon moves). */
@@ -1553,7 +1561,7 @@
1553
1561
  };
1554
1562
  const displayName = p.isGroup ? en.authorName : p.agent?.name;
1555
1563
  const authorColor = 'hsl(' + String(hueOf(avAgent.id)) + ' 55% 45%)';
1556
- return e('div', { className: 'dbs-botRow' }, e('div', { className: 'dbs-author' }, e(Avatar, { agent: avAgent, size: 22 }), displayName != null && displayName !== ''
1564
+ return e('div', { className: 'dbs-botRow' }, e('div', { className: 'dbs-author' }, e(Avatar, { agent: avAgent, size: 22, round: true }), displayName != null && displayName !== ''
1557
1565
  ? e('span', { className: 'dbs-authorName', style: { color: authorColor } }, displayName)
1558
1566
  : null, e(MsgTime, { entry: en })), e('div', { className: 'dbs-mdRow' }, e(MarkdownText, { text: en.content, streaming: en.isStreaming === true }), en.isStreaming === true ? e('span', { className: 'dbs-caret' }) : null, en.isStreaming === true ? null : e(MediaRefs, { text: en.content, files: true })));
1559
1567
  }
@@ -1813,7 +1821,7 @@
1813
1821
  variant: 'ghost', size: 'sm', title: t('action.close'), 'aria-label': t('action.close'),
1814
1822
  icon: Ico('IconCloseOutline16', { size: 16 }),
1815
1823
  onClick: () => patch({ chatAgentId: null }),
1816
- }), agent !== null ? e(Avatar, { agent, size: 24 }) : null, e('span', { className: 'dbs-chatbarName' }, agent?.name ?? t('chat.loading')), e('span', { className: 'dbs-meta' }, isGroup ? t('chat.group', { n: memberNames.length }) : t('chat.single')), e('span', { style: { flex: 1 } }), isGroup
1824
+ }), agent !== null ? e(Avatar, { agent, size: 24, round: true }) : null, e('span', { className: 'dbs-chatbarName' }, agent?.name ?? t('chat.loading')), e('span', { className: 'dbs-meta' }, isGroup ? t('chat.group', { n: memberNames.length }) : t('chat.single')), e('span', { style: { flex: 1 } }), isGroup
1817
1825
  ? e(Button, {
1818
1826
  variant: 'ghost', size: 'sm', title: t('chat.members.manage'), 'aria-label': t('chat.members.manage'),
1819
1827
  onClick: () => patch({ manageMembers: p.agentId }),
@@ -1827,7 +1835,7 @@
1827
1835
  : null, e('div', { className: 'dbs-scrollArea' }, e('div', { className: 'dbs-scrollBody', ref: scrollRef, onScroll: onScrollBody }, e('div', { className: 'dbs-scroll' }, e('div', { className: 'dbs-column' }, entries === null
1828
1836
  ? e('div', { className: 'dbs-skeleton' }, e('div', { className: 'dbs-skelRow', style: { width: '42%' } }), e('div', { className: 'dbs-skelRow', style: { width: '76%' } }), e('div', { className: 'dbs-skelRow', style: { width: '58%' } }))
1829
1837
  : list.length === 0
1830
- ? e('div', { className: 'dbs-welcome' }, agent !== null ? e(Avatar, { agent, size: 44 }) : null, e('div', { className: 'dbs-welcomeName' }, agent?.name ?? ''), typeof agent?.description === 'string' && agent.description !== ''
1838
+ ? e('div', { className: 'dbs-welcome' }, agent !== null ? e(Avatar, { agent, size: 44, round: true }) : null, e('div', { className: 'dbs-welcomeName' }, agent?.name ?? ''), typeof agent?.description === 'string' && agent.description !== ''
1831
1839
  ? e('div', { className: 'dbs-welcomeDesc' }, agent.description) : null, e('div', { className: 'dbs-welcomeHint' }, isGroup ? t('chat.empty.group') : t('chat.empty.single', { name: agent?.name ?? '' })))
1832
1840
  : thread, composing
1833
1841
  ? e('div', { className: 'dbs-typing', 'aria-label': t('chat.composing'), title: t('chat.composing') },
@@ -2224,7 +2232,6 @@
2224
2232
  const [picked, setPicked] = React.useState(null);
2225
2233
  const [err, setErr] = React.useState(null);
2226
2234
  const [working, setWorking] = React.useState(false);
2227
- const [capText, setCapText] = React.useState('8');
2228
2235
  // Seed once per opened group; a group vanishing mid-edit (deleted by
2229
2236
  // the swarm, say) just renders the modal inert until closed.
2230
2237
  React.useEffect(() => {
@@ -2234,18 +2241,17 @@
2234
2241
  for (const id of group.memberIds ?? [])
2235
2242
  init[id] = true;
2236
2243
  setPicked(init);
2237
- setCapText(String(group.maxMembers ?? 8));
2238
2244
  // eslint-disable-next-line react-hooks/exhaustive-deps
2239
2245
  }, [s.manageMembers]);
2240
2246
  if (group === null || group === undefined)
2241
2247
  return null;
2242
2248
  const singles = s.agents.filter((a) => !a.isGroup && a.isHiddenFromSidebar !== true);
2243
2249
  const pickedIds = picked === null ? [] : Object.keys(picked).filter((k) => picked[k]);
2244
- // Per-group cap (group.json maxMembers, default 8, engine hard max 16):
2245
- // the glue slice(0, maxMembers)-truncates the roster SILENTLY an
2246
- // over-cap save writes successfully and just drops whoever ranked past
2247
- // the cap. Surface it here instead of letting a save pretend to work.
2248
- const MEMBER_CAP = Math.min(16, Math.max(1, Math.floor(Number(capText)) || 8));
2250
+ // Per-group cap (group.json maxMembers, default 8, engine hard max 16;
2251
+ // the host enriches group rows with the real value). The glue slice(0,
2252
+ // maxMembers)-truncates the roster SILENTLY, so an over-cap pick is
2253
+ // blocked here the cap itself is edited in session settings.
2254
+ const MEMBER_CAP = group.maxMembers ?? 8;
2249
2255
  const overCap = pickedIds.length > MEMBER_CAP;
2250
2256
  function toggle(id) {
2251
2257
  if (picked === null)
@@ -2263,7 +2269,7 @@
2263
2269
  setWorking(true);
2264
2270
  setErr(null);
2265
2271
  try {
2266
- await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds, maxMembers: MEMBER_CAP });
2272
+ await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds });
2267
2273
  patch({ manageMembers: null });
2268
2274
  await refreshAgents();
2269
2275
  return;
@@ -2286,10 +2292,7 @@
2286
2292
  disabled: working,
2287
2293
  icon: Ico('IconCloseOutline16', { size: 16 }),
2288
2294
  onClick: () => patch({ manageMembers: null }),
2289
- })), 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, {
2290
- value: capText, disabled: working, style: { width: 72 },
2291
- onChange: (ev) => setCapText(ev.target.value),
2292
- })), picked === null
2295
+ })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-meta', style: { padding: '0 2px 6px' } }, t('modal.members.hint')), picked === null
2293
2296
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.loading'))
2294
2297
  : e('div', { className: 'dbs-modalMembers' }, singles.length === 0
2295
2298
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.empty'))
@@ -2328,11 +2331,13 @@
2328
2331
  const [wsLoaded, setWsLoaded] = React.useState(false);
2329
2332
  const [working, setWorking] = React.useState(false);
2330
2333
  const [err, setErr] = React.useState(null);
2334
+ const [capText, setCapText] = React.useState('8');
2331
2335
  React.useEffect(() => {
2332
2336
  if (agent === null || agent === undefined)
2333
2337
  return;
2334
2338
  setName(String(agent.name ?? ''));
2335
2339
  setDesc(String(agent.description ?? ''));
2340
+ setCapText(String(agent.maxMembers ?? 8));
2336
2341
  if (agent.isGroup === true) {
2337
2342
  setWsLoaded(true);
2338
2343
  return;
@@ -2354,13 +2359,24 @@
2354
2359
  if (agent === null || agent === undefined)
2355
2360
  return null;
2356
2361
  const valid = name.trim() !== '';
2362
+ // Member cap lives here (session settings), not in the roster editor:
2363
+ // it is this group's own config field. Empty/unchanged = keep.
2364
+ const capNum = Math.floor(Number(capText));
2365
+ const capValid = capText.trim() === '' || (Number.isInteger(capNum) && capNum >= 1 && capNum <= 16);
2357
2366
  async function save() {
2358
2367
  if (working || valid === false)
2359
2368
  return;
2369
+ if (capValid === false) {
2370
+ setErr(t('chat.settings.capBad'));
2371
+ return;
2372
+ }
2360
2373
  setWorking(true);
2361
2374
  setErr(null);
2362
2375
  try {
2363
2376
  await botsCall('update', { id: agent.id, profile: { name: name.trim(), description: desc.trim() } });
2377
+ if (agent.isGroup === true && capText.trim() !== '') {
2378
+ await botsCall('setGroupMembers', { id: agent.id, memberIds: agent.memberIds ?? [], maxMembers: capNum });
2379
+ }
2364
2380
  if (agent.isGroup !== true && wsLoaded === true) {
2365
2381
  const list = paths.split(/[,,]/).map((x) => x.trim()).filter((x) => x !== '');
2366
2382
  await botsCall('workspaceSet', {
@@ -2394,7 +2410,12 @@
2394
2410
  })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-setLabel' }, t('chat.settings.name')), e(Input, {
2395
2411
  value: name, autoFocus: true, disabled: working,
2396
2412
  onChange: (ev) => setName(ev.target.value),
2397
- }), isGroup === false
2413
+ }), isGroup === true
2414
+ ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.cap')), e(Input, {
2415
+ value: capText, disabled: working, inputMode: 'numeric', style: { width: 96 },
2416
+ onChange: (ev) => setCapText(ev.target.value),
2417
+ }), e('div', { className: 'dbs-meta', style: { marginTop: 6 } }, t('chat.settings.capHint')))
2418
+ : null, isGroup === false
2398
2419
  ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.desc')), e(Input, {
2399
2420
  value: desc, disabled: working,
2400
2421
  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.27",
3
+ "version": "0.2.29",
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",