@roaming-ai/dsh-group-chat 0.2.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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +3308 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +2583 -0
  7. package/lib/types/client/Bubble.d.ts +10 -0
  8. package/lib/types/client/Glyph.d.ts +12 -0
  9. package/lib/types/client/GroupChatPanel.d.ts +10 -0
  10. package/lib/types/client/GroupChatPanel.old.d.ts +10 -0
  11. package/lib/types/client/GroupChatSettingsSection.d.ts +15 -0
  12. package/lib/types/client/PermissionSelect.d.ts +13 -0
  13. package/lib/types/client/RoleDrawer.d.ts +17 -0
  14. package/lib/types/client/ThinkRow.d.ts +9 -0
  15. package/lib/types/client/ToolRow.d.ts +9 -0
  16. package/lib/types/client/api.d.ts +8 -0
  17. package/lib/types/client/components/AsidePanel.d.ts +31 -0
  18. package/lib/types/client/components/Bubble.d.ts +19 -0
  19. package/lib/types/client/components/ChatPanel.d.ts +57 -0
  20. package/lib/types/client/components/Composer.d.ts +48 -0
  21. package/lib/types/client/components/Glyph.d.ts +12 -0
  22. package/lib/types/client/components/GroupChatSettingsSection.d.ts +15 -0
  23. package/lib/types/client/components/MessageFlow.d.ts +15 -0
  24. package/lib/types/client/components/NavPanel.d.ts +40 -0
  25. package/lib/types/client/components/PermissionSelect.d.ts +13 -0
  26. package/lib/types/client/components/RoleDrawer.d.ts +17 -0
  27. package/lib/types/client/components/ThinkRow.d.ts +9 -0
  28. package/lib/types/client/components/ToolRow.d.ts +9 -0
  29. package/lib/types/client/components/index.d.ts +9 -0
  30. package/lib/types/client/components.d.ts +22 -0
  31. package/lib/types/client/drawer.d.ts +17 -0
  32. package/lib/types/client/glyph.d.ts +12 -0
  33. package/lib/types/client/hooks/index.d.ts +6 -0
  34. package/lib/types/client/hooks/useComposer.d.ts +27 -0
  35. package/lib/types/client/hooks/useFileSearch.d.ts +19 -0
  36. package/lib/types/client/hooks/useGroupChatState.d.ts +95 -0
  37. package/lib/types/client/hooks/useMentionState.d.ts +27 -0
  38. package/lib/types/client/index.d.ts +52 -0
  39. package/lib/types/client/lib/api.d.ts +8 -0
  40. package/lib/types/client/lib/model.d.ts +80 -0
  41. package/lib/types/client/lib/styles.d.ts +10 -0
  42. package/lib/types/client/lib/ui.d.ts +17 -0
  43. package/lib/types/client/model.d.ts +78 -0
  44. package/lib/types/client/panel.d.ts +10 -0
  45. package/lib/types/client/settings.d.ts +15 -0
  46. package/lib/types/client/styles.d.ts +10 -0
  47. package/lib/types/client/ui.d.ts +17 -0
  48. package/lib/types/client/utils/utils.d.ts +29 -0
  49. package/lib/types/client/utils.d.ts +22 -0
  50. package/lib/types/core/json.d.ts +9 -0
  51. package/lib/types/core/status.d.ts +18 -0
  52. package/lib/types/core/tools.d.ts +59 -0
  53. package/lib/types/core/types.d.ts +256 -0
  54. package/lib/types/host/actions.d.ts +34 -0
  55. package/lib/types/host/api/actions.d.ts +36 -0
  56. package/lib/types/host/api/http.d.ts +16 -0
  57. package/lib/types/host/api/index.d.ts +7 -0
  58. package/lib/types/host/api/routes.d.ts +11 -0
  59. package/lib/types/host/broadcast.d.ts +20 -0
  60. package/lib/types/host/conversation.d.ts +25 -0
  61. package/lib/types/host/engine/conversation.d.ts +25 -0
  62. package/lib/types/host/engine/index.d.ts +7 -0
  63. package/lib/types/host/engine/retitle.d.ts +20 -0
  64. package/lib/types/host/http.d.ts +16 -0
  65. package/lib/types/host/index.d.ts +8 -0
  66. package/lib/types/host/materials/index.d.ts +6 -0
  67. package/lib/types/host/materials/materials.d.ts +40 -0
  68. package/lib/types/host/materials.d.ts +32 -0
  69. package/lib/types/host/persistence/index.d.ts +6 -0
  70. package/lib/types/host/persistence/persistence.d.ts +31 -0
  71. package/lib/types/host/persistence/store.d.ts +65 -0
  72. package/lib/types/host/persistence.d.ts +31 -0
  73. package/lib/types/host/routes.d.ts +11 -0
  74. package/lib/types/host/service.d.ts +28 -0
  75. package/lib/types/host/state.d.ts +45 -0
  76. package/lib/types/host/store.d.ts +65 -0
  77. package/lib/types/host/tools/index.d.ts +6 -0
  78. package/lib/types/host/tools/tools.d.ts +28 -0
  79. package/lib/types/host/tools.d.ts +28 -0
  80. package/lib/types/index.d.ts +41 -0
  81. package/lib/types/mount-once.d.ts +9 -0
  82. package/lib/types/shared/file-mention-grammar.d.ts +37 -0
  83. package/lib/types/shared/file-mention-grammar.test.d.ts +5 -0
  84. package/package.json +98 -0
  85. package/src/client/GroupChatPanel.tsx +385 -0
  86. package/src/client/components/AsidePanel.tsx +188 -0
  87. package/src/client/components/Bubble.tsx +76 -0
  88. package/src/client/components/ChatPanel.tsx +214 -0
  89. package/src/client/components/Composer.tsx +238 -0
  90. package/src/client/components/Glyph.tsx +27 -0
  91. package/src/client/components/GroupChatSettingsSection.tsx +74 -0
  92. package/src/client/components/MessageFlow.tsx +97 -0
  93. package/src/client/components/NavPanel.tsx +249 -0
  94. package/src/client/components/PermissionSelect.tsx +159 -0
  95. package/src/client/components/RoleDrawer.tsx +186 -0
  96. package/src/client/components/ThinkRow.tsx +32 -0
  97. package/src/client/components/ToolRow.tsx +51 -0
  98. package/src/client/components/index.ts +10 -0
  99. package/src/client/hooks/index.ts +7 -0
  100. package/src/client/hooks/useComposer.ts +272 -0
  101. package/src/client/hooks/useFileSearch.ts +115 -0
  102. package/src/client/hooks/useGroupChatState.ts +214 -0
  103. package/src/client/hooks/useMentionState.ts +56 -0
  104. package/src/client/index.ts +110 -0
  105. package/src/client/lib/api.ts +23 -0
  106. package/src/client/lib/model.ts +119 -0
  107. package/src/client/lib/styles.ts +317 -0
  108. package/src/client/lib/ui.ts +27 -0
  109. package/src/client/react-dom-shim.d.ts +13 -0
  110. package/src/client/utils/utils.ts +104 -0
  111. package/src/core/json.ts +28 -0
  112. package/src/core/status.ts +33 -0
  113. package/src/core/tools.ts +41 -0
  114. package/src/core/types.ts +222 -0
  115. package/src/host/api/actions.ts +335 -0
  116. package/src/host/api/http.ts +80 -0
  117. package/src/host/api/index.ts +8 -0
  118. package/src/host/api/routes.ts +95 -0
  119. package/src/host/broadcast.ts +65 -0
  120. package/src/host/engine/conversation.ts +331 -0
  121. package/src/host/engine/index.ts +8 -0
  122. package/src/host/engine/retitle.ts +135 -0
  123. package/src/host/index.ts +9 -0
  124. package/src/host/materials/index.ts +7 -0
  125. package/src/host/materials/materials.ts +193 -0
  126. package/src/host/persistence/index.ts +7 -0
  127. package/src/host/persistence/persistence.ts +311 -0
  128. package/src/host/persistence/store.ts +375 -0
  129. package/src/host/service.ts +78 -0
  130. package/src/host/state.ts +85 -0
  131. package/src/host/tools/index.ts +7 -0
  132. package/src/host/tools/tools.ts +194 -0
  133. package/src/index.ts +99 -0
  134. package/src/mount-once.ts +49 -0
  135. package/src/shared/file-mention-grammar.test.ts +77 -0
  136. package/src/shared/file-mention-grammar.ts +82 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * 中央会话面板组件
