@roaming-ai/dsh-group-chat 0.3.0 → 0.3.1

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.
Files changed (66) hide show
  1. package/README.md +89 -26
  2. package/lib/client.js +289 -321
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +124 -132
  5. package/lib/types/client/components/AsidePanel.d.ts +3 -19
  6. package/lib/types/client/components/NavPanel.d.ts +9 -11
  7. package/lib/types/client/components/RoleDrawer.d.ts +3 -2
  8. package/lib/types/client/components/ToolRow.d.ts +2 -2
  9. package/lib/types/client/hooks/useGroupChatState.d.ts +0 -16
  10. package/lib/types/client/lib/model.d.ts +1 -7
  11. package/lib/types/client/utils/utils.d.ts +4 -4
  12. package/lib/types/core/json.d.ts +6 -0
  13. package/lib/types/core/types.d.ts +57 -63
  14. package/lib/types/host/engine/defaults.d.ts +13 -0
  15. package/lib/types/shared/file-mention-grammar.d.ts +3 -12
  16. package/package.json +1 -1
  17. package/src/client/GroupChatPanel.tsx +7 -56
  18. package/src/client/components/AsidePanel.tsx +40 -15
  19. package/src/client/components/ConstraintList.tsx +1 -6
  20. package/src/client/components/MessageFlow.tsx +2 -1
  21. package/src/client/components/NavPanel.tsx +96 -78
  22. package/src/client/components/RoleDrawer.tsx +29 -7
  23. package/src/client/components/ToolRow.tsx +2 -2
  24. package/src/client/hooks/useComposer.ts +14 -53
  25. package/src/client/hooks/useGroupChatState.ts +2 -13
  26. package/src/client/lib/model.ts +1 -14
  27. package/src/client/utils/utils.ts +6 -6
  28. package/src/core/constraints.ts +6 -12
  29. package/src/core/errors.ts +2 -12
  30. package/src/core/json.ts +17 -0
  31. package/src/core/types.ts +27 -19
  32. package/src/host/api/actions.ts +14 -13
  33. package/src/host/engine/conversation.ts +49 -50
  34. package/src/host/engine/defaults.ts +28 -0
  35. package/src/host/engine/fold.ts +2 -17
  36. package/src/host/engine/retitle.ts +9 -31
  37. package/src/host/materials/materials.ts +2 -0
  38. package/src/host/tools/tools.ts +2 -2
  39. package/src/shared/file-mention-grammar.test.ts +1 -38
  40. package/src/shared/file-mention-grammar.ts +7 -33
  41. package/lib/types/client/Bubble.d.ts +0 -10
  42. package/lib/types/client/Glyph.d.ts +0 -12
  43. package/lib/types/client/GroupChatPanel.old.d.ts +0 -10
  44. package/lib/types/client/GroupChatSettingsSection.d.ts +0 -15
  45. package/lib/types/client/PermissionSelect.d.ts +0 -13
  46. package/lib/types/client/RoleDrawer.d.ts +0 -17
  47. package/lib/types/client/ThinkRow.d.ts +0 -9
  48. package/lib/types/client/ToolRow.d.ts +0 -9
  49. package/lib/types/client/api.d.ts +0 -8
  50. package/lib/types/client/components.d.ts +0 -22
  51. package/lib/types/client/drawer.d.ts +0 -17
  52. package/lib/types/client/glyph.d.ts +0 -12
  53. package/lib/types/client/model.d.ts +0 -78
  54. package/lib/types/client/panel.d.ts +0 -10
  55. package/lib/types/client/settings.d.ts +0 -15
  56. package/lib/types/client/styles.d.ts +0 -10
  57. package/lib/types/client/ui.d.ts +0 -17
  58. package/lib/types/client/utils.d.ts +0 -22
  59. package/lib/types/host/actions.d.ts +0 -34
  60. package/lib/types/host/conversation.d.ts +0 -25
  61. package/lib/types/host/http.d.ts +0 -16
  62. package/lib/types/host/materials.d.ts +0 -32
  63. package/lib/types/host/persistence.d.ts +0 -31
  64. package/lib/types/host/routes.d.ts +0 -11
  65. package/lib/types/host/store.d.ts +0 -65
  66. package/lib/types/host/tools.d.ts +0 -28