3
+ * @module dsh-group-chat/client/components
4
+ */
5
+
6
+ import type { ReactNode } from 'react'
7
+ import { Icon, P } from '../lib/ui.ts'
8
+ import { MessageFlow } from './MessageFlow.tsx'
9
+ import { Composer } from './Composer.tsx'
10
+ import type { ClientSnapshot, SnapshotRole } from '../lib/model.ts'
11
+ import type { AtToken } from '../../shared/file-mention-grammar.ts'
12
+
13
+ interface ChatPanelProps {
14
+ snap: ClientSnapshot
15
+ sess: ClientSnapshot['sessions'][number] | null
16
+ group: ClientSnapshot['groups'][number]
17
+ enabledRoles: SnapshotRole[]
18
+ participants: string[]
19
+ mentionedRoles: SnapshotRole[]
20
+ busyNow: boolean
21
+ input: string
22
+ mention: AtToken | null
23
+ mentionCandidates: SnapshotRole[]
24
+ mentionIdxC: number
25
+ fileCandidates: Array<{ path: string, isDir: boolean }>
26
+ fileSearchError: string | null
27
+ fileSearchLoading: boolean
28
+ rounds: number
29
+ err: string
30
+ atBottom: boolean
31
+ topicDraft: string | null
32
+ msgById: Record<string, ClientSnapshot['messages'][number]>
33
+ navOpen: boolean
34
+ asideOpen: boolean
35
+ inputRef: React.RefObject<HTMLDivElement>
36
+ scrollRef: React.RefObject<HTMLDivElement>
37
+ setTopicDraft: (val: string | null) => void
38
+ setConfirmClear: (val: boolean) => void
39
+ setMentionIdx: (val: number) => void
40
+ setRounds: (val: number) => void
41
+ setNavOpen: (val: boolean) => void
42
+ setAsideOpen: (val: boolean) => void
43
+ togglePart: (rid: string) => void
44
+ onMsgsScroll: () => void
45
+ onInputCE: () => void
46
+ onInputKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void
47
+ onPasteCE: (e: React.ClipboardEvent<HTMLDivElement>) => void
48
+ onDropCE: (e: React.DragEvent<HTMLDivElement>) => void
49
+ onDragOverCE: (e: React.DragEvent<HTMLDivElement>) => void
50
+ insertChip: (role: SnapshotRole) => void
51
+ insertFileChip: (path: string, kind: 'file' | 'directory') => void
52
+ sendMsg: () => Promise<void>
53
+ stopRun: () => Promise<void>
54
+ action: (payload: Record<string, unknown>) => Promise<unknown>
55
+ mutate: (args: Record<string, unknown>) => Promise<unknown>
56
+ setMention: (val: AtToken | null) => void
57
+ }
58
+
59
+ export function ChatPanel(props: ChatPanelProps): ReactNode {
60
+ const {
61
+ snap,
62
+ sess,
63
+ group,
64
+ enabledRoles,
65
+ participants,
66
+ mentionedRoles,
67
+ busyNow,
68
+ input,
69
+ mention,
70
+ mentionCandidates,
71
+ mentionIdxC,
72
+ fileCandidates,
73
+ fileSearchError,
74
+ fileSearchLoading,
75
+ rounds,
76
+ err,
77
+ atBottom,
78
+ topicDraft,
79
+ msgById,
80
+ navOpen,
81
+ asideOpen,
82
+ inputRef,
83
+ scrollRef,
84
+ setTopicDraft,
85
+ setConfirmClear,
86
+ setMentionIdx,
87
+ setRounds,
88
+ setNavOpen,
89
+ setAsideOpen,
90
+ togglePart,
91
+ onMsgsScroll,
92
+ onInputCE,
93
+ onInputKeyDown,
94
+ onPasteCE,
95
+ onDropCE,
96
+ onDragOverCE,
97
+ insertChip,
98
+ insertFileChip,
99
+ sendMsg,
100
+ stopRun,
101
+ action,
102
+ mutate,
103
+ setMention,
104
+ } = props
105
+
106
+ const commitTopic = (): void => {
107
+ if (topicDraft !== null && sess && topicDraft !== sess.topic) void mutate({ op: 'setTopic', sessionId: sess.id, topic: topicDraft })
108
+ setTopicDraft(null)
109
+ }
110
+
111
+ const bubbles = (
112
+ <MessageFlow
113
+ snap={snap}
114
+ sess={sess}
115
+ busyNow={busyNow}
116
+ msgById={msgById}
117
+ action={action}
118
+ />
119
+ )
120
+
121
+ return (
122
+ <section className="dsgc-chat">
123
+ {/* 接缝收合钮 */}
124
+ <button
125
+ type="button"
126
+ className={'dsgc-seambtn left' + (navOpen ? '' : ' closed')}
127
+ aria-label={navOpen ? '收起群组导航栏' : '展开群组导航栏'}
128
+ title={navOpen ? '收起群组导航栏' : '展开群组导航栏'}
129
+ aria-expanded={navOpen}
130
+ onClick={() => { setNavOpen(!navOpen) }}
131
+ >
132
+ {Icon(P.IconChevronLeftOutline14, 16)}
133
+ </button>
134
+ <button
135
+ type="button"
136
+ className={'dsgc-seambtn right' + (asideOpen ? '' : ' closed')}
137
+ aria-label={asideOpen ? '收起成员与工作区栏' : '展开成员与工作区栏'}
138
+ title={asideOpen ? '收起成员与工作区栏' : '展开成员与工作区栏'}
139
+ aria-expanded={asideOpen}
140
+ onClick={() => { setAsideOpen(!asideOpen) }}
141
+ >
142
+ {Icon(P.IconChevronRightOutline14, 16)}
143
+ </button>
144
+
145
+ <div className="dsgc-chathead">
146
+ {sess ? <span className="dsgc-sess-title" title={'当前会话:' + sess.name}>{sess.name}</span> : null}
147
+ <input
148
+ className="dsgc-topic"
149
+ value={topicDraft === null ? (sess ? sess.topic : '') : topicDraft}
150
+ placeholder="设置本会话主题(可选)…"
151
+ onChange={(e) => { setTopicDraft(e.target.value) }}
152
+ onBlur={commitTopic}
153
+ onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() }}
154
+ />
155
+ <P.Button variant="ghost" size="sm" className="dsgc-clearbtn" title="清空当前会话的消息记录" onClick={() => { if (sess) setConfirmClear(true) }}>
156
+ 清空
157
+ </P.Button>
158
+ </div>
159
+
160
+ <div className="dsgc-msgs" ref={scrollRef} onScroll={onMsgsScroll}>
161
+ {sess && sess.messageIds.length > 0
162
+ ? bubbles
163
+ : (
164
+ <div className="dsgc-empty">
165
+ {Icon(P.IconSparkle16, 20)}
166
+ <div className="dsgc-emptytitle">{sess && sess.topic ? '「' + sess.topic + '」' : '会话已就绪'}</div>
167
+ <div className="dsgc-hint">
168
+ {enabledRoles.length
169
+ ? '发送消息开始讨论;@成员 点名让其优先回应;留空直接发送可让角色自由讨论——每轮全体参与角色按顺序各发言一次,可用右下角轮数控制(1–10 轮)'
170
+ : '先在右侧添加角色(每个角色可绑定不同模型),再回到这里发起讨论。'}
171
+ </div>
172
+ </div>
173
+ )}
174
+ </div>
175
+
176
+ <Composer
177
+ snap={snap}
178
+ sess={sess}
179
+ group={group}
180
+ enabledRoles={enabledRoles}
181
+ participants={participants}
182
+ mentionedRoles={mentionedRoles}
183
+ busyNow={busyNow}
184
+ input={input}
185
+ mention={mention}
186
+ mentionCandidates={mentionCandidates}
187
+ mentionIdxC={mentionIdxC}
188
+ fileCandidates={fileCandidates}
189
+ fileSearchError={fileSearchError}
190
+ fileSearchLoading={fileSearchLoading}
191
+ rounds={rounds}
192
+ err={err}
193
+ atBottom={atBottom}
194
+ bubblesLength={sess?.messageIds.length || 0}
195
+ inputRef={inputRef}
196
+ scrollRef={scrollRef}
197
+ setMentionIdx={setMentionIdx}
198
+ setRounds={setRounds}
199
+ togglePart={togglePart}
200
+ onInputCE={onInputCE}
201
+ onInputKeyDown={onInputKeyDown}
202
+ onPasteCE={onPasteCE}
203
+ onDropCE={onDropCE}
204
+ onDragOverCE={onDragOverCE}
205
+ insertChip={insertChip}
206
+ insertFileChip={insertFileChip}
207
+ sendMsg={sendMsg}
208
+ stopRun={stopRun}
209
+ mutate={mutate}
210
+ setMention={setMention}
211
+ />
212
+ </section>
213
+ )
214
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * 消息输入编辑器组件
3
+ * @module dsh-group-chat/client/components
4
+ */
5
+
6
+ import type { ReactNode, KeyboardEvent as ReactKeyboardEvent, ClipboardEvent as ReactClipboardEvent, DragEvent as ReactDragEvent } from 'react'
7
+ import { Icon, P } from '../lib/ui.ts'
8
+ import { PermissionSelect } from './PermissionSelect.tsx'
9
+ import { escapeRegExp, type ClientSnapshot, type SnapshotRole } from '../lib/model.ts'
10
+ import type { AtToken } from '../../shared/file-mention-grammar.ts'
11
+
12
+ interface ComposerProps {
13
+ snap: ClientSnapshot
14
+ sess: ClientSnapshot['sessions'][number] | null
15
+ group: ClientSnapshot['groups'][number]
16
+ enabledRoles: SnapshotRole[]
17
+ participants: string[]
18
+ mentionedRoles: SnapshotRole[]
19
+ busyNow: boolean
20
+ input: string
21
+ mention: AtToken | null
22
+ mentionCandidates: SnapshotRole[]
23
+ mentionIdxC: number
24
+ fileCandidates: Array<{ path: string, isDir: boolean }>
25
+ fileSearchError: string | null
26
+ fileSearchLoading: boolean
27
+ rounds: number
28
+ err: string
29
+ atBottom: boolean
30
+ bubblesLength: number
31
+ inputRef: React.RefObject<HTMLDivElement>
32
+ scrollRef: React.RefObject<HTMLDivElement>
33
+ setMentionIdx: (val: number) => void
34
+ setRounds: (val: number) => void
35
+ togglePart: (rid: string) => void
36
+ onInputCE: () => void
37
+ onInputKeyDown: (e: ReactKeyboardEvent<HTMLDivElement>) => void
38
+ onPasteCE: (e: ReactClipboardEvent<HTMLDivElement>) => void
39
+ onDropCE: (e: ReactDragEvent<HTMLDivElement>) => void
40
+ onDragOverCE: (e: ReactDragEvent<HTMLDivElement>) => void
41
+ insertChip: (role: SnapshotRole) => void
42
+ insertFileChip: (path: string, kind: 'file' | 'directory') => void
43
+ sendMsg: () => Promise<void>
44
+ stopRun: () => Promise<void>
45
+ mutate: (args: Record<string, unknown>) => Promise<unknown>
46
+ setMention: (val: AtToken | null) => void
47
+ }
48
+
49
+ export function Composer(props: ComposerProps): ReactNode {
50
+ const {
51
+ snap,
52
+ sess,
53
+ group,
54
+ enabledRoles,
55
+ participants,
56
+ mentionedRoles,
57
+ busyNow,
58
+ input,
59
+ mention,
60
+ mentionCandidates,
61
+ mentionIdxC,
62
+ fileCandidates,
63
+ fileSearchError,
64
+ fileSearchLoading,
65
+ rounds,
66
+ err,
67
+ atBottom,
68
+ bubblesLength,
69
+ inputRef,
70
+ scrollRef,
71
+ setMentionIdx,
72
+ setRounds,
73
+ togglePart,
74
+ onInputCE,
75
+ onInputKeyDown,
76
+ onPasteCE,
77
+ onDropCE,
78
+ onDragOverCE,
79
+ insertChip,
80
+ insertFileChip,
81
+ sendMsg,
82
+ stopRun,
83
+ mutate,
84
+ setMention,
85
+ } = props
86
+
87
+ return (
88
+ <div className="dsgc-composer">
89
+ {!atBottom && bubblesLength
90
+ ? (
91
+ <div className="dsgc-tobottom">
92
+ <button
93
+ type="button"
94
+ className="dsgc-tobtn"
95
+ onClick={() => { const el = scrollRef.current; if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) }}
96
+ >
97
+ {Icon(P.IconChevronDownOutline14, 14)}回到底部
98
+ </button>
99
+ </div>
100
+ )
101
+ : null}
102
+ {err ? <div className="dsgc-err">{err}</div> : null}
103
+ <div className="dsgc-parts">
104
+ <span className="dsgc-partslabel">参与角色</span>
105
+ {mentionedRoles.length
106
+ ? <span className="dsgc-partslabel">已 @ {mentionedRoles.map((r) => r.name).join('、')}(本轮仅被点名成员发言)</span>
107
+ : enabledRoles.length
108
+ ? enabledRoles.map((r) => (
109
+ <button
110
+ key={r.id}
111
+ type="button"
112
+ className={'dsgc-partchip' + (participants.includes(r.id) ? ' on' : '')}
113
+ onClick={() => togglePart(r.id)}
114
+ disabled={busyNow}
115
+ title={busyNow ? '对话进行中,暂停调整' : '点击切换本轮是否参与'}
116
+ >
117
+ <span className="dsgc-chipdot" style={{ background: r.color || '#888' }} />
118
+ {r.name}
119
+ </button>
120
+ ))
121
+ : <span className="dsgc-hint">还没有启用的角色,请在右侧添加</span>}
122
+ </div>
123
+ <div className="dsgc-card">
124
+ <div className="dsgc-mentionwrap">
125
+ {mention !== null && mention.query === '' && mentionCandidates.length > 0
126
+ ? (
127
+ <div className="dsgc-mention" role="listbox">
128
+ {mentionCandidates.map((r, i) => (
129
+ <button
130
+ key={r.id}
131
+ type="button"
132
+ className={'dsgc-mentionitem' + (i === mentionIdxC ? ' on' : '')}
133
+ role="option"
134
+ aria-selected={i === mentionIdxC ? 'true' : 'false'}
135
+ onMouseDown={(e) => { e.preventDefault() }}
136
+ onClick={() => { insertChip(r) }}
137
+ onMouseEnter={() => setMentionIdx(i)}
138
+ >
139
+ <span className="dsgc-chipdot" style={{ background: r.color || '#888' }} />
140
+ <span className="dsgc-mentionname">{r.name}</span>
141
+ <span className="dsgc-mentionmodel">{r.provider} / {r.model}</span>
142
+ </button>
143
+ ))}
144
+ <span className="dsgc-mentionhint">↑↓ 选择 · Enter/Tab 插入</span>
145
+ </div>
146
+ )
147
+ : mention !== null && mention.query !== ''
148
+ ? (
149
+ <div className="dsgc-mention dsgc-mention-file" role="listbox">
150
+ {fileCandidates.length
151
+ ? fileCandidates.map((item, i) => (
152
+ <button
153
+ key={item.path}
154
+ type="button"
155
+ className={'dsgc-mentionitem' + (i === mentionIdxC ? ' on' : '')}
156
+ role="option"
157
+ aria-selected={i === mentionIdxC ? 'true' : 'false'}
158
+ onMouseDown={(e) => { e.preventDefault() }}
159
+ onClick={() => {
160
+ insertFileChip(item.path, item.isDir ? 'directory' : 'file')
161
+ }}
162
+ onMouseEnter={() => setMentionIdx(i)}
163
+ >
164
+ <span className={'dsgc-fileglyph' + (item.isDir ? ' dir' : ' file')} aria-hidden="true">
165
+ {item.isDir
166
+ ? <P.FileTypeIcon kind="folder" size={16} />
167
+ : <P.FileTypeIcon path={item.path} size={16} />}
168
+ </span>
169
+ <span className="dsgc-mentionname">{item.path}</span>
170
+ {item.isDir ? <span className="dsgc-filedrill">{Icon(P.IconChevronRightOutline14, 12)}</span> : null}
171
+ </button>
172
+ ))
173
+ : fileSearchLoading
174
+ ? <div className="dsgc-mentionitem dsgc-hint">检索中…</div>
175
+ : fileSearchError
176
+ ? <div className="dsgc-mentionitem dsgc-hint">{fileSearchError}</div>
177
+ : <div className="dsgc-mentionitem dsgc-hint">无匹配文件</div>}
178
+ <span className="dsgc-mentionhint">
179
+ {fileSearchLoading && fileCandidates.length
180
+ ? '检索中…'
181
+ : fileCandidates.some((f) => f.isDir)
182
+ ? '↑↓ 选择 · Enter 插入 · Tab 进入目录 · Esc 关闭'
183
+ : '↑↓ 选择 · Enter 插入 · Esc 关闭'}
184
+ </span>
185
+ </div>
186
+ )
187
+ : null}
188
+ {!input.trim()
189
+ ? <div className="dsgc-ph" aria-hidden="true">发消息给全群,@成员 点名让其回应(留空则让角色自由讨论)…</div>
190
+ : null}
191
+ <div
192
+ className="dsgc-edit"
193
+ ref={inputRef}
194
+ contentEditable
195
+ suppressContentEditableWarning
196
+ role="textbox"
197
+ aria-multiline="true"
198
+ aria-label="群聊消息输入框"
199
+ onInput={onInputCE}
200
+ onKeyDown={onInputKeyDown}
201
+ onPaste={onPasteCE}
202
+ onDrop={onDropCE}
203
+ onDragOver={onDragOverCE}
204
+ onBlur={() => { setMention(null) }}
205
+ />
206
+ </div>
207
+ <div className="dsgc-sendrow">
208
+ <PermissionSelect
209
+ tier={group.permissionTier}
210
+ onSelect={(tier) => { void mutate({ op: 'setPermissionTier', groupId: group.id, tier }) }}
211
+ />
212
+ <span style={{ flex: 1 }} />
213
+ <div className="dsgc-rounds" title="自由讨论的轮数(1–10):一轮 = 全体参与角色按顺序各发言一次">
214
+ <button type="button" className="dsgc-roundbtn" aria-label="减少轮数" disabled={rounds <= 1} onClick={() => { setRounds(Math.max(1, rounds - 1)) }}>
215
+ {Icon(P.IconChevronLeftOutline14, 12)}
216
+ </button>
217
+ <span className="dsgc-roundnum" title="轮数">{rounds}</span>
218
+ <button type="button" className="dsgc-roundbtn" aria-label="增加轮数" disabled={rounds >= 10} onClick={() => { setRounds(Math.min(10, rounds + 1)) }}>
219
+ {Icon(P.IconChevronRightOutline14, 12)}
220
+ </button>
221
+ <span style={{ padding: '0 6px 0 2px' }}>轮</span>
222
+ </div>
223
+ {busyNow
224
+ ? (
225
+ <P.Button variant="outline" className="dsgc-stopbtn" onClick={() => { void stopRun() }}>
226
+ {Icon(P.IconStopFill16, 16)}停止
227
+ </P.Button>
228
+ )
229
+ : (
230
+ <P.Button variant="primary" onClick={() => { void sendMsg() }} disabled={(!participants.length && !mentionedRoles.length) || !sess}>
231
+ {Icon(P.IconSendOutline16, 16)}发送
232
+ </P.Button>
233
+ )}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ )
238
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 侧边栏入口图标。
3
+ *
4
+ * 整行命中层携带 “newSession” 类名:任务看板等 DOM 注入式面板用
5
+ * [class*="newSession"] 识别“侧边栏导航点击”并自动收起自己,与点击
6
+ * “新建会话”行为一致——类名不可哈希化。
7
+ * @module dsh-group-chat/client/glyph
8
+ */
9
+
10
+ import type { ReactNode } from 'react'
11
+
12
+ export function Glyph(props: { size?: number }): ReactNode {
13
+ const size = (props && props.size) || 18
14
+ return (
15
+ <span className="dsgc-entryOverlay newSession">
16
+ <span className="dsgc-entryIcon">
17
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
18
+ strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
19
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
20
+ <circle cx={9} cy={7} r={4} />
21
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
22
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
23
+ </svg>
24
+ </span>
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 设置页:启停开关(settings.section 插槽)。写 host 的 group-chat 设置
3
+ * 命名空间(settings.yaml 持久化)。
4
+ * @module dsh-group-chat/client/settings
5
+ */
6
+
7
+ import { useEffect, useState, type ReactNode } from 'react'
8
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
9
+ import { P } from '../lib/ui.ts'
10
+
11
+ /** Host 侧注册的设置命名空间 shape。 */
12
+ interface GroupChatSettings {
13
+ enabled?: boolean
14
+ }
15
+
16
+ export function GroupChatSettingsSection(props: { settingsScope: SettingsScope<GroupChatSettings> }): ReactNode {
17
+ const scope = props.settingsScope
18
+ const [snap, setSnap] = useState<ReturnType<SettingsScope<GroupChatSettings>['getSnapshot']> | null>(null)
19
+ const [pending, setPending] = useState(false)
20
+
21
+ useEffect(() => {
22
+ let live = true
23
+ const read = (): void => {
24
+ if (!live) return
25
+ try {
26
+ setSnap(scope.getSnapshot())
27
+ } catch { /* ignore */ }
28
+ }
29
+ read()
30
+ const off = scope.subscribe(read)
31
+ return () => {
32
+ live = false
33
+ off()
34
+ }
35
+ }, [])
36
+
37
+ const ready = snap !== null && snap.status === 'ready'
38
+ const writable = ready ? snap.writable !== false : false
39
+ const enabled = ready ? (snap.value && snap.value.enabled) !== false : true
40
+
41
+ const toggle = async (): Promise<void> => {
42
+ if (!writable || pending) return
43
+ setPending(true)
44
+ try {
45
+ await scope.set('enabled', !enabled)
46
+ } catch (e) {
47
+ console.error('[dsh-group-chat] settings write failed', e)
48
+ } finally {
49
+ setPending(false)
50
+ }
51
+ }
52
+
53
+ return (
54
+ <div className="dgcs-page">
55
+ <div className="dgcs-head">
56
+ <h3 className="dgcs-title">模型群聊</h3>
57
+ <p className="dgcs-desc">多模型角色群组对话面板:每个角色绑定不同的 provider/model;群组内多会话目录树管理,群内共享对话记录与工作区目录;消息以 markdown 渲染、支持思考折叠;输入框支持 @成员 / @文件。数据持久化于 ~/.dsh/storages/group-chat/,重启 dsh web 后恢复。</p>
58
+ </div>
59
+ <div className="dgcs-card">
60
+ <div className="dgcs-cardtext">
61
+ <div className="dgcs-cardtitle">{enabled ? '已启用' : '已停用'}</div>
62
+ <div className="dgcs-cardhint">{writable ? (enabled ? '关闭后侧边栏将不再显示「群聊」入口,进行中的对话会被中止。' : '开启后侧边栏显示「群聊」入口。') : '当前设置不可写(可能被配置文件覆盖)。'}</div>
63
+ </div>
64
+ <P.Switch
65
+ checked={enabled}
66
+ onChange={() => { void toggle() }}
67
+ disabled={!writable || pending}
68
+ label="模型群聊启停"
69
+ title={pending ? '正在写入…' : enabled ? '点击停用' : '点击启用'}
70
+ />
71
+ </div>
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 消息流显示组件
3
+ * @module dsh-group-chat/client/components
4
+ */
5
+
6
+ import type { ReactNode, CSSProperties } from 'react'
7
+ import { Icon, P } from '../lib/ui.ts'
8
+ import { Bubble } from './Bubble.tsx'
9
+ import { ThinkRow } from './ThinkRow.tsx'
10
+ import { roleById, type ClientSnapshot } from '../lib/model.ts'
11
+ import { MD_LABELS } from '../lib/model.ts'
12
+
13
+ interface MessageFlowProps {
14
+ snap: ClientSnapshot
15
+ sess: ClientSnapshot['sessions'][number] | null
16
+ busyNow: boolean
17
+ msgById: Record<string, ClientSnapshot['messages'][number]>
18
+ action: (payload: Record<string, unknown>) => Promise<unknown>
19
+ }
20
+
21
+ export function MessageFlow(props: MessageFlowProps): ReactNode {
22
+ const { snap, sess, busyNow, msgById, action } = props
23
+
24
+ const bubbles: ReactNode[] = []
25
+
26
+ if (sess) {
27
+ for (const mid of sess.messageIds) {
28
+ const m = msgById[mid]
29
+ if (!m) continue
30
+ // 角色在父级解析:Bubble 按值 memo(不依赖 snap identity),流式帧
31
+ // 不再触发已完成消息的全量重渲(每帧仅 live 行与派生列表变化)
32
+ const role = m.speaker !== 'user' && m.speaker !== 'system' ? roleById(snap, m.speaker) : null
33
+ bubbles.push(<Bubble key={m.id} m={m} role={role} />)
34
+ }
35
+ }
36
+
37
+ // 流式尾巴
38
+ if (busyNow && snap.run.currentRoleId) {
39
+ const lr = roleById(snap, snap.run.currentRoleId)
40
+ if (lr) {
41
+ bubbles.push(
42
+ <div key="__live" className="dsgc-msg live">
43
+ <div
44
+ className="dsgc-avatar"
45
+ style={{ border: '2px solid ' + (lr.color || '#888'), '--role-color': lr.color || '#888' } as CSSProperties}
46
+ >
47
+ {lr.name.slice(0, 1)}
48
+ </div>
49
+ <div className="dsgc-msgbody">
50
+ <div className="dsgc-msghead">
51
+ <span className="dsgc-msgname">{lr.name}</span>
52
+ <span className="dsgc-msgmodel">{lr.provider} / {lr.model}</span>
53
+ <span className="dsgc-msgtime dsgc-typing">深度求索...</span>
54
+ </div>
55
+ <div className="dsgc-msgtext live">
56
+ {snap.run.partialReasoning ? <ThinkRow text={snap.run.partialReasoning} running /> : null}
57
+ {snap.run.partial ? <P.MarkdownText text={snap.run.partial} streaming labels={MD_LABELS} /> : null}
58
+ {/* 首 delta 前的「思考中」占位(深度思考模型首字节可能等数秒到数十秒,
59
+ 空白气泡会被感知为卡死);delta 到达后被真实思考行/正文自然替换 */}
60
+ {!snap.run.partial && !snap.run.partialReasoning
61
+ ? (
62
+ <div className="dsgc-pending">
63
+ {Icon(P.IconThinkOutline14, 14)}
64
+ <span>思考中</span>
65
+ <span className="dsgc-pendingdots" aria-hidden="true"><i /><i /><i /></span>
66
+ </div>
67
+ )
68
+ : null}
69
+ </div>
70
+ </div>
71
+ </div>,
72
+ )
73
+ }
74
+ }
75
+
76
+ // 命令确认卡片
77
+ const pc = busyNow && snap.run.pendingConfirm && sess && snap.run.sessionId === sess.id ? snap.run.pendingConfirm : null
78
+ if (pc) {
79
+ const lr = roleById(snap, snap.run.currentRoleId)
80
+ bubbles.push(
81
+ <div key="__confirm" className="dsgc-confirm">
82
+ <div className="dsgc-confirmtitle">
83
+ {Icon(P.IconWarningOutline16, 14)}
84
+ {(lr ? lr.name : '角色') + ' 请求执行命令(工作区内)'}
85
+ </div>
86
+ <div className="dsgc-confirmcmd">{String((pc.args && pc.args.command) || '')}</div>
87
+ <div className="dsgc-hint">允许后将在工作区目录执行;拒绝后角色将继续纯文本讨论</div>
88
+ <div className="dsgc-confirmops">
89
+ <P.Button variant="primary" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: true }) }}>允许</P.Button>
90
+ <P.Button variant="outline" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: false }) }}>拒绝</P.Button>
91
+ </div>
92
+ </div>,
93
+ )
94
+ }
95
+
96
+ return <>{bubbles}</>
97
+ }