@@ -193,78 +193,72 @@ export interface FileSearchResult {
193
193
  }[];
194
194
  error?: string;
195
195
  }
196
- /** 发到客户端的全量快照(wire 形态)。 */
196
+ /** 快照内的会话行(wire 形态:不含 hydrate 用的固定标记与折入水位)。 */
197
+ export type SnapshotSession = Omit<SessionRecord, 'namePinned' | 'topicPinned' | 'constraintsUpToSeq'>;
198
+ /** 快照内的消息行(wire 形态:思考全文与摘要不外发)。 */
199
+ export type SnapshotMessage = Omit<MessageRecord, 'reasoningFull' | 'thinkingSummary'>;
200
+ /** 发到客户端的全量快照(wire 形态;各表行由领域记录派生,字段增删由编译器同步)。 */
197
201
  export interface Snapshot {
198
202
  revision: number;
199
- run: {
200
- running: boolean;
201
- sessionId: string | null;
202
- currentRoleId: string | null;
203
- partial: string;
204
- partialReasoning: string;
205
- pendingConfirm: PendingConfirm | null;
206
- finished: RunFinished | null;
207
- replaceMessageId: string | null;
208
- };
203
+ run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | 'childProc'>;
209
204
  lastCreated: LastCreated | null;
210
- groups: {
211
- id: string;
212
- name: string;
213
- workspaceDir: string;
214
- permissionTier: PermissionTier;
215
- roleIds: string[];
216
- sessionIds: string[];
217
- }[];
218
- sessions: {
219
- id: string;
220
- groupId: string;
221
- name: string;
222
- topic: string;
223
- constraints?: SessionConstraint[];
224
- messageIds: string[];
225
- createdAt: number;
226
- }[];
227
- roles: {
228
- id: string;
229
- groupId: string;
230
- name: string;
231
- color?: string;
232
- persona: string;
233
- provider: string;
234
- model: string;
235
- temperature?: number;
236
- reasoningEffort?: string;
237
- enabled: boolean;
238
- thinking: boolean;
239
- }[];
240
- messages: {
241
- id: string;
242
- sessionId: string;
243
- seq: number;
244
- speaker: string;
245
- text: string;
246
- reasoning?: string;
247
- model?: string;
248
- error?: boolean;
249
- failedRoleId?: string;
250
- toolCalls?: ToolCallRecord[];
251
- ts: number;
252
- }[];
205
+ groups: GroupRecord[];
206
+ sessions: SnapshotSession[];
207
+ roles: RoleRecord[];
208
+ messages: SnapshotMessage[];
253
209
  error?: string;
254
210
  }
255
- /** mutate 动作的参数形态(op 分发见 host/service)。 */
256
- export interface MutateArgs {
257
- op: string;
258
- groupId?: string;
259
- sessionId?: string;
260
- roleId?: string;
261
- role?: Partial<RoleRecord>;
211
+ /** mutate 动作的参数形态(判别联合:按 op 收窄各分支字段;wire 上字段可缺,消费方各自守卫)。 */
212
+ export type MutateArgs = {
213
+ op: 'createGroup';
262
214
  name?: string;
215
+ } | {
216
+ op: 'renameGroup';
217
+ groupId: string;
218
+ name?: string;
219
+ } | {
220
+ op: 'deleteGroup';
221
+ groupId: string;
222
+ } | {
223
+ op: 'createSession';
224
+ groupId: string;
225
+ name?: string;
226
+ } | {
227
+ op: 'renameSession';
228
+ sessionId: string;
229
+ name?: string;
230
+ } | {
231
+ op: 'deleteSession';
232
+ sessionId: string;
233
+ } | {
234
+ op: 'setTopic';
235
+ sessionId: string;
263
236
  topic?: string;
237
+ } | {
238
+ op: 'upsertRole';
239
+ groupId: string;
240
+ role?: Partial<RoleRecord>;
241
+ } | {
242
+ op: 'deleteRole';
243
+ roleId: string;
244
+ } | {
245
+ op: 'setRoleEnabled';
246
+ roleId: string;
247
+ enabled?: boolean;
248
+ } | {
249
+ op: 'setWorkspaceDir';
250
+ groupId: string;
264
251
  path?: string;
252
+ } | {
253
+ op: 'setPermissionTier';
254
+ groupId: string;
265
255
  tier?: string;
266
- enabled?: boolean;
267
- }
256
+ } | {
257
+ op: 'ackFinish';
258
+ } | {
259
+ op: 'clearMessages';
260
+ sessionId: string;
261
+ };
268
262
  /** send 动作的参数形态。 */
269
263
  export interface SendArgs {
270
264
  sessionId: string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 引擎共享小件:DSH 默认模型读取 + 发言人展示名(retitle / fold / conversation 共用)。
3
+ * @module dsh-group-chat/host/engine/defaults
4
+ */
5
+ import type { RoleRecord } from '../../core/types.ts';
6
+ import type { HostState } from '../state.ts';
7
+ /** DSH 默认模型(agentDefaultModel 服务缺位或未配置时返回 null,调用方静默跳过)。 */
8
+ export declare const defaultModel: (core: HostState) => {
9
+ provider: string;
10
+ model: string;
11
+ } | null;
12
+ /** 发言人展示名(roles 表内查角色名;user/system 固定文案,见 speakerLabel)。 */
13
+ export declare const speakerNameOf: (roles: Map<string, RoleRecord>, speaker: string) => string;
@@ -4,6 +4,9 @@
4
4
  * Sourced from @deepseek-ai/dsh-file-reference/lib/types/grammar.js
5
5
  * Cannot import directly due to client bundle purity gate (dsh-client-bundle-purity).
6
6
  *
7
+ * 只保留输入侧的 @token 识别(activeAtToken);候选的文本化(formatFileMention)
8
+ * 未被本插件采用——目录钻入走「插入整颗芯片」语义,见 docs/DESIGN.md Composer 契约。
9
+ *
7
10
  * @module dsh-group-chat/shared
8
11
  */
9
12
  export interface AtToken {
@@ -14,10 +17,6 @@ export interface AtToken {
14
17
  /** Whether the token uses quoted form @"..." */
15
18
  quoted: boolean;
16
19
  }
17
- export interface FileCandidate {
18
- kind: 'file' | 'directory';
19
- path: string;
20
- }
21
20
  /**
22
21
  * Extract active @token at cursor position.
23
22
  * Matches @ at word boundary (line start or after whitespace).
@@ -27,11 +26,3 @@ export interface FileCandidate {
27
26
  * @returns Token or undefined if no active @token at cursor
28
27
  */
29
28
  export declare function activeAtToken(line: string, cursor: number): AtToken | undefined;
30
- /**
31
- * Format file/directory candidate as @mention text.
32
- *
33
- * @param candidate - File or directory to format
34
- * @param preserveQuote - Keep quotes even when unnecessary (for drill-down continuity)
35
- * @returns Formatted @path or undefined for invalid paths
36
- */
37
- export declare function formatFileMention(candidate: FileCandidate, preserveQuote: boolean): string | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@roaming-ai/dsh-group-chat",
3
3
  "description": "DSH 模型群聊:多模型角色群组对话面板。角色绑定不同 provider/model,群内共享对话记录;设置页支持启用/停用。",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -41,16 +41,10 @@ export function GroupChatPanel(): ReactNode {
41
41
  setConfirmClear,
42
42
  roleDraft,
43
43
  setRoleDraft,
44
- roleFormError,
45
- setRoleFormError,
46
44
  models,
47
45
  setModels,
48
46
  modelsError,
49
47
  setModelsError,
50
- fileBrowser,
51
- setFileBrowser,
52
- wsDraft,
53
- setWsDraft,
54
48
  partsSel,
55
49
  setPartsSel,
56
50
  rounds,
@@ -129,11 +123,11 @@ export function GroupChatPanel(): ReactNode {
129
123
  if (busyNow || !sess || sendingRef.current) return
130
124
  sendingRef.current = true
131
125
  try {
132
- const parts = mentionedRoles.length ? mentionedRoles.map((r) => r.id) : participants
133
- if (!parts.length) {
134
- setErr(mentionedRoles.length ? '' : '请至少选择一个参与角色(或在消息中 @成员)')
135
- if (!mentionedRoles.length) return
126
+ if (!participants.length && !mentionedRoles.length) {
127
+ setErr('请至少选择一个参与角色(或在消息中 @成员)')
128
+ return
136
129
  }
130
+ const parts = mentionedRoles.length ? mentionedRoles.map((r) => r.id) : participants
137
131
  const res = await action({ kind: 'send', sessionId: sess.id, text: input, participantRoleIds: parts, rounds }) as ActionOk | null
138
132
  if (res && !res.ok && res.error) setErr(res.error)
139
133
  else if (res && res.ok) {
@@ -220,51 +214,14 @@ export function GroupChatPanel(): ReactNode {
220
214
 
221
215
  const openRoleEditor = async (role: SnapshotRole | null): Promise<void> => {
222
216
  setErr('')
223
- setRoleFormError('')
224
217
  if (!models || modelsError) await fetchModels()
225
218
  setRoleDraft(role ? draftFromRole(role) : blankDraft())
226
219
  }
227
220
 
228
- const saveRole = async (): Promise<void> => {
229
- if (!roleDraft) return
230
- if (!roleDraft.name.trim()) {
231
- setRoleFormError('角色名称不能为空')
232
- return
233
- }
234
- if (!roleDraft.provider || !roleDraft.model) {
235
- setRoleFormError('请选择角色绑定的模型')
236
- return
237
- }
238
- const res = await mutate({ op: 'upsertRole', groupId: group.id, role: roleDraft })
239
- if (res && res.ok && res.snapshot && !res.snapshot.error) {
240
- setRoleDraft(null)
241
- setRoleFormError('')
242
- }
243
- }
244
-
245
221
  const stopRun = async (): Promise<void> => {
246
222
  await action({ kind: 'stop', sessionId: sess!.id })
247
223
  }
248
224
 
249
- const openBrowser = async (path: string | undefined): Promise<void> => {
250
- setFileBrowser({ open: true, loading: true, list: null, error: '' })
251
- try {
252
- const res = await api.action({ kind: 'browse', path: path || '' }) as import('../core/types.ts').BrowseResult
253
- if (res && res.ok) setFileBrowser({ open: true, loading: false, list: res, error: '' })
254
- else setFileBrowser({ open: true, loading: false, list: null, error: (res && res.error) || '浏览失败' })
255
- } catch (e) {
256
- setFileBrowser({ open: true, loading: false, list: null, error: String((e && (e as Error).message) || e) })
257
- }
258
- }
259
-
260
- const selectCurrentDir = (): void => {
261
- if (!fileBrowser || !fileBrowser.list || !group) return
262
- const path = fileBrowser.list.path!
263
- void mutate({ op: 'setWorkspaceDir', groupId: group.id, path })
264
- setWsDraft(null)
265
- setFileBrowser(null)
266
- }
267
-
268
225
  return (
269
226
  <div className="dsgc-root">
270
227
  <NavPanel
@@ -338,14 +295,8 @@ export function GroupChatPanel(): ReactNode {
338
295
  snap={snap}
339
296
  group={group}
340
297
  asideOpen={asideOpen}
341
- wsDraft={wsDraft}
342
- setWsDraft={setWsDraft}
343
- fileBrowser={fileBrowser}
344
- setFileBrowser={setFileBrowser}
345
298
  mutate={mutate}
346
299
  openRoleEditor={openRoleEditor}
347
- openBrowser={openBrowser}
348
- selectCurrentDir={selectCurrentDir}
349
300
  />
350
301
  {sess && confirmClear
351
302
  ? (
@@ -386,12 +337,12 @@ export function GroupChatPanel(): ReactNode {
386
337
  <RoleDrawer
387
338
  draft={roleDraft}
388
339
  set={setRoleDraft}
340
+ groupId={group.id}
389
341
  models={models}
390
342
  modelsError={modelsError}
391
343
  onRetryModels={() => { void fetchModels() }}
392
- onSave={() => { void saveRole() }}
393
- onCancel={() => { setRoleDraft(null); setRoleFormError('') }}
394
- formError={roleFormError}
344
+ mutate={mutate}
345
+ onCancel={() => { setRoleDraft(null) }}
395
346
  />
396
347
  )
397
348
  : null}
@@ -1,28 +1,34 @@
1
1
  /**
2
- * 右侧成员与工作区栏组件
2
+ * 右侧成员与工作区栏组件。工作区目录编辑态(草稿、文件浏览器)为栏内自有状态。
3
3
  * @module dsh-group-chat/client/components
4
4
  */
5
5
 
6
- import type { ReactNode } from 'react'
6
+ import { useState, type ReactNode } from 'react'
7
7
  import { Icon, P } from '../lib/ui.ts'
8
- import { roleById, type ClientSnapshot, type ModelsResponse, type RoleDraft, type SnapshotRole } from '../lib/model.ts'
8
+ import { api } from '../lib/api.ts'
9
+ import { roleById, type ClientSnapshot, type SnapshotRole } from '../lib/model.ts'
10
+ import type { BrowseResult } from '../../core/types.ts'
9
11
 
10
12
  interface AsidePanelProps {
11
13
  snap: ClientSnapshot
12
14
  group: ClientSnapshot['groups'][number]
13
15
  asideOpen: boolean
14
- wsDraft: string | null
15
- setWsDraft: (val: string | null) => void
16
- fileBrowser: { open: boolean, loading: boolean, list: import('../../core/types.ts').BrowseResult | null, error: string } | null
17
- setFileBrowser: (val: { open: boolean, loading: boolean, list: import('../../core/types.ts').BrowseResult | null, error: string } | null) => void
18
16
  mutate: (args: Record<string, unknown>) => Promise<unknown>
19
- openRoleEditor: (role: SnapshotRole | null) => Promise<void>
20
- openBrowser: (path: string | undefined) => Promise<void>
21
- selectCurrentDir: () => void
17
+ openRoleEditor: (role: SnapshotRole | null) => void
18
+ }
19
+
20
+ /** 文件浏览器态(栏内自有)。 */
21
+ interface FileBrowserState {
22
+ open: boolean
23
+ loading: boolean
24
+ list: BrowseResult | null
25
+ error: string
22
26
  }
23
27
 
24
28
  export function AsidePanel(props: AsidePanelProps): ReactNode {
25
- const { snap, group, asideOpen, wsDraft, setWsDraft, fileBrowser, setFileBrowser, mutate, openRoleEditor, openBrowser, selectCurrentDir } = props
29
+ const { snap, group, asideOpen, mutate, openRoleEditor } = props
30
+ const [wsDraft, setWsDraft] = useState<string | null>(null)
31
+ const [fileBrowser, setFileBrowser] = useState<FileBrowserState | null>(null)
26
32
 
27
33
  const commitWsDir = (): void => {
28
34
  if (wsDraft !== null && group && wsDraft !== (group.workspaceDir || '')) {
@@ -31,6 +37,25 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
31
37
  setWsDraft(null)
32
38
  }
33
39
 
40
+ const openBrowser = async (path: string | undefined): Promise<void> => {
41
+ setFileBrowser({ open: true, loading: true, list: null, error: '' })
42
+ try {
43
+ const res = await api.action({ kind: 'browse', path: path || '' }) as BrowseResult
44
+ if (res && res.ok) setFileBrowser({ open: true, loading: false, list: res, error: '' })
45
+ else setFileBrowser({ open: true, loading: false, list: null, error: (res && res.error) || '浏览失败' })
46
+ } catch (e) {
47
+ setFileBrowser({ open: true, loading: false, list: null, error: String((e && (e as Error).message) || e) })
48
+ }
49
+ }
50
+
51
+ const selectCurrentDir = (): void => {
52
+ if (!fileBrowser || !fileBrowser.list || !group) return
53
+ const path = fileBrowser.list.path!
54
+ void mutate({ op: 'setWorkspaceDir', groupId: group.id, path })
55
+ setWsDraft(null)
56
+ setFileBrowser(null)
57
+ }
58
+
34
59
  return (
35
60
  <div className={'dsgc-aside' + (asideOpen ? '' : ' closed')} aria-hidden={asideOpen ? undefined : 'true'}>
36
61
  <div className="dsgc-sec">
@@ -38,7 +63,7 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
38
63
  群成员
39
64
  <span className="dsgc-secspacer" />
40
65
  <span className="dsgc-seccount">{group.roleIds.length ? group.roleIds.length + ' 个' : ''}</span>
41
- <P.Button variant="ghost" size="sm" onClick={() => { void openRoleEditor(null) }} aria-label="添加角色">
66
+ <P.Button variant="ghost" size="sm" onClick={() => { openRoleEditor(null) }} aria-label="添加角色">
42
67
  {Icon(P.IconPlusOutline16, 14)}添加
43
68
  </P.Button>
44
69
  </div>
@@ -54,8 +79,8 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
54
79
  className={'dsgc-role' + (r.enabled ? '' : ' off')}
55
80
  role="button"
56
81
  tabIndex={0}
57
- onClick={() => { void openRoleEditor(r) }}
58
- onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void openRoleEditor(r) } }}
82
+ onClick={() => { openRoleEditor(r) }}
83
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openRoleEditor(r) } }}
59
84
  title="点击编辑角色"
60
85
  >
61
86
  <div className="dsgc-rolehead">
@@ -94,7 +119,7 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
94
119
  className="dsgc-opbtn"
95
120
  title="编辑角色"
96
121
  aria-label="编辑角色"
97
- onClick={(e) => { e.stopPropagation(); void openRoleEditor(r) }}
122
+ onClick={(e) => { e.stopPropagation(); openRoleEditor(r) }}
98
123
  >
99
124
  {Icon(P.IconEditOutline16, 14)}
100
125
  </button>
@@ -5,14 +5,9 @@
5
5
 
6
6
  import { useState, type ReactNode } from 'react'
7
7
  import type { SessionConstraint } from '../../core/types.ts'
8
+ import { KIND_LABEL } from '../../core/constraints.ts'
8
9
  import { ClipWell, Fold } from './Fold.tsx'
9
10
 
10
- const KIND_LABEL: Record<SessionConstraint['kind'], string> = {
11
- decided: '已定',
12
- rejected: '否决',
13
- open: '未决',
14
- }
15
-
16
11
  interface ConstraintListProps {
17
12
  items: SessionConstraint[]
18
13
  }
@@ -87,6 +87,8 @@ export function MessageFlow(props: MessageFlowProps): ReactNode {
87
87
  let livePlaced = false
88
88
 
89
89
  if (sess) {
90
+ // 循环不变量:本群角色表只过滤一次(流式期间每帧执行的热路径)
91
+ const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId)
90
92
  const ids = sess.messageIds
91
93
  const memo = sess.constraints && sess.constraints.length
92
94
  ? <ConstraintList key={sess.id + '-constraints'} items={sess.constraints} />
@@ -100,7 +102,6 @@ export function MessageFlow(props: MessageFlowProps): ReactNode {
100
102
  if (!m) continue
101
103
  // 角色在父级解析:Bubble 按值 memo(不依赖 snap identity),流式帧
102
104
  // 不再触发已完成消息的全量重渲(每帧仅 live 行与派生列表变化)
103
- const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId)
104
105
  const role = resolveFailedRole(m, groupRoles) || (m.speaker !== 'user' && m.speaker !== 'system' ? roleById(snap, m.speaker) : null)
105
106
  // 原地重试:该槽位换成 live(确认卡紧随其后),不要钉在列表末尾
106
107
  if (replaceId && m.id === replaceId) {