@roaming-ai/dsh-group-chat 0.2.2 → 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 (103) hide show
  1. package/README.md +91 -27
  2. package/lib/client.js +1254 -598
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +573 -109
  5. package/lib/types/client/components/AsidePanel.d.ts +3 -19
  6. package/lib/types/client/components/Bubble.d.ts +6 -3
  7. package/lib/types/client/components/ChatPanel.d.ts +1 -0
  8. package/lib/types/client/components/ConstraintList.d.ts +11 -0
  9. package/lib/types/client/components/FailCard.d.ts +9 -0
  10. package/lib/types/client/components/Fold.d.ts +19 -0
  11. package/lib/types/client/components/HoverTip.d.ts +22 -0
  12. package/lib/types/client/components/MessageFlow.d.ts +1 -0
  13. package/lib/types/client/components/MsgActions.d.ts +14 -0
  14. package/lib/types/client/components/NavPanel.d.ts +9 -11
  15. package/lib/types/client/components/RoleDrawer.d.ts +3 -2
  16. package/lib/types/client/components/ToolRow.d.ts +2 -2
  17. package/lib/types/client/hooks/useComposer.d.ts +6 -1
  18. package/lib/types/client/hooks/useGroupChatState.d.ts +0 -16
  19. package/lib/types/client/lib/composer-draft.d.ts +17 -0
  20. package/lib/types/client/lib/model.d.ts +1 -7
  21. package/lib/types/client/utils/utils.d.ts +4 -4
  22. package/lib/types/core/constraints.d.ts +61 -0
  23. package/lib/types/core/errors.d.ts +54 -0
  24. package/lib/types/core/json.d.ts +6 -0
  25. package/lib/types/core/types.d.ts +76 -60
  26. package/lib/types/host/api/actions.d.ts +1 -1
  27. package/lib/types/host/engine/conversation.d.ts +5 -3
  28. package/lib/types/host/engine/defaults.d.ts +13 -0
  29. package/lib/types/host/engine/fold.d.ts +17 -0
  30. package/lib/types/host/engine/index.d.ts +1 -0
  31. package/lib/types/host/engine/retitle.d.ts +5 -5
  32. package/lib/types/host/service.d.ts +1 -1
  33. package/lib/types/host/state.d.ts +2 -0
  34. package/lib/types/index.d.ts +2 -0
  35. package/lib/types/shared/file-mention-grammar.d.ts +3 -12
  36. package/package.json +1 -1
  37. package/src/client/GroupChatPanel.tsx +25 -59
  38. package/src/client/components/AsidePanel.tsx +50 -23
  39. package/src/client/components/Bubble.tsx +45 -18
  40. package/src/client/components/ChatPanel.tsx +17 -12
  41. package/src/client/components/Composer.tsx +26 -20
  42. package/src/client/components/ConstraintList.tsx +64 -0
  43. package/src/client/components/FailCard.tsx +49 -0
  44. package/src/client/components/Fold.tsx +72 -0
  45. package/src/client/components/HoverTip.tsx +134 -0
  46. package/src/client/components/MessageFlow.tsx +78 -54
  47. package/src/client/components/MsgActions.tsx +85 -0
  48. package/src/client/components/NavPanel.tsx +102 -79
  49. package/src/client/components/RoleDrawer.tsx +29 -7
  50. package/src/client/components/ThinkRow.tsx +7 -3
  51. package/src/client/components/ToolRow.tsx +9 -5
  52. package/src/client/hooks/useComposer.ts +54 -56
  53. package/src/client/hooks/useGroupChatState.ts +7 -17
  54. package/src/client/lib/composer-draft.ts +47 -0
  55. package/src/client/lib/model.ts +1 -14
  56. package/src/client/lib/styles.ts +73 -12
  57. package/src/client/react-dom-shim.d.ts +2 -0
  58. package/src/client/utils/utils.ts +6 -6
  59. package/src/core/constraints.ts +204 -0
  60. package/src/core/errors.ts +174 -0
  61. package/src/core/json.ts +18 -0
  62. package/src/core/types.ts +52 -19
  63. package/src/host/api/actions.ts +49 -14
  64. package/src/host/broadcast.ts +3 -3
  65. package/src/host/engine/conversation.ts +104 -40
  66. package/src/host/engine/defaults.ts +28 -0
  67. package/src/host/engine/fold.ts +99 -0
  68. package/src/host/engine/index.ts +1 -0
  69. package/src/host/engine/retitle.ts +24 -41
  70. package/src/host/materials/materials.ts +2 -0
  71. package/src/host/persistence/persistence.ts +18 -1
  72. package/src/host/service.ts +1 -1
  73. package/src/host/state.ts +5 -4
  74. package/src/host/tools/tools.ts +2 -2
  75. package/src/index.ts +2 -0
  76. package/src/shared/file-mention-grammar.test.ts +1 -38
  77. package/src/shared/file-mention-grammar.ts +7 -33
  78. package/lib/types/client/Bubble.d.ts +0 -10
  79. package/lib/types/client/Glyph.d.ts +0 -12
  80. package/lib/types/client/GroupChatPanel.old.d.ts +0 -10
  81. package/lib/types/client/GroupChatSettingsSection.d.ts +0 -15
  82. package/lib/types/client/PermissionSelect.d.ts +0 -13
  83. package/lib/types/client/RoleDrawer.d.ts +0 -17
  84. package/lib/types/client/ThinkRow.d.ts +0 -9
  85. package/lib/types/client/ToolRow.d.ts +0 -9
  86. package/lib/types/client/api.d.ts +0 -8
  87. package/lib/types/client/components.d.ts +0 -22
  88. package/lib/types/client/drawer.d.ts +0 -17
  89. package/lib/types/client/glyph.d.ts +0 -12
  90. package/lib/types/client/model.d.ts +0 -78
  91. package/lib/types/client/panel.d.ts +0 -10
  92. package/lib/types/client/settings.d.ts +0 -15
  93. package/lib/types/client/styles.d.ts +0 -10
  94. package/lib/types/client/ui.d.ts +0 -17
  95. package/lib/types/client/utils.d.ts +0 -22
  96. package/lib/types/host/actions.d.ts +0 -34
  97. package/lib/types/host/conversation.d.ts +0 -25
  98. package/lib/types/host/http.d.ts +0 -16
  99. package/lib/types/host/materials.d.ts +0 -32
  100. package/lib/types/host/persistence.d.ts +0 -31
  101. package/lib/types/host/routes.d.ts +0 -11
  102. package/lib/types/host/store.d.ts +0 -65
  103. package/lib/types/host/tools.d.ts +0 -28
package/lib/client.js CHANGED
@@ -83,7 +83,32 @@ window.__ModuleLoader__.load({
83
83
  ".dsgc-seambtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l3,rgba(128,128,128,.4))}",
84
84
  ".dsgc-seambtn.left{left:0;border-left:none;border-radius:0 8px 8px 0;box-shadow:3px 0 10px rgba(0,0,0,.07)}",
85
85
  ".dsgc-seambtn.right{right:0;border-right:none;border-radius:8px 0 0 8px;box-shadow:-3px 0 10px rgba(0,0,0,.07)}",
86
- ".dsgc-chathead{display:flex;gap:10px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2))}",
86
+ ".dsgc-chathead{display:flex;flex-direction:column;gap:6px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2))}",
87
+ ".dsgc-chathead-row{display:flex;gap:10px;align-items:center;min-width:0}",
88
+ ".dsgc-constraints{align-self:center;box-sizing:border-box;width:76%;max-width:76%;display:flex;flex-direction:column;gap:8px;min-width:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));border-radius:12px;animation:dsgc-fade-in .18s ease-out}",
89
+ ".dsgc-chead{display:flex;flex-direction:column;gap:2px;min-width:0}",
90
+ ".dsgc-ctitle{margin:0;font:inherit;font-size:12px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,inherit)}",
91
+ ".dsgc-cdesc{margin:0;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit)}",
92
+ ".dsgc-fold{display:grid;grid-template-rows:0fr;transition:grid-template-rows .16s ease-in}",
93
+ ".dsgc-fold.open{grid-template-rows:1fr;transition:grid-template-rows .22s cubic-bezier(.16,1,.3,1)}",
94
+ ".dsgc-fold-inner{min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .12s ease-in}",
95
+ ".dsgc-fold.open .dsgc-fold-inner{opacity:1;transition:opacity .18s cubic-bezier(.16,1,.3,1)}",
96
+ ".dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.18));position:relative;min-width:0}",
97
+ ".dsgc-clip::before,.dsgc-clip::after{content:\"\";position:absolute;left:0;right:0;height:14px;pointer-events:none;z-index:1;opacity:0;transition:opacity .12s}",
98
+ ".dsgc-clip::before{top:0;background:linear-gradient(to bottom,var(--dsgc-clip-from),transparent)}",
99
+ ".dsgc-clip::after{bottom:0;background:linear-gradient(to top,var(--dsgc-clip-from),transparent)}",
100
+ ".dsgc-clip.can-up::before,.dsgc-clip.can-down::after{opacity:1}",
101
+ ".dsgc-clip-scroll{min-width:0;overflow-y:auto;overscroll-behavior:contain;overflow-anchor:none;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}",
102
+ ".dsgc-clist{display:flex;flex-direction:column;gap:2px;min-width:0}",
103
+ ".dsgc-constraint{display:flex;gap:8px;align-items:flex-start;min-width:0;font-size:11.5px;line-height:1.5}",
104
+ ".dsgc-ckind{flex:none;font-weight:500}",
105
+ ".dsgc-ckind.decided{color:var(--dsw-alias-label-secondary,inherit)}",
106
+ ".dsgc-ckind.rejected{color:var(--dsw-alias-state-error-primary,#e5484d)}",
107
+ ".dsgc-ckind.open{color:var(--dsw-alias-label-tertiary,inherit)}",
108
+ ".dsgc-ctext{min-width:0;color:var(--dsw-alias-label-primary,inherit);overflow-wrap:break-word}",
109
+ ".dsgc-cmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}",
110
+ ".dsgc-cmore:hover{color:var(--dsw-alias-label-secondary,inherit)}",
111
+ ".dsgc-cmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}",
87
112
  ".dsgc-sess-title{flex:none;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,inherit);max-width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
88
113
  ".dsgc-topic{flex:1;background:transparent;border:none;border-bottom:1px solid transparent;padding:2px;min-height:20px;color:var(--dsw-alias-label-secondary,inherit);font:inherit;outline:none;min-width:0;transition:color .12s,border-color .12s}",
89
114
  ".dsgc-topic:hover{border-bottom-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}",
@@ -113,7 +138,8 @@ window.__ModuleLoader__.load({
113
138
  ".dsgc-think .dsgc-thinkrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}",
114
139
  ".dsgc-think .dsgc-thinkrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}",
115
140
  ".dsgc-think .dsgc-thinksummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:44ch;font-size:11.5px;margin-left:6px}",
116
- ".dsgc-think .dsgc-thinkbody{font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));margin:2px 0 4px;padding:2px 0 2px 10px;max-height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent;animation:dsgc-think-in .2s ease-out}",
141
+ ".dsgc-think .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}",
142
+ ".dsgc-think .dsgc-thinkbody{font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}",
117
143
  ".dsgc-pending{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary,inherit)}",
118
144
  ".dsgc-pending svg{flex:none}",
119
145
  ".dsgc-pendingdots{display:inline-flex;gap:2px;align-items:center}",
@@ -122,16 +148,39 @@ window.__ModuleLoader__.load({
122
148
  ".dsgc-pendingdots i:nth-child(3){animation-delay:.4s}",
123
149
  "@keyframes dsgc-dot-breathe{0%,100%{opacity:.25}50%{opacity:1}}",
124
150
  ".dsgc-sysmsg{align-self:center;font-size:11.5px;color:var(--dsw-alias-label-secondary,inherit);background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.1));border-radius:999px;padding:2px 10px;text-align:center;max-width:90%;animation:dsgc-sys-in .24s ease-out}",
151
+ ".dsgc-msgops{display:inline-flex;align-items:center;gap:2px;margin-top:2px;min-height:24px;visibility:hidden;opacity:0;transition:opacity .12s,visibility .12s}",
152
+ ".dsgc-msg:hover .dsgc-msgops,.dsgc-msg:focus-within .dsgc-msgops,.dsgc-msgops.always{visibility:visible;opacity:1}",
153
+ "@media (hover:none){.dsgc-msg .dsgc-msgops{visibility:visible;opacity:1}}",
154
+ ".dsgc-msg.mine .dsgc-msgops{justify-content:flex-end}",
155
+ ".dsgc-msgop{border:none;background:none;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer;padding:3px 6px;border-radius:6px;display:inline-flex;align-items:center;gap:4px;font:inherit;font-size:11.5px;line-height:16px;transition:background-color .12s,color .12s}",
156
+ ".dsgc-msgop svg{flex:none}",
157
+ ".dsgc-msgop:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}",
158
+ ".dsgc-msgop:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}",
159
+ ".dsgc-msgop.done{color:var(--dsw-alias-state-success-primary,#30a46c)}",
160
+ ".dsgc-msgop.retry{color:var(--dsw-alias-state-error-primary,#e5484d)}",
161
+ ".dsgc-msgop.retry:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 10%,transparent);color:var(--dsw-alias-state-error-primary,#e5484d)}",
162
+ ".dsgc-msgop:disabled{opacity:.4;cursor:default}",
163
+ ".dsgc-fail{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 8%,var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06)));border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 28%,var(--dsw-alias-border-l1,rgba(128,128,128,.15)));border-radius:12px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0;animation:dsgc-fade-in .18s ease-out}",
164
+ ".dsgc-failhead{display:flex;align-items:flex-start;gap:8px;min-width:0}",
165
+ ".dsgc-failicon{flex:none;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-state-error-primary,#e5484d);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 12%,transparent);margin-top:1px}",
166
+ ".dsgc-failcopy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}",
167
+ ".dsgc-failtitle{font-size:13px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,inherit)}",
168
+ ".dsgc-faildetail{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,inherit);overflow-wrap:anywhere}",
169
+ ".dsgc-failmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}",
170
+ ".dsgc-failmore:hover{color:var(--dsw-alias-label-secondary,inherit)}",
171
+ ".dsgc-failmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}",
172
+ ".dsgc-fail .dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.12));border-radius:8px}",
173
+ ".dsgc-failraw{margin:0;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-secondary,inherit)}",
125
174
  ".dsgc-tool{margin:2px 0}",
126
175
  ".dsgc-tool .dsgc-toolrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}",
127
176
  ".dsgc-tool .dsgc-toolrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}",
128
177
  ".dsgc-tool .dsgc-toolsummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40ch;font-size:11.5px;margin-left:6px}",
129
- ".dsgc-tool .dsgc-toolbody{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));margin:2px 0 4px;padding:2px 0 2px 10px;max-height:260px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}",
178
+ ".dsgc-tool .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}",
179
+ ".dsgc-tool .dsgc-toolbody{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}",
130
180
  ".dsgc-confirm{border:1px solid var(--dsw-alias-state-business-primary,#4f6ef7);border-radius:12px;background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));padding:10px 12px;display:flex;flex-direction:column;gap:8px;max-width:76%;animation:dsgc-fade-in .18s ease-out}",
131
181
  ".dsgc-confirmtitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);display:flex;align-items:center;gap:6px}",
132
182
  ".dsgc-confirmcmd{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}",
133
183
  ".dsgc-confirmops{display:flex;gap:8px;justify-content:flex-end}",
134
- ".dsgc-sysmsg.err{color:var(--dsw-alias-state-error-primary,#e5484d)}",
135
184
  ".dsgc-clearnotes{margin:0;padding-left:18px;font-size:13px;line-height:1.8;color:var(--dsw-alias-label-secondary,inherit);display:flex;flex-direction:column;gap:2px}",
136
185
  ".dsgc-err{font-size:12px;color:var(--dsw-alias-state-error-primary,#e5484d);animation:dsgc-fade-in .18s ease-out}",
137
186
  ".dsgc-empty{margin:auto;display:flex;flex-direction:column;align-items:center;gap:10px;color:var(--dsw-alias-label-tertiary,inherit);text-align:center;max-width:40ch;padding:24px;animation:dsgc-fade-in .3s ease-out}",
@@ -150,8 +199,9 @@ window.__ModuleLoader__.load({
150
199
  ".dsgc-card .dsgc-sendrow{padding:0 12px 10px}",
151
200
  ".dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}",
152
201
  ".dsgc-stopbtn:hover:not(:disabled){filter:brightness(1.08);color:#fff}",
153
- ".dsgc-parts{display:flex;gap:6px;flex-wrap:wrap;align-items:center;row-gap:4px;height:24px}",
154
- ".dsgc-partslabel{font-size:12px;color:var(--dsw-alias-label-tertiary,inherit);flex:none}",
202
+ ".dsgc-parts{display:flex;align-items:flex-start;gap:6px;min-height:24px}",
203
+ ".dsgc-partslabel{font-size:12px;line-height:24px;color:var(--dsw-alias-label-tertiary,inherit);flex:none}",
204
+ ".dsgc-partlist{display:flex;flex-wrap:wrap;align-items:center;gap:6px;row-gap:4px;flex:1;min-width:0}",
155
205
  ".dsgc-partchip{display:inline-flex;gap:6px;align-items:center;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:999px;padding:2px 10px;font-size:12px;line-height:1.5;cursor:pointer;transition:background-color .12s,color .12s,border-color .12s;font-family:inherit}",
156
206
  ".dsgc-partchip:hover{color:var(--dsw-alias-label-primary,inherit)}",
157
207
  ".dsgc-partchip.on{background:var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.18));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}",
@@ -173,6 +223,11 @@ window.__ModuleLoader__.load({
173
223
  ".dsgc-rounds .dsgc-roundbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}",
174
224
  ".dsgc-rounds .dsgc-roundbtn:disabled{opacity:.4;cursor:default}",
175
225
  ".dsgc-rounds .dsgc-roundnum{width:26px;text-align:center;font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}",
226
+ ".dsgc-hovertip{position:fixed;z-index:100;width:max-content;max-width:50vw;padding:3px 7px;border-radius:8px;background:var(--dsw-alias-tooltip-bg,#1f1f1f);color:var(--dsw-static-neutral-bluish-00,#fff);font-size:13px;line-height:20px;white-space:pre-line;overflow-wrap:break-word;pointer-events:none;animation:dsgc-hovertip-in 150ms var(--ds-ease-in-out,ease)}",
227
+ ".dsgc-hovertip[data-side=\"right\"]{transform:translateY(-50%)}",
228
+ ".dsgc-hovertip[data-side=\"top\"]{transform:translate(-50%,-100%)}",
229
+ ".dsgc-hovertip[data-side=\"bottom\"]{transform:translateX(-50%)}",
230
+ "@keyframes dsgc-hovertip-in{from{opacity:0}}",
176
231
  ".dsgc-mentionwrap{position:relative}",
177
232
  ".dsgc-mention{animation:dsgc-pop-in .16s cubic-bezier(.16,1,.3,1);position:absolute;left:0;right:0;bottom:calc(100% + 6px);background:var(--dsw-alias-bg-layer-3,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:10px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.18));max-height:220px;overflow-y:auto;padding:4px;z-index:30;display:flex;flex-direction:column;gap:2px;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}",
178
233
  ".dsgc-mentionitem{display:flex;align-items:center;gap:8px;border:none;background:none;color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:12.5px;text-align:left;cursor:pointer;padding:6px 8px;border-radius:6px;transition:background-color .12s,color .12s}",
@@ -206,10 +261,12 @@ window.__ModuleLoader__.load({
206
261
  ".dsgc-rolename{font-weight:600;font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}",
207
262
  ".dsgc-role.off .dsgc-rolename{color:var(--dsw-alias-label-tertiary,inherit)}",
208
263
  ".dsgc-rolepersona{font-size:11.5px;color:var(--dsw-alias-label-tertiary,inherit);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:0}",
209
- ".dsgc-rolemodel{width:fit-content;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12));border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;color:var(--dsw-alias-label-secondary,inherit)}",
210
- ".dsgc-roleops{display:flex;gap:2px;flex:none;opacity:0;transition:opacity .12s}",
264
+ ".dsgc-rolemeta{display:flex;align-items:center;gap:6px;min-width:0;flex:1}",
265
+ ".dsgc-rolemodel{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12));border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;color:var(--dsw-alias-label-secondary,inherit)}",
266
+ ".dsgc-rolethink{display:inline-flex;align-items:center;flex:none;color:var(--dsw-alias-label-tertiary,inherit)}",
267
+ ".dsgc-roleops{display:flex;gap:2px;flex:none;margin-left:auto;opacity:0;transition:opacity .12s}",
211
268
  ".dsgc-role:hover .dsgc-roleops,.dsgc-role:focus-within .dsgc-roleops{opacity:1}",
212
- ".dsgc-rolemenu{display:flex;align-items:center;justify-content:space-between;gap:8px}",
269
+ ".dsgc-rolemenu{display:flex;align-items:center;gap:8px}",
213
270
  ".dsgc-field{display:flex;flex-direction:column;gap:5px}",
214
271
  ".dsgc-field>label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary,inherit)}",
215
272
  ".dsgc-input,.dsgc-select,.dsgc-textarea{background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-3,transparent));border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));color:var(--dsw-alias-label-primary,inherit);font:inherit;font-size:13px;border-radius:8px;padding:6px 10px;width:100%;box-sizing:border-box;outline:none;transition:border-color .16s,background-color .16s}",
@@ -229,7 +286,6 @@ window.__ModuleLoader__.load({
229
286
  "@keyframes dsgc-drawer-out{to{transform:translateX(32px);opacity:0}}",
230
287
  "@keyframes dsgc-msg-in{from{opacity:0;transform:translateY(6px)}}",
231
288
  "@keyframes dsgc-sys-in{from{opacity:0;transform:scale(.96)}}",
232
- "@keyframes dsgc-think-in{from{opacity:0;transform:translateY(-3px)}}",
233
289
  "@keyframes dsgc-pop-in{from{opacity:0;transform:translateY(4px)}}",
234
290
  "@keyframes dsgc-rise-in{from{opacity:0;transform:translate(-50%,6px)}}",
235
291
  "@keyframes dsgc-fade-in{from{opacity:0}}",
@@ -256,7 +312,7 @@ window.__ModuleLoader__.load({
256
312
  ".dsgc-fbsize{flex:none;font-size:10.5px;color:var(--dsw-alias-label-tertiary,inherit)}",
257
313
  ".dsgc-partchip:focus-visible,.dsgc-mentionitem:focus-visible,.dsgc-dot:focus-visible,.dsgc-opbtn:focus-visible,.dsgc-twist:focus-visible,.dsgc-addsess:focus-visible,.dsgc-grow-row:focus-visible,.dsgc-sess-row:focus-visible,.dsgc-rename:focus-visible,.dsgc-fbrow:focus-visible,.dsgc-role:focus-visible,.dsgc-roundbtn:focus-visible,.dsgc-tobtn:focus-visible,.dsgc-seambtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}",
258
314
  "@container (max-width: 880px){.dsgc-aside{position:absolute;top:0;right:0;bottom:0;z-index:15;width:min(304px,88%);box-shadow:-12px 0 32px rgba(0,0,0,.16);border-left:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));transition:transform .3s cubic-bezier(.16,1,.3,1),opacity .2s ease}.dsgc-aside.closed{width:min(304px,88%);padding-left:12px;padding-right:12px;border-left-width:1px;transform:translateX(calc(100% + 14px));opacity:0;transition:transform .26s ease-in,opacity .18s ease-in,visibility 0s .24s}.dsgc-seambtn.right:not(.closed){right:min(304px,88%)}}",
259
- "@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}}",
315
+ "@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}.dsgc-constraints{width:88%;max-width:88%}}",
260
316
  "[class*=\"panelList\"]:has(.dsgc-entryOverlay){margin-top:4px}",
261
317
  "[class*=\"panelRow\"]:has(.dsgc-entryOverlay){position:relative;box-sizing:border-box;height:36px;min-height:36px;padding:0 10px;font-size:13px;transition:background-color .12s,color .12s}",
262
318
  "[class*=\"panelRow\"][class*=\"panelActive\"]:has(.dsgc-entryOverlay){font-weight:600}",
@@ -270,7 +326,7 @@ window.__ModuleLoader__.load({
270
326
  ".dsgc-entryIcon svg{width:18px;height:18px;display:block}",
271
327
  "[data-sidebar-collapsed] .dsgc-entryOverlay,[class*=\"collapsed\"] .dsgc-entryOverlay{justify-content:center;padding:0}",
272
328
  "[data-sidebar-collapsed] [class*=\"panelRow\"]:has(.dsgc-entryOverlay),[class*=\"collapsed\"] [class*=\"panelRow\"]:has(.dsgc-entryOverlay){margin:0 auto}",
273
- "@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,[class*=\"panelRow\"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-think .dsgc-thinkbody,.dsgc-msg.live .dsgc-avatar,.dsgc-loading svg,.dsgc-pendingdots i,.dsgc-typing{animation:none}.dsgc-typing{background-position:0 0;background-size:100% 100%}}",
329
+ "@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,.dsgc-cmore,.dsgc-msgops,.dsgc-msgop,.dsgc-failmore,.dsgc-clip::before,.dsgc-clip::after,.dsgc-fold,.dsgc-fold-inner,.dsgc-fold.open,.dsgc-fold.open .dsgc-fold-inner,[class*=\"panelRow\"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-msg.live .dsgc-avatar,.dsgc-loading svg,.dsgc-pendingdots i,.dsgc-typing,.dsgc-constraints,.dsgc-hovertip,.dsgc-fail{animation:none}.dsgc-typing{background-position:0 0;background-size:100% 100%}}",
274
330
  ".dgcs-page{display:flex;flex-direction:column;gap:14px;padding:4px 0}",
275
331
  ".dgcs-head{display:flex;flex-direction:column;gap:6px}",
276
332
  ".dgcs-title{margin:0;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,inherit)}",
@@ -436,14 +492,17 @@ window.__ModuleLoader__.load({
436
492
  * @module dsh-group-chat/client/drawer
437
493
  */
438
494
  function RoleDrawer(props) {
439
- const { draft, set } = props;
495
+ const { draft, set, groupId } = props;
440
496
  const models = props.models;
441
497
  const providers = models && models.providers || [];
442
498
  const modelsOf = models && models.modelsByProvider && models.modelsByProvider[draft.provider] || [];
443
499
  const [closing, setClosing] = (0, react.useState)(false);
500
+ const closingRef = (0, react.useRef)(false);
501
+ const [formError, setFormError] = (0, react.useState)("");
444
502
  const [effortsInfo, setEffortsInfo] = (0, react.useState)(null);
445
503
  const close = () => {
446
- if (closing) return;
504
+ if (closingRef.current) return;
505
+ closingRef.current = true;
447
506
  setClosing(true);
448
507
  window.setTimeout(() => props.onCancel(), 140);
449
508
  };
@@ -456,6 +515,23 @@ window.__ModuleLoader__.load({
456
515
  reasoningEffort: "default"
457
516
  }));
458
517
  };
518
+ /** 保存(校验在本地,落库经 mutate;失败文案与原实现逐字一致)。 */
519
+ const save = async () => {
520
+ if (!draft.name.trim()) {
521
+ setFormError("角色名称不能为空");
522
+ return;
523
+ }
524
+ if (!draft.provider || !draft.model) {
525
+ setFormError("请选择角色绑定的模型");
526
+ return;
527
+ }
528
+ const res = await props.mutate({
529
+ op: "upsertRole",
530
+ groupId,
531
+ role: draft
532
+ });
533
+ if (res && res.ok && res.snapshot && !res.snapshot.error) props.onCancel();
534
+ };
459
535
  (0, react.useEffect)(() => {
460
536
  const onKey = (e) => {
461
537
  if (e.key === "Escape") close();
@@ -673,9 +749,9 @@ window.__ModuleLoader__.load({
673
749
  children: "当前模型未提供推理等级,将使用默认"
674
750
  })]
675
751
  }) : null,
676
- props.formError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
752
+ formError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
677
753
  className: "dsgc-formerr",
678
- children: props.formError
754
+ children: formError
679
755
  }) : null
680
756
  ]
681
757
  })
@@ -688,7 +764,9 @@ window.__ModuleLoader__.load({
688
764
  children: "取消"
689
765
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
690
766
  variant: "primary",
691
- onClick: props.onSave,
767
+ onClick: () => {
768
+ save();
769
+ },
692
770
  children: draft.id ? "保存" : "添加"
693
771
  })]
694
772
  })
@@ -696,7 +774,169 @@ window.__ModuleLoader__.load({
696
774
  });
697
775
  }
698
776
  //#endregion
777
+ //#region src/client/components/HoverTip.tsx
778
+ /**
779
+ * 对齐宿主 Tooltip 的 hover 气泡:portal 到 document.body,躲开
780
+ * `.dsgc-root` 的 container-type 把 position:fixed 按容器定位。
781
+ * @module dsh-group-chat/client/components
782
+ */
783
+ /**
784
+ * @param props.label 气泡正文(pre-line)
785
+ * @param props.side 默认 right(对齐侧栏会话行);composer 轮数用 top
786
+ * @param props.delayMs hover 延迟,默认 500;键盘 focus 立即出
787
+ */
788
+ function HoverTip(props) {
789
+ const { label, side = "right", delayMs = 500, maxWidth, className, children } = props;
790
+ const anchorRef = (0, react.useRef)(null);
791
+ const bubbleRef = (0, react.useRef)(null);
792
+ const showTimer = (0, react.useRef)(null);
793
+ const hover = (0, react.useRef)(false);
794
+ const [pos, setPos] = (0, react.useState)(null);
795
+ const [placement, setPlacement] = (0, react.useState)(side);
796
+ const cancel = () => {
797
+ if (showTimer.current === null) return;
798
+ clearTimeout(showTimer.current);
799
+ showTimer.current = null;
800
+ };
801
+ const show = () => {
802
+ const el = anchorRef.current;
803
+ if (!el || !label) return;
804
+ const r = el.getBoundingClientRect();
805
+ setPlacement(side);
806
+ setPos({
807
+ x: side === "right" ? r.right + 10 : r.left + r.width / 2,
808
+ top: r.top,
809
+ bottom: r.bottom
810
+ });
811
+ };
812
+ const hideIfIdle = () => {
813
+ if (!hover.current && !(anchorRef.current && anchorRef.current.contains(document.activeElement))) setPos(null);
814
+ };
815
+ (0, react.useEffect)(() => cancel, []);
816
+ (0, react.useLayoutEffect)(() => {
817
+ if (!pos) return;
818
+ const fit = () => {
819
+ const el = bubbleRef.current;
820
+ if (!el) return;
821
+ el.style.left = pos.x + "px";
822
+ const r = el.getBoundingClientRect();
823
+ let dx = 0;
824
+ if (r.right > window.innerWidth - 12) dx = window.innerWidth - 12 - r.right;
825
+ if (r.left + dx < 12) dx = 12 - r.left;
826
+ el.style.left = pos.x + dx + "px";
827
+ if (side === "right") return;
828
+ const fitsBelow = pos.bottom + 8 + r.height <= window.innerHeight - 12;
829
+ const fitsAbove = pos.top - 8 - r.height >= 12;
830
+ if (placement === "bottom" && !fitsBelow && fitsAbove) setPlacement("top");
831
+ if (placement === "top" && !fitsAbove && fitsBelow) setPlacement("bottom");
832
+ };
833
+ fit();
834
+ window.addEventListener("resize", fit);
835
+ return () => {
836
+ window.removeEventListener("resize", fit);
837
+ };
838
+ }, [
839
+ pos,
840
+ placement,
841
+ label,
842
+ side
843
+ ]);
844
+ const y = pos === null ? 0 : placement === "right" ? pos.top + (pos.bottom - pos.top) / 2 : placement === "top" ? pos.top - 8 : pos.bottom + 8;
845
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
846
+ ref: anchorRef,
847
+ className,
848
+ onMouseEnter: () => {
849
+ hover.current = true;
850
+ cancel();
851
+ if (delayMs <= 0) {
852
+ show();
853
+ return;
854
+ }
855
+ showTimer.current = setTimeout(() => {
856
+ showTimer.current = null;
857
+ show();
858
+ }, delayMs);
859
+ },
860
+ onMouseLeave: () => {
861
+ hover.current = false;
862
+ cancel();
863
+ hideIfIdle();
864
+ },
865
+ onFocus: () => {
866
+ cancel();
867
+ show();
868
+ },
869
+ onBlur: (e) => {
870
+ const next = e.relatedTarget;
871
+ if (next && anchorRef.current && anchorRef.current.contains(next)) return;
872
+ cancel();
873
+ hideIfIdle();
874
+ },
875
+ children
876
+ }), pos ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
877
+ ref: bubbleRef,
878
+ className: "dsgc-hovertip",
879
+ "data-side": placement,
880
+ role: "tooltip",
881
+ style: {
882
+ left: pos.x,
883
+ top: y,
884
+ ...maxWidth ? { maxWidth } : {}
885
+ },
886
+ children: label
887
+ }), document.body) : null] });
888
+ }
889
+ //#endregion
699
890
  //#region src/client/components/NavPanel.tsx
891
+ /** 内联重命名输入(群组行/会话行同款)。 */
892
+ function RenameField(props) {
893
+ const { draft, set, commit, keyDown } = props;
894
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
895
+ className: "dsgc-rename",
896
+ value: draft.value,
897
+ autoFocus: true,
898
+ onChange: (e) => {
899
+ set({
900
+ kind: draft.kind,
901
+ id: draft.id,
902
+ value: e.target.value
903
+ });
904
+ },
905
+ onBlur: () => {
906
+ commit();
907
+ },
908
+ onKeyDown: keyDown,
909
+ onClick: (e) => {
910
+ e.stopPropagation();
911
+ }
912
+ });
913
+ }
914
+ /** 行尾操作钮:重命名 + 两次点击确认删除(群组行/会话行同款)。 */
915
+ function NodeOps(props) {
916
+ const { renameLabel, deleteLabel, deleteTitle, danger, onRename, onDelete } = props;
917
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
918
+ className: "dsgc-nodeops",
919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
920
+ className: "dsgc-opbtn",
921
+ title: renameLabel,
922
+ "aria-label": renameLabel,
923
+ onClick: (e) => {
924
+ e.stopPropagation();
925
+ onRename();
926
+ },
927
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
928
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
929
+ className: "dsgc-opbtn" + (danger ? " danger" : ""),
930
+ title: deleteTitle,
931
+ "aria-label": deleteLabel,
932
+ onClick: (e) => {
933
+ e.stopPropagation();
934
+ onDelete();
935
+ },
936
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
937
+ })]
938
+ });
939
+ }
700
940
  function NavPanel(props) {
701
941
  const { snap, search, setSearch, collapsedGroups, setCollapsedGroups, gid, sid, setGid, setSid, setPartsSel, renameDraft, setRenameDraft, confirmDel, setConfirmDel, mutate, navOpen } = props;
702
942
  const commitRename = async () => {
@@ -738,6 +978,20 @@ window.__ModuleLoader__.load({
738
978
  sessionId: d.id
739
979
  });
740
980
  };
981
+ const clearEdits = () => {
982
+ setPartsSel(null);
983
+ setConfirmDel(null);
984
+ setRenameDraft(null);
985
+ };
986
+ const pickGroup = (id) => {
987
+ setGid(id);
988
+ clearEdits();
989
+ };
990
+ const pickSession = (gidToSet, sidToSet) => {
991
+ setGid(gidToSet);
992
+ setSid(sidToSet);
993
+ clearEdits();
994
+ };
741
995
  const q = search.trim().toLowerCase();
742
996
  const groupMatches = (g) => {
743
997
  if (!q) return {
@@ -754,6 +1008,7 @@ window.__ModuleLoader__.load({
754
1008
  };
755
1009
  };
756
1010
  const group = gid ? groupById(snap, gid) : null;
1011
+ const selected = sid ? sessById(snap, sid) : null;
757
1012
  const treeNodes = [];
758
1013
  for (const g of snap.groups) {
759
1014
  const match = groupMatches(g);
@@ -767,8 +1022,7 @@ window.__ModuleLoader__.load({
767
1022
  const s = sessById(snap, sessionId);
768
1023
  if (!s) continue;
769
1024
  if (match.filterSessions && !s.name.toLowerCase().includes(q)) continue;
770
- const sess = sid ? sessById(snap, sid) : null;
771
- const isActive = sess && s.id === sess.id && g.id === group?.id;
1025
+ const isActive = selected && s.id === selected.id && g.id === group?.id;
772
1026
  const isRenameSess = renameDraft && renameDraft.kind === "session" && renameDraft.id === s.id;
773
1027
  const isConfirm = confirmDel && confirmDel.kind === "session" && confirmDel.id === s.id;
774
1028
  const st = sessStatus(snap.run, s.id);
@@ -777,20 +1031,12 @@ window.__ModuleLoader__.load({
777
1031
  role: "button",
778
1032
  tabIndex: 0,
779
1033
  onClick: () => {
780
- setGid(g.id);
781
- setSid(s.id);
782
- setPartsSel(null);
783
- setConfirmDel(null);
784
- setRenameDraft(null);
1034
+ pickSession(g.id, s.id);
785
1035
  },
786
1036
  onKeyDown: (e) => {
787
1037
  if (e.key === "Enter" || e.key === " ") {
788
1038
  e.preventDefault();
789
- setGid(g.id);
790
- setSid(s.id);
791
- setPartsSel(null);
792
- setConfirmDel(null);
793
- setRenameDraft(null);
1039
+ pickSession(g.id, s.id);
794
1040
  }
795
1041
  },
796
1042
  children: [
@@ -806,57 +1052,39 @@ window.__ModuleLoader__.load({
806
1052
  })
807
1053
  }) : null
808
1054
  }),
809
- isRenameSess ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
810
- className: "dsgc-rename",
811
- value: renameDraft.value,
812
- autoFocus: true,
813
- onChange: (e) => {
1055
+ isRenameSess ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenameField, {
1056
+ draft: renameDraft,
1057
+ set: setRenameDraft,
1058
+ commit: () => {
1059
+ commitRename();
1060
+ },
1061
+ keyDown: renameKeyDown
1062
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverTip, {
1063
+ label: s.name,
1064
+ side: "right",
1065
+ delayMs: 500,
1066
+ className: "dsgc-sess-name",
1067
+ children: s.name
1068
+ }),
1069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeOps, {
1070
+ renameLabel: "重命名会话",
1071
+ deleteLabel: "删除会话",
1072
+ deleteTitle: isConfirm ? "再次点击确认删除" : "删除会话",
1073
+ danger: !!isConfirm,
1074
+ onRename: () => {
814
1075
  setRenameDraft({
815
1076
  kind: "session",
816
1077
  id: s.id,
817
- value: e.target.value
1078
+ value: s.name
818
1079
  });
819
1080
  },
820
- onBlur: () => {
821
- commitRename();
822
- },
823
- onKeyDown: renameKeyDown,
824
- onClick: (e) => {
825
- e.stopPropagation();
1081
+ onDelete: () => {
1082
+ if (isConfirm) doDelete();
1083
+ else setConfirmDel({
1084
+ kind: "session",
1085
+ id: s.id
1086
+ });
826
1087
  }
827
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
828
- className: "dsgc-sess-name",
829
- children: s.name
830
- }),
831
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
832
- className: "dsgc-nodeops",
833
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
834
- className: "dsgc-opbtn",
835
- title: "重命名会话",
836
- "aria-label": "重命名会话",
837
- onClick: (e) => {
838
- e.stopPropagation();
839
- setRenameDraft({
840
- kind: "session",
841
- id: s.id,
842
- value: s.name
843
- });
844
- },
845
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
846
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
847
- className: "dsgc-opbtn" + (isConfirm ? " danger" : ""),
848
- title: isConfirm ? "再次点击确认删除" : "删除会话",
849
- "aria-label": "删除会话",
850
- onClick: (e) => {
851
- e.stopPropagation();
852
- if (isConfirm) doDelete();
853
- else setConfirmDel({
854
- kind: "session",
855
- id: s.id
856
- });
857
- },
858
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
859
- })]
860
1088
  })
861
1089
  ]
862
1090
  }, s.id));
@@ -879,18 +1107,12 @@ window.__ModuleLoader__.load({
879
1107
  role: "button",
880
1108
  tabIndex: 0,
881
1109
  onClick: () => {
882
- setGid(g.id);
883
- setPartsSel(null);
884
- setConfirmDel(null);
885
- setRenameDraft(null);
1110
+ pickGroup(g.id);
886
1111
  },
887
1112
  onKeyDown: (e) => {
888
1113
  if (e.key === "Enter" || e.key === " ") {
889
1114
  e.preventDefault();
890
- setGid(g.id);
891
- setPartsSel(null);
892
- setConfirmDel(null);
893
- setRenameDraft(null);
1115
+ pickGroup(g.id);
894
1116
  }
895
1117
  },
896
1118
  children: [
@@ -909,57 +1131,36 @@ window.__ModuleLoader__.load({
909
1131
  },
910
1132
  children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, 14)
911
1133
  }),
912
- isRenameGroup ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
913
- className: "dsgc-rename",
914
- value: renameDraft.value,
915
- autoFocus: true,
916
- onChange: (e) => {
917
- setRenameDraft({
918
- kind: "group",
919
- id: g.id,
920
- value: e.target.value
921
- });
922
- },
923
- onBlur: () => {
1134
+ isRenameGroup ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenameField, {
1135
+ draft: renameDraft,
1136
+ set: setRenameDraft,
1137
+ commit: () => {
924
1138
  commitRename();
925
1139
  },
926
- onKeyDown: renameKeyDown,
927
- onClick: (e) => {
928
- e.stopPropagation();
929
- }
1140
+ keyDown: renameKeyDown
930
1141
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
931
1142
  className: "dsgc-gname",
932
1143
  children: g.name
933
1144
  }),
934
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
935
- className: "dsgc-nodeops",
936
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
937
- className: "dsgc-opbtn",
938
- title: "重命名群组",
939
- "aria-label": "重命名群组",
940
- onClick: (e) => {
941
- e.stopPropagation();
942
- setRenameDraft({
943
- kind: "group",
944
- id: g.id,
945
- value: g.name
946
- });
947
- },
948
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
949
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
950
- className: "dsgc-opbtn" + (isConfirmGroup ? " danger" : ""),
951
- title: isConfirmGroup ? "再次点击确认删除" : "删除群组",
952
- "aria-label": "删除群组",
953
- onClick: (e) => {
954
- e.stopPropagation();
955
- if (isConfirmGroup) doDelete();
956
- else setConfirmDel({
957
- kind: "group",
958
- id: g.id
959
- });
960
- },
961
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
962
- })]
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeOps, {
1146
+ renameLabel: "重命名群组",
1147
+ deleteLabel: "删除群组",
1148
+ deleteTitle: isConfirmGroup ? "再次点击确认删除" : "删除群组",
1149
+ danger: !!isConfirmGroup,
1150
+ onRename: () => {
1151
+ setRenameDraft({
1152
+ kind: "group",
1153
+ id: g.id,
1154
+ value: g.name
1155
+ });
1156
+ },
1157
+ onDelete: () => {
1158
+ if (isConfirmGroup) doDelete();
1159
+ else setConfirmDel({
1160
+ kind: "group",
1161
+ id: g.id
1162
+ });
1163
+ }
963
1164
  })
964
1165
  ]
965
1166
  }), expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -998,8 +1199,14 @@ window.__ModuleLoader__.load({
998
1199
  }
999
1200
  //#endregion
1000
1201
  //#region src/client/components/AsidePanel.tsx
1202
+ /**
1203
+ * 右侧成员与工作区栏组件。工作区目录编辑态(草稿、文件浏览器)为栏内自有状态。
1204
+ * @module dsh-group-chat/client/components
1205
+ */
1001
1206
  function AsidePanel(props) {
1002
- const { snap, group, asideOpen, wsDraft, setWsDraft, fileBrowser, setFileBrowser, mutate, openRoleEditor, openBrowser, selectCurrentDir } = props;
1207
+ const { snap, group, asideOpen, mutate, openRoleEditor } = props;
1208
+ const [wsDraft, setWsDraft] = (0, react.useState)(null);
1209
+ const [fileBrowser, setFileBrowser] = (0, react.useState)(null);
1003
1210
  const commitWsDir = () => {
1004
1211
  if (wsDraft !== null && group && wsDraft !== (group.workspaceDir || "")) mutate({
1005
1212
  op: "setWorkspaceDir",
@@ -1008,6 +1215,50 @@ window.__ModuleLoader__.load({
1008
1215
  });
1009
1216
  setWsDraft(null);
1010
1217
  };
1218
+ const openBrowser = async (path) => {
1219
+ setFileBrowser({
1220
+ open: true,
1221
+ loading: true,
1222
+ list: null,
1223
+ error: ""
1224
+ });
1225
+ try {
1226
+ const res = await api.action({
1227
+ kind: "browse",
1228
+ path: path || ""
1229
+ });
1230
+ if (res && res.ok) setFileBrowser({
1231
+ open: true,
1232
+ loading: false,
1233
+ list: res,
1234
+ error: ""
1235
+ });
1236
+ else setFileBrowser({
1237
+ open: true,
1238
+ loading: false,
1239
+ list: null,
1240
+ error: res && res.error || "浏览失败"
1241
+ });
1242
+ } catch (e) {
1243
+ setFileBrowser({
1244
+ open: true,
1245
+ loading: false,
1246
+ list: null,
1247
+ error: String(e && e.message || e)
1248
+ });
1249
+ }
1250
+ };
1251
+ const selectCurrentDir = () => {
1252
+ if (!fileBrowser || !fileBrowser.list || !group) return;
1253
+ const path = fileBrowser.list.path;
1254
+ mutate({
1255
+ op: "setWorkspaceDir",
1256
+ groupId: group.id,
1257
+ path
1258
+ });
1259
+ setWsDraft(null);
1260
+ setFileBrowser(null);
1261
+ };
1011
1262
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1012
1263
  className: "dsgc-aside" + (asideOpen ? "" : " closed"),
1013
1264
  "aria-hidden": asideOpen ? void 0 : "true",
@@ -1101,8 +1352,9 @@ window.__ModuleLoader__.load({
1101
1352
  }) : null,
1102
1353
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1103
1354
  className: "dsgc-rolemenu",
1104
- children: [
1105
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1355
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1356
+ className: "dsgc-rolemeta",
1357
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1106
1358
  className: "dsgc-rolemodel",
1107
1359
  title: r.provider + " / " + r.model + (r.thinking ? " · 深度思考" + (r.reasoningEffort && r.reasoningEffort !== "default" ? "(" + r.reasoningEffort + ")" : "") : ""),
1108
1360
  children: [
@@ -1110,42 +1362,36 @@ window.__ModuleLoader__.load({
1110
1362
  " / ",
1111
1363
  r.model
1112
1364
  ]
1113
- }),
1114
- r.thinking ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1365
+ }), r.thinking ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1366
+ className: "dsgc-rolethink",
1115
1367
  title: "深度思考",
1116
- style: {
1117
- display: "inline-flex",
1118
- alignItems: "center",
1119
- color: "var(--dsw-alias-label-tertiary,inherit)"
1120
- },
1121
1368
  children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, 14)
1122
- }) : null,
1123
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1124
- className: "dsgc-roleops",
1125
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1126
- className: "dsgc-opbtn",
1127
- title: "编辑角色",
1128
- "aria-label": "编辑角色",
1129
- onClick: (e) => {
1130
- e.stopPropagation();
1131
- openRoleEditor(r);
1132
- },
1133
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
1134
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1135
- className: "dsgc-opbtn danger",
1136
- title: "移除角色",
1137
- "aria-label": "移除角色",
1138
- onClick: (e) => {
1139
- e.stopPropagation();
1140
- mutate({
1141
- op: "deleteRole",
1142
- roleId: r.id
1143
- });
1144
- },
1145
- children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
1146
- })]
1147
- })
1148
- ]
1369
+ }) : null]
1370
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1371
+ className: "dsgc-roleops",
1372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1373
+ className: "dsgc-opbtn",
1374
+ title: "编辑角色",
1375
+ "aria-label": "编辑角色",
1376
+ onClick: (e) => {
1377
+ e.stopPropagation();
1378
+ openRoleEditor(r);
1379
+ },
1380
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
1381
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1382
+ className: "dsgc-opbtn danger",
1383
+ title: "移除角色",
1384
+ "aria-label": "移除角色",
1385
+ onClick: (e) => {
1386
+ e.stopPropagation();
1387
+ mutate({
1388
+ op: "deleteRole",
1389
+ roleId: r.id
1390
+ });
1391
+ },
1392
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
1393
+ })]
1394
+ })]
1149
1395
  })
1150
1396
  ]
1151
1397
  }, r.id);
@@ -1288,6 +1534,218 @@ window.__ModuleLoader__.load({
1288
1534
  });
1289
1535
  }
1290
1536
  //#endregion
1537
+ //#region src/core/json.ts
1538
+ /**
1539
+ * 宽容 JSON 提取(模型输出/供应商错误原文的统一解析入口):
1540
+ * 取首个 { 至末个 } 的片段解析,仅接受普通对象;失败返回 null。
1541
+ * 代码围栏剥离由调用方按需先行处理(供应商错误原文不剥)。
1542
+ */
1543
+ function looseJson(raw) {
1544
+ const start = raw.indexOf("{");
1545
+ const end = raw.lastIndexOf("}");
1546
+ if (start < 0 || end <= start) return null;
1547
+ try {
1548
+ const parsed = JSON.parse(raw.slice(start, end + 1));
1549
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1550
+ } catch {
1551
+ return null;
1552
+ }
1553
+ }
1554
+ //#endregion
1555
+ //#region src/core/types.ts
1556
+ /** 全部合法档位(展示顺序)。 */
1557
+ const PERMISSION_TIERS = [
1558
+ "view_only",
1559
+ "workspace_write",
1560
+ "full_access"
1561
+ ];
1562
+ /** 档位安全化:合法字符串原样,其余 undefined。 */
1563
+ function asPermissionTier(value) {
1564
+ return typeof value === "string" && PERMISSION_TIERS.includes(value) ? value : void 0;
1565
+ }
1566
+ //#endregion
1567
+ //#region src/core/constraints.ts
1568
+ const KIND_LABEL = {
1569
+ decided: "已定",
1570
+ rejected: "否决",
1571
+ open: "未决"
1572
+ };
1573
+ //#endregion
1574
+ //#region src/core/errors.ts
1575
+ const PREFIX = "模型输出异常终止: ";
1576
+ const LEGACY_ROLE = /^角色「([^」]+)」发言失败[::]\s*/;
1577
+ /** 剥旧系统胶囊与引擎包装前缀,保留供应商原文。 */
1578
+ function unwrapSpeakFailure(raw) {
1579
+ const text = String(raw || "").trim();
1580
+ const legacy = parseLegacyRoleFailure(text);
1581
+ const body = legacy ? legacy.rest : text;
1582
+ return body.startsWith(PREFIX) ? body.slice(10).trim() : body;
1583
+ }
1584
+ /** 旧系统胶囊文案:角色「名」发言失败:原文。对不上则 null。 */
1585
+ function parseLegacyRoleFailure(text) {
1586
+ const m = LEGACY_ROLE.exec(String(text || ""));
1587
+ if (!m) return null;
1588
+ return {
1589
+ roleName: m[1],
1590
+ rest: String(text).slice(m[0].length)
1591
+ };
1592
+ }
1593
+ /** 发言失败卡:error 标记,或旧系统胶囊文案。 */
1594
+ function isSpeakFailure(m) {
1595
+ return !!m.error || !!parseLegacyRoleFailure(m.text);
1596
+ }
1597
+ /** 失败回合对应角色:failedRoleId / speaker / 旧文案里的角色名(恰好一名才命中)。 */
1598
+ function resolveFailedRole(m, roles) {
1599
+ const byId = (id) => {
1600
+ if (!id) return null;
1601
+ for (const r of roles) if (r.id === id) return r;
1602
+ return null;
1603
+ };
1604
+ const direct = byId(m.failedRoleId) || (m.speaker !== "user" && m.speaker !== "system" ? byId(m.speaker) : null);
1605
+ if (direct) return direct;
1606
+ const parsed = parseLegacyRoleFailure(m.text);
1607
+ if (!parsed) return null;
1608
+ const hits = roles.filter((r) => r.name === parsed.roleName);
1609
+ return hits.length === 1 ? hits[0] : null;
1610
+ }
1611
+ function httpStatus(text) {
1612
+ const m = text.match(/\b([1-5]\d{2})\b/);
1613
+ if (!m) return null;
1614
+ const n = Number(m[1]);
1615
+ return n >= 100 && n <= 599 ? n : null;
1616
+ }
1617
+ function resetHint(message) {
1618
+ const m = message.match(/reset at ([^.]+\S)/i) || message.match(/将在\s*([^\s。]+)\s*重置/);
1619
+ return m ? "将在 " + m[1] + " 重置" : void 0;
1620
+ }
1621
+ /**
1622
+ * 把供应商错误压成可扫描的标题 + 短因。
1623
+ * 未知形态回退为「发言失败」,原文仍可展开。
1624
+ */
1625
+ function classifySpeakFailure(raw) {
1626
+ const source = unwrapSpeakFailure(raw);
1627
+ const blob = looseJson(source);
1628
+ const code = blob && typeof blob.code === "string" ? blob.code : "";
1629
+ const type = blob && typeof blob.type === "string" ? blob.type : "";
1630
+ const message = blob && typeof blob.message === "string" ? blob.message : source;
1631
+ const status = httpStatus(source);
1632
+ const joined = (code + " " + type + " " + message + " " + source).toLowerCase();
1633
+ if (/quota|accountquotaexceeded|exceeded the .*quota|额度|配额/.test(joined) || code === "AccountQuotaExceeded") return {
1634
+ title: "额度已用尽",
1635
+ detail: resetHint(message),
1636
+ raw: source
1637
+ };
1638
+ if (status === 429 || /too.?many.?requests|rate.?limit|限流/.test(joined) || type === "TooManyRequests") return {
1639
+ title: "请求过于频繁",
1640
+ detail: "稍后再试,或降低并发",
1641
+ raw: source
1642
+ };
1643
+ if (status === 401 || status === 403 || /unauthorized|forbidden|invalid.?api.?key|鉴权|未授权/.test(joined)) return {
1644
+ title: "模型鉴权失败",
1645
+ detail: "检查该角色绑定的提供方密钥",
1646
+ raw: source
1647
+ };
1648
+ if (status === 404 || /model.?not.?found|unknown.?model|模型不存在/.test(joined)) return {
1649
+ title: "模型不可用",
1650
+ detail: "该角色绑定的模型可能已下线",
1651
+ raw: source
1652
+ };
1653
+ if (/timeout|timed out|etimedout|超时/.test(joined)) return {
1654
+ title: "模型响应超时",
1655
+ raw: source
1656
+ };
1657
+ if (/network|econnreset|econnrefused|enotfound|fetch failed|网络/.test(joined)) return {
1658
+ title: "网络异常",
1659
+ detail: "检查网络后重试",
1660
+ raw: source
1661
+ };
1662
+ if (status !== null && status >= 500) return {
1663
+ title: "模型服务暂时不可用",
1664
+ detail: "HTTP " + status,
1665
+ raw: source
1666
+ };
1667
+ if (status !== null) return {
1668
+ title: "发言失败",
1669
+ detail: "HTTP " + status,
1670
+ raw: source
1671
+ };
1672
+ return {
1673
+ title: "发言失败",
1674
+ raw: source
1675
+ };
1676
+ }
1677
+ /** 失败卡默认复制内容:标题 + 短因 + 原文。 */
1678
+ function formatSpeakFailureCopy(view) {
1679
+ const lines = [view.title];
1680
+ if (view.detail) lines.push(view.detail);
1681
+ if (view.raw && view.raw !== view.title) lines.push(view.raw);
1682
+ return lines.join("\n");
1683
+ }
1684
+ //#endregion
1685
+ //#region src/client/components/Fold.tsx
1686
+ /**
1687
+ * 会话内折叠:高度 0fr→1fr + 溢出滚动遮罩。思考 / 工具 / 失败原文 / 结论备忘共用。
1688
+ * @module dsh-group-chat/client/components
1689
+ */
1690
+ function Fold(props) {
1691
+ const { open, children, className } = props;
1692
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1693
+ className: "dsgc-fold" + (open ? " open" : "") + (className ? " " + className : ""),
1694
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1695
+ className: "dsgc-fold-inner",
1696
+ children
1697
+ })
1698
+ });
1699
+ }
1700
+ function ClipWell(props) {
1701
+ const { children, maxHeight, className, watch } = props;
1702
+ const [clip, setClip] = (0, react.useState)({
1703
+ up: false,
1704
+ down: false
1705
+ });
1706
+ const ref = (0, react.useRef)(null);
1707
+ (0, react.useLayoutEffect)(() => {
1708
+ const el = ref.current;
1709
+ if (!el) return;
1710
+ const measure = () => {
1711
+ if (el.clientHeight < 2) {
1712
+ setClip((cur) => cur.up || cur.down ? {
1713
+ up: false,
1714
+ down: false
1715
+ } : cur);
1716
+ return;
1717
+ }
1718
+ const max = el.scrollHeight - el.clientHeight;
1719
+ const up = el.scrollTop > 1;
1720
+ const down = max > 1 && el.scrollTop < max - 1;
1721
+ setClip((cur) => cur.up === up && cur.down === down ? cur : {
1722
+ up,
1723
+ down
1724
+ });
1725
+ };
1726
+ measure();
1727
+ const ro = new ResizeObserver(measure);
1728
+ ro.observe(el);
1729
+ for (const child of el.children) ro.observe(child);
1730
+ el.addEventListener("scroll", measure, { passive: true });
1731
+ el.addEventListener("transitionend", measure);
1732
+ return () => {
1733
+ ro.disconnect();
1734
+ el.removeEventListener("scroll", measure);
1735
+ el.removeEventListener("transitionend", measure);
1736
+ };
1737
+ }, [watch]);
1738
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1739
+ className: "dsgc-clip" + (clip.up ? " can-up" : "") + (clip.down ? " can-down" : ""),
1740
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1741
+ className: "dsgc-clip-scroll" + (className ? " " + className : ""),
1742
+ ref,
1743
+ style: { maxHeight },
1744
+ children
1745
+ })
1746
+ });
1747
+ }
1748
+ //#endregion
1291
1749
  //#region src/client/components/ThinkRow.tsx
1292
1750
  /**
1293
1751
  * 思考折叠行(对标宿主 ReasoningRow:折叠摘要 / 展开全文)。
@@ -1299,9 +1757,9 @@ window.__ModuleLoader__.load({
1299
1757
  const [expanded, setExpanded] = (0, react.useState)(false);
1300
1758
  const plain = (line) => line.replace(/^(\s*#{1,6}\s+|\s*[-*+]\s+|\s*>\s*)+/, "").replace(/[*`_~]/g, "").replace(/\s+/g, " ").trim();
1301
1759
  const summary = plain(running ? latestLine(text) : firstLine(text));
1302
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1760
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1303
1761
  className: "dsgc-think",
1304
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1305
1763
  rowClassName: "dsgc-thinkrow",
1306
1764
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
1307
1765
  title: "思考",
@@ -1314,12 +1772,18 @@ window.__ModuleLoader__.load({
1314
1772
  collapsedContent: text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1315
1773
  className: "dsgc-thinksummary",
1316
1774
  children: summary
1317
- }) : null,
1318
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1319
- className: "dsgc-thinkbody",
1320
- children: text
1775
+ }) : null
1776
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1777
+ open: expanded,
1778
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1779
+ maxHeight: 320,
1780
+ watch: text,
1781
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1782
+ className: "dsgc-thinkbody",
1783
+ children: text
1784
+ })
1321
1785
  })
1322
- })
1786
+ })]
1323
1787
  });
1324
1788
  }
1325
1789
  //#endregion
@@ -1347,9 +1811,9 @@ window.__ModuleLoader__.load({
1347
1811
  const statusText = c.status === "ok" ? "成功" : c.status === "denied" ? "用户拒绝" : "失败";
1348
1812
  const StatusIcon = c.status === "ok" ? _deepseek_ai_dsh_client_ui_primitives.IconCheckOutline14 : c.status === "denied" ? _deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16 : _deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16;
1349
1813
  const dur = c.durationMs ? c.durationMs >= 1e3 ? (c.durationMs / 1e3).toFixed(1) + "s" : c.durationMs + "ms" : "";
1350
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1814
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
1815
  className: "dsgc-tool",
1352
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1353
1817
  rowClassName: "dsgc-toolrow",
1354
1818
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconOf, { size: 14 }),
1355
1819
  title: c.tool + (brief ? " " + brief : ""),
@@ -1366,12 +1830,138 @@ window.__ModuleLoader__.load({
1366
1830
  marginRight: 4,
1367
1831
  color: c.status === "ok" ? "var(--dsw-alias-label-tertiary,inherit)" : "var(--dsw-alias-state-error-primary,#e5484d)"
1368
1832
  }), statusText + (dur ? " · " + dur : "")]
1369
- }),
1370
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1371
- className: "dsgc-toolbody",
1372
- children: c.output || "(无输出)"
1373
1833
  })
1374
- })
1834
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1835
+ open: expanded,
1836
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1837
+ maxHeight: 260,
1838
+ watch: c.output,
1839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1840
+ className: "dsgc-toolbody",
1841
+ children: c.output || "(无输出)"
1842
+ })
1843
+ })
1844
+ })]
1845
+ });
1846
+ }
1847
+ //#endregion
1848
+ //#region src/client/components/FailCard.tsx
1849
+ /**
1850
+ * 角色发言失败卡:人话标题 + 可展开原文。操作条由 Bubble 放在气泡外下方。
1851
+ * @module dsh-group-chat/client/FailCard
1852
+ */
1853
+ function FailCard(props) {
1854
+ const view = classifySpeakFailure(props.raw);
1855
+ const [open, setOpen] = (0, react.useState)(false);
1856
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1857
+ className: "dsgc-fail",
1858
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1859
+ className: "dsgc-failhead",
1860
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1861
+ className: "dsgc-failicon",
1862
+ "aria-hidden": "true",
1863
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, 14)
1864
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1865
+ className: "dsgc-failcopy",
1866
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1867
+ className: "dsgc-failtitle",
1868
+ children: view.title
1869
+ }), view.detail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1870
+ className: "dsgc-faildetail",
1871
+ children: view.detail
1872
+ }) : null]
1873
+ })]
1874
+ }), view.raw ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1875
+ type: "button",
1876
+ className: "dsgc-failmore" + (open ? " open" : ""),
1877
+ "aria-expanded": open,
1878
+ onClick: () => {
1879
+ setOpen((v) => !v);
1880
+ },
1881
+ children: open ? "收起原始错误" : "查看原始错误"
1882
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1883
+ open,
1884
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1885
+ maxHeight: 220,
1886
+ watch: open,
1887
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1888
+ className: "dsgc-failraw",
1889
+ children: view.raw
1890
+ })
1891
+ })
1892
+ })] }) : null]
1893
+ });
1894
+ }
1895
+ //#endregion
1896
+ //#region src/client/components/MsgActions.tsx
1897
+ /**
1898
+ * 消息操作条:贴在气泡外下方。用户/角色消息悬停出「复制」;失败卡常驻「复制 / 重试」。
1899
+ * @module dsh-group-chat/client/MsgActions
1900
+ */
1901
+ async function writeClipboard(text) {
1902
+ try {
1903
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1904
+ await navigator.clipboard.writeText(text);
1905
+ return true;
1906
+ }
1907
+ } catch {}
1908
+ try {
1909
+ const el = document.createElement("textarea");
1910
+ el.value = text;
1911
+ el.setAttribute("readonly", "");
1912
+ el.style.position = "fixed";
1913
+ el.style.left = "-9999px";
1914
+ document.body.appendChild(el);
1915
+ el.select();
1916
+ const ok = document.execCommand("copy");
1917
+ document.body.removeChild(el);
1918
+ return ok;
1919
+ } catch {
1920
+ return false;
1921
+ }
1922
+ }
1923
+ function MsgActions(props) {
1924
+ const { copyText, onRetry, retryDisabled, retryTitle, always } = props;
1925
+ const [copied, setCopied] = (0, react.useState)(false);
1926
+ const copiedTimer = (0, react.useRef)(null);
1927
+ (0, react.useEffect)(() => () => {
1928
+ if (copiedTimer.current) clearTimeout(copiedTimer.current);
1929
+ }, []);
1930
+ const copy = async () => {
1931
+ if (!copyText) return;
1932
+ if (!await writeClipboard(copyText)) return;
1933
+ setCopied(true);
1934
+ if (copiedTimer.current) clearTimeout(copiedTimer.current);
1935
+ copiedTimer.current = setTimeout(() => {
1936
+ setCopied(false);
1937
+ }, 1400);
1938
+ };
1939
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1940
+ className: "dsgc-msgops" + (always ? " always" : ""),
1941
+ role: "group",
1942
+ "aria-label": "消息操作",
1943
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1944
+ type: "button",
1945
+ className: "dsgc-msgop" + (copied ? " done" : ""),
1946
+ title: copied ? "已复制" : "复制",
1947
+ "aria-label": copied ? "已复制" : "复制",
1948
+ onClick: (e) => {
1949
+ e.stopPropagation();
1950
+ copy();
1951
+ },
1952
+ children: [Icon(copied ? _deepseek_ai_dsh_client_ui_primitives.IconCheckOutline14 : _deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, 14), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: copied ? "已复制" : "复制" })]
1953
+ }), onRetry ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1954
+ type: "button",
1955
+ className: "dsgc-msgop retry",
1956
+ title: retryTitle || "重试",
1957
+ "aria-label": retryTitle || "重试",
1958
+ disabled: retryDisabled,
1959
+ onClick: (e) => {
1960
+ e.stopPropagation();
1961
+ if (!retryDisabled) onRetry();
1962
+ },
1963
+ children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, 14), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "重试" })]
1964
+ }) : null]
1375
1965
  });
1376
1966
  }
1377
1967
  //#endregion
@@ -1385,62 +1975,78 @@ window.__ModuleLoader__.load({
1385
1975
  * MarkdownText 重新解析打满主线程导致「卡死后一次性蹦出」。
1386
1976
  * @module dsh-group-chat/client/Bubble
1387
1977
  */
1388
- function BubbleInner({ m, role }) {
1978
+ function BubbleInner({ m, role, busy, onRetry }) {
1389
1979
  const isUser = m.speaker === "user";
1390
- const isSys = m.speaker === "system";
1391
- const name = isUser ? "" : isSys ? "系统" : role ? role.name : "成员";
1980
+ const isFail = isSpeakFailure(m);
1981
+ const isSys = m.speaker === "system" && !isFail && !role;
1982
+ const name = isUser ? "我" : role ? role.name : isSys ? "系统" : "成员";
1392
1983
  if (isSys) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1393
- className: "dsgc-sysmsg" + (m.error ? " err" : ""),
1984
+ className: "dsgc-sysmsg",
1394
1985
  children: m.text
1395
1986
  });
1987
+ const retryTitle = !role ? "失败角色已不存在,无法重试" : !role.enabled ? "该角色已停用,无法重试" : busy ? "已有对话进行中,请先停止" : "重试该角色发言";
1988
+ const copyText = isFail ? formatSpeakFailureCopy(classifySpeakFailure(m.text)) : m.text || "";
1396
1989
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1397
- className: "dsgc-msg" + (isUser ? " mine" : ""),
1990
+ className: "dsgc-msg" + (isUser ? " mine" : "") + (isFail ? " fail" : ""),
1398
1991
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1399
1992
  className: "dsgc-avatar" + (isUser ? " mine" : ""),
1400
1993
  style: isUser || !role ? void 0 : { border: "2px solid " + (role.color || "#888") },
1401
1994
  children: name.slice(0, 1)
1402
1995
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1403
1996
  className: "dsgc-msgbody",
1404
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1405
- className: "dsgc-msghead",
1406
- children: [
1407
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1408
- className: "dsgc-msgname",
1409
- children: name
1410
- }),
1411
- m.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1412
- className: "dsgc-msgmodel",
1413
- title: m.model,
1414
- children: m.model
1415
- }) : null,
1416
- m.ts ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1417
- className: "dsgc-msgtime",
1418
- children: fmtTime(m.ts)
1419
- }) : null
1420
- ]
1421
- }), isUser ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1422
- className: "dsgc-msgtext",
1423
- children: m.text
1424
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1425
- className: "dsgc-msgtext",
1426
- children: [
1427
- m.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, { text: m.reasoning }) : null,
1428
- (Array.isArray(m.toolCalls) ? m.toolCalls : []).map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolRow, { c }, "tc" + i)),
1429
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1430
- text: m.text || "(无内容)",
1431
- labels: MD_LABELS
1432
- })
1433
- ]
1434
- })]
1997
+ children: [
1998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1999
+ className: "dsgc-msghead",
2000
+ children: [
2001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2002
+ className: "dsgc-msgname",
2003
+ children: name
2004
+ }),
2005
+ m.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2006
+ className: "dsgc-msgmodel",
2007
+ title: m.model,
2008
+ children: m.model
2009
+ }) : null,
2010
+ m.ts ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2011
+ className: "dsgc-msgtime",
2012
+ children: fmtTime(m.ts)
2013
+ }) : null
2014
+ ]
2015
+ }),
2016
+ isFail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailCard, { raw: m.text }) : isUser ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2017
+ className: "dsgc-msgtext",
2018
+ children: m.text
2019
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2020
+ className: "dsgc-msgtext",
2021
+ children: [
2022
+ m.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, { text: m.reasoning }) : null,
2023
+ (Array.isArray(m.toolCalls) ? m.toolCalls : []).map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolRow, { c }, "tc" + i)),
2024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2025
+ text: m.text || "(无内容)",
2026
+ labels: MD_LABELS
2027
+ })
2028
+ ]
2029
+ }),
2030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MsgActions, {
2031
+ copyText,
2032
+ always: isFail,
2033
+ onRetry: isFail && onRetry ? () => {
2034
+ onRetry(m.id);
2035
+ } : void 0,
2036
+ retryDisabled: busy || !role || !role.enabled,
2037
+ retryTitle
2038
+ })
2039
+ ]
1435
2040
  })]
1436
2041
  });
1437
2042
  }
1438
2043
  /** 渲染相关字段的值比较(消息不可变;角色仅名/色参与渲染)。 */
1439
2044
  function bubblePropsEqual(a, b) {
2045
+ if (a.busy !== b.busy || a.onRetry == null !== (b.onRetry == null)) return false;
1440
2046
  const x = a.m;
1441
2047
  const y = b.m;
1442
2048
  if (x !== y) {
1443
- if (x.id !== y.id || x.speaker !== y.speaker || x.text !== y.text || x.reasoning !== y.reasoning || x.model !== y.model || x.error !== y.error || x.ts !== y.ts) return false;
2049
+ if (x.id !== y.id || x.speaker !== y.speaker || x.text !== y.text || x.reasoning !== y.reasoning || x.model !== y.model || x.error !== y.error || x.failedRoleId !== y.failedRoleId || x.ts !== y.ts) return false;
1444
2050
  const ta = Array.isArray(x.toolCalls) ? x.toolCalls : [];
1445
2051
  const tb = Array.isArray(y.toolCalls) ? y.toolCalls : [];
1446
2052
  if (ta.length !== tb.length) return false;
@@ -1452,151 +2058,219 @@ window.__ModuleLoader__.load({
1452
2058
  }
1453
2059
  const ra = a.role;
1454
2060
  const rb = b.role;
1455
- return (ra ? ra.name + "\0" + (ra.color || "") : "") === (rb ? rb.name + "\0" + (rb.color || "") : "");
2061
+ return (ra ? ra.id + "\0" + ra.name + "\0" + (ra.color || "") + "\0" + (ra.enabled ? "1" : "0") : "") === (rb ? rb.id + "\0" + rb.name + "\0" + (rb.color || "") + "\0" + (rb.enabled ? "1" : "0") : "");
1456
2062
  }
1457
2063
  const Bubble = (0, react.memo)(BubbleInner, bubblePropsEqual);
1458
2064
  //#endregion
2065
+ //#region src/client/components/ConstraintList.tsx
2066
+ /**
2067
+ * 会话流折点处:只读结论备忘卡(超过 4 条默认露 3 条)。
2068
+ * @module dsh-group-chat/client/components
2069
+ */
2070
+ function ConstraintRow(props) {
2071
+ const { item } = props;
2072
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2073
+ className: "dsgc-constraint",
2074
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2075
+ className: "dsgc-ckind " + item.kind,
2076
+ children: KIND_LABEL[item.kind]
2077
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2078
+ className: "dsgc-ctext",
2079
+ children: item.text
2080
+ })]
2081
+ });
2082
+ }
2083
+ function ConstraintList(props) {
2084
+ const { items } = props;
2085
+ const [open, setOpen] = (0, react.useState)(false);
2086
+ if (!items.length) return null;
2087
+ const overflow = items.length > 4;
2088
+ const head = overflow ? items.slice(0, 3) : items;
2089
+ const extra = overflow ? items.slice(3) : [];
2090
+ const rest = items.length - 3;
2091
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2092
+ className: "dsgc-constraints",
2093
+ "aria-labelledby": "dsgc-constraints-title",
2094
+ children: [
2095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2096
+ className: "dsgc-chead",
2097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2098
+ id: "dsgc-constraints-title",
2099
+ className: "dsgc-ctitle",
2100
+ children: "结论备忘"
2101
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2102
+ className: "dsgc-cdesc",
2103
+ children: "窗口外消息折成的已定 / 否决 / 未决,供后续角色接着用。"
2104
+ })]
2105
+ }),
2106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ClipWell, {
2107
+ maxHeight: 160,
2108
+ className: "dsgc-clist",
2109
+ watch: open,
2110
+ children: [head.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintRow, { item: c }, i)), overflow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
2111
+ open,
2112
+ children: extra.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintRow, { item: c }, i + 3))
2113
+ }) : null]
2114
+ }),
2115
+ overflow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2116
+ type: "button",
2117
+ className: "dsgc-cmore",
2118
+ onClick: () => {
2119
+ setOpen(!open);
2120
+ },
2121
+ "aria-expanded": open,
2122
+ children: open ? "收起" : "还有 " + rest + " 条约束"
2123
+ }) : null
2124
+ ]
2125
+ });
2126
+ }
2127
+ //#endregion
1459
2128
  //#region src/client/components/MessageFlow.tsx
1460
2129
  function MessageFlow(props) {
1461
- const { snap, sess, busyNow, msgById, action } = props;
2130
+ const { snap, sess, busyNow, msgById, action, onRetrySpeak } = props;
1462
2131
  const bubbles = [];
1463
- if (sess) for (const mid of sess.messageIds) {
1464
- const m = msgById[mid];
1465
- if (!m) continue;
1466
- const role = m.speaker !== "user" && m.speaker !== "system" ? roleById(snap, m.speaker) : null;
1467
- bubbles.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bubble, {
1468
- m,
1469
- role
1470
- }, m.id));
1471
- }
1472
- if (busyNow && snap.run.currentRoleId) {
1473
- const lr = roleById(snap, snap.run.currentRoleId);
1474
- if (lr) bubbles.push(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1475
- className: "dsgc-msg live",
1476
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1477
- className: "dsgc-avatar",
1478
- style: {
1479
- border: "2px solid " + (lr.color || "#888"),
1480
- "--role-color": lr.color || "#888"
1481
- },
1482
- children: lr.name.slice(0, 1)
2132
+ const replaceId = busyNow ? snap.run.replaceMessageId : null;
2133
+ const replaceMsg = replaceId ? msgById[replaceId] : null;
2134
+ const liveRoleId = snap.run.currentRoleId || replaceMsg && (replaceMsg.failedRoleId || replaceMsg.speaker) || null;
2135
+ const lr = busyNow && liveRoleId ? roleById(snap, liveRoleId) : null;
2136
+ const live = lr ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2137
+ className: "dsgc-msg live",
2138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2139
+ className: "dsgc-avatar",
2140
+ style: {
2141
+ border: "2px solid " + (lr.color || "#888"),
2142
+ "--role-color": lr.color || "#888"
2143
+ },
2144
+ children: lr.name.slice(0, 1)
2145
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2146
+ className: "dsgc-msgbody",
2147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2148
+ className: "dsgc-msghead",
2149
+ children: [
2150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2151
+ className: "dsgc-msgname",
2152
+ children: lr.name
2153
+ }),
2154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2155
+ className: "dsgc-msgmodel",
2156
+ children: [
2157
+ lr.provider,
2158
+ " / ",
2159
+ lr.model
2160
+ ]
2161
+ }),
2162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2163
+ className: "dsgc-msgtime dsgc-typing",
2164
+ children: "深度求索..."
2165
+ })
2166
+ ]
1483
2167
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1484
- className: "dsgc-msgbody",
1485
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1486
- className: "dsgc-msghead",
1487
- children: [
1488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1489
- className: "dsgc-msgname",
1490
- children: lr.name
1491
- }),
1492
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1493
- className: "dsgc-msgmodel",
1494
- children: [
1495
- lr.provider,
1496
- " / ",
1497
- lr.model
1498
- ]
1499
- }),
1500
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1501
- className: "dsgc-msgtime dsgc-typing",
1502
- children: "深度求索..."
1503
- })
1504
- ]
1505
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1506
- className: "dsgc-msgtext live",
1507
- children: [
1508
- snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, {
1509
- text: snap.run.partialReasoning,
1510
- running: true
1511
- }) : null,
1512
- snap.run.partial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1513
- text: snap.run.partial,
1514
- streaming: true,
1515
- labels: MD_LABELS
1516
- }) : null,
1517
- !snap.run.partial && !snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1518
- className: "dsgc-pending",
1519
- children: [
1520
- Icon(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, 14),
1521
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "思考中" }),
1522
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1523
- className: "dsgc-pendingdots",
1524
- "aria-hidden": "true",
1525
- children: [
1526
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
1527
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
1528
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
1529
- ]
1530
- })
1531
- ]
1532
- }) : null
1533
- ]
1534
- })]
2168
+ className: "dsgc-msgtext live",
2169
+ children: [
2170
+ snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, {
2171
+ text: snap.run.partialReasoning,
2172
+ running: true
2173
+ }) : null,
2174
+ snap.run.partial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2175
+ text: snap.run.partial,
2176
+ streaming: true,
2177
+ labels: MD_LABELS
2178
+ }) : null,
2179
+ !snap.run.partial && !snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2180
+ className: "dsgc-pending",
2181
+ children: [
2182
+ Icon(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, 14),
2183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "思考中" }),
2184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2185
+ className: "dsgc-pendingdots",
2186
+ "aria-hidden": "true",
2187
+ children: [
2188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
2189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
2190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
2191
+ ]
2192
+ })
2193
+ ]
2194
+ }) : null
2195
+ ]
1535
2196
  })]
1536
- }, "__live"));
1537
- }
2197
+ })]
2198
+ }, "__live") : null;
1538
2199
  const pc = busyNow && snap.run.pendingConfirm && sess && snap.run.sessionId === sess.id ? snap.run.pendingConfirm : null;
1539
- if (pc) {
1540
- const lr = roleById(snap, snap.run.currentRoleId);
1541
- bubbles.push(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1542
- className: "dsgc-confirm",
1543
- children: [
1544
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
- className: "dsgc-confirmtitle",
1546
- children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, 14), (lr ? lr.name : "角色") + " 请求执行命令(工作区内)"]
1547
- }),
1548
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1549
- className: "dsgc-confirmcmd",
1550
- children: String(pc.args && pc.args.command || "")
1551
- }),
1552
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1553
- className: "dsgc-hint",
1554
- children: "允许后将在工作区目录执行;拒绝后角色将继续纯文本讨论"
1555
- }),
1556
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1557
- className: "dsgc-confirmops",
1558
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1559
- variant: "primary",
1560
- size: "sm",
1561
- onClick: () => {
1562
- action({
1563
- kind: "confirmCommand",
1564
- toolCallId: pc.toolCallId,
1565
- allow: true
1566
- });
1567
- },
1568
- children: "允许"
1569
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1570
- variant: "outline",
1571
- size: "sm",
1572
- onClick: () => {
1573
- action({
1574
- kind: "confirmCommand",
1575
- toolCallId: pc.toolCallId,
1576
- allow: false
1577
- });
1578
- },
1579
- children: "拒绝"
1580
- })]
1581
- })
1582
- ]
1583
- }, "__confirm"));
2200
+ const confirm = pc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2201
+ className: "dsgc-confirm",
2202
+ children: [
2203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
+ className: "dsgc-confirmtitle",
2205
+ children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, 14), (lr ? lr.name : "角色") + " 请求执行命令(工作区内)"]
2206
+ }),
2207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2208
+ className: "dsgc-confirmcmd",
2209
+ children: String(pc.args && pc.args.command || "")
2210
+ }),
2211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2212
+ className: "dsgc-hint",
2213
+ children: "允许后将在工作区目录执行;拒绝后角色将继续纯文本讨论"
2214
+ }),
2215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2216
+ className: "dsgc-confirmops",
2217
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2218
+ variant: "primary",
2219
+ size: "sm",
2220
+ onClick: () => {
2221
+ action({
2222
+ kind: "confirmCommand",
2223
+ toolCallId: pc.toolCallId,
2224
+ allow: true
2225
+ });
2226
+ },
2227
+ children: "允许"
2228
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2229
+ variant: "outline",
2230
+ size: "sm",
2231
+ onClick: () => {
2232
+ action({
2233
+ kind: "confirmCommand",
2234
+ toolCallId: pc.toolCallId,
2235
+ allow: false
2236
+ });
2237
+ },
2238
+ children: "拒绝"
2239
+ })]
2240
+ })
2241
+ ]
2242
+ }, "__confirm") : null;
2243
+ let livePlaced = false;
2244
+ if (sess) {
2245
+ const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId);
2246
+ const ids = sess.messageIds;
2247
+ const memo = sess.constraints && sess.constraints.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintList, { items: sess.constraints }, sess.id + "-constraints") : null;
2248
+ const foldAt = memo && ids.length > 40 ? ids.length - 40 : 0;
2249
+ if (memo && foldAt === 0) bubbles.push(memo);
2250
+ for (let i = 0; i < ids.length; i++) {
2251
+ if (memo && foldAt > 0 && i === foldAt) bubbles.push(memo);
2252
+ const m = msgById[ids[i]];
2253
+ if (!m) continue;
2254
+ const role = resolveFailedRole(m, groupRoles) || (m.speaker !== "user" && m.speaker !== "system" ? roleById(snap, m.speaker) : null);
2255
+ if (replaceId && m.id === replaceId) {
2256
+ if (live) bubbles.push(live);
2257
+ if (confirm) bubbles.push(confirm);
2258
+ livePlaced = true;
2259
+ continue;
2260
+ }
2261
+ bubbles.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bubble, {
2262
+ m,
2263
+ role,
2264
+ busy: !!snap.run.running,
2265
+ onRetry: onRetrySpeak
2266
+ }, m.id));
2267
+ }
1584
2268
  }
2269
+ if (live && !livePlaced) bubbles.push(live);
2270
+ if (confirm && !livePlaced) bubbles.push(confirm);
1585
2271
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: bubbles });
1586
2272
  }
1587
2273
  //#endregion
1588
- //#region src/core/types.ts
1589
- /** 全部合法档位(展示顺序)。 */
1590
- const PERMISSION_TIERS = [
1591
- "view_only",
1592
- "workspace_write",
1593
- "full_access"
1594
- ];
1595
- /** 档位安全化:合法字符串原样,其余 undefined。 */
1596
- function asPermissionTier(value) {
1597
- return typeof value === "string" && PERMISSION_TIERS.includes(value) ? value : void 0;
1598
- }
1599
- //#endregion
1600
2274
  //#region src/client/components/PermissionSelect.tsx
1601
2275
  /**
1602
2276
  * 群组权限档位选择器(对齐主会话 composer 左下角的 /permission 选择器):
@@ -1790,6 +2464,8 @@ window.__ModuleLoader__.load({
1790
2464
  }
1791
2465
  //#endregion
1792
2466
  //#region src/client/components/Composer.tsx
2467
+ /** 轮数控件 hover:宿主 Tooltip 三行说明(pre-line)。 */
2468
+ const ROUNDS_HINT = "一轮 = 参与角色各说一次。\n要他们自己互相反驳、你不插话时再加轮。\n要边看边插话,就留 1,再点发送。";
1793
2469
  function Composer(props) {
1794
2470
  const { snap, sess, group, enabledRoles, participants, mentionedRoles, busyNow, input, mention, mentionCandidates, mentionIdxC, fileCandidates, fileSearchError, fileSearchLoading, rounds, err, atBottom, bubblesLength, inputRef, scrollRef, setMentionIdx, setRounds, togglePart, onInputCE, onInputKeyDown, onPasteCE, onDropCE, onDragOverCE, insertChip, insertFileChip, sendMsg, stopRun, mutate, setMention } = props;
1795
2471
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1819,26 +2495,29 @@ window.__ModuleLoader__.load({
1819
2495
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1820
2496
  className: "dsgc-partslabel",
1821
2497
  children: "参与角色"
1822
- }), mentionedRoles.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1823
- className: "dsgc-partslabel",
1824
- children: [
1825
- "已 @ ",
1826
- mentionedRoles.map((r) => r.name).join("、"),
1827
- "(本轮仅被点名成员发言)"
1828
- ]
1829
- }) : enabledRoles.length ? enabledRoles.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1830
- type: "button",
1831
- className: "dsgc-partchip" + (participants.includes(r.id) ? " on" : ""),
1832
- onClick: () => togglePart(r.id),
1833
- disabled: busyNow,
1834
- title: busyNow ? "对话进行中,暂停调整" : "点击切换本轮是否参与",
1835
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1836
- className: "dsgc-chipdot",
1837
- style: { background: r.color || "#888" }
1838
- }), r.name]
1839
- }, r.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1840
- className: "dsgc-hint",
1841
- children: "还没有启用的角色,请在右侧添加"
2498
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2499
+ className: "dsgc-partlist",
2500
+ children: mentionedRoles.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2501
+ className: "dsgc-partslabel",
2502
+ children: [
2503
+ "已 @ ",
2504
+ mentionedRoles.map((r) => r.name).join("、"),
2505
+ "(本轮仅被点名成员发言)"
2506
+ ]
2507
+ }) : enabledRoles.length ? enabledRoles.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2508
+ type: "button",
2509
+ className: "dsgc-partchip" + (participants.includes(r.id) ? " on" : ""),
2510
+ onClick: () => togglePart(r.id),
2511
+ disabled: busyNow,
2512
+ title: busyNow ? "对话进行中,暂停调整" : "点击切换本轮是否参与",
2513
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2514
+ className: "dsgc-chipdot",
2515
+ style: { background: r.color || "#888" }
2516
+ }), r.name]
2517
+ }, r.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2518
+ className: "dsgc-hint",
2519
+ children: "还没有启用的角色,请在右侧添加"
2520
+ })
1842
2521
  })]
1843
2522
  }),
1844
2523
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1970,9 +2649,12 @@ window.__ModuleLoader__.load({
1970
2649
  }
1971
2650
  }),
1972
2651
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1973
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverTip, {
2653
+ label: ROUNDS_HINT,
2654
+ side: "top",
2655
+ delayMs: 500,
2656
+ maxWidth: 280,
1974
2657
  className: "dsgc-rounds",
1975
- title: "自由讨论的轮数(1–10):一轮 = 全体参与角色按顺序各发言一次",
1976
2658
  children: [
1977
2659
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1978
2660
  type: "button",
@@ -1986,7 +2668,6 @@ window.__ModuleLoader__.load({
1986
2668
  }),
1987
2669
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1988
2670
  className: "dsgc-roundnum",
1989
- title: "轮数",
1990
2671
  children: rounds
1991
2672
  }),
1992
2673
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2029,7 +2710,7 @@ window.__ModuleLoader__.load({
2029
2710
  //#endregion
2030
2711
  //#region src/client/components/ChatPanel.tsx
2031
2712
  function ChatPanel(props) {
2032
- const { snap, sess, group, enabledRoles, participants, mentionedRoles, busyNow, input, mention, mentionCandidates, mentionIdxC, fileCandidates, fileSearchError, fileSearchLoading, rounds, err, atBottom, topicDraft, msgById, navOpen, asideOpen, inputRef, scrollRef, setTopicDraft, setConfirmClear, setMentionIdx, setRounds, setNavOpen, setAsideOpen, togglePart, onMsgsScroll, onInputCE, onInputKeyDown, onPasteCE, onDropCE, onDragOverCE, insertChip, insertFileChip, sendMsg, stopRun, action, mutate, setMention } = props;
2713
+ const { snap, sess, group, enabledRoles, participants, mentionedRoles, busyNow, input, mention, mentionCandidates, mentionIdxC, fileCandidates, fileSearchError, fileSearchLoading, rounds, err, atBottom, topicDraft, msgById, navOpen, asideOpen, inputRef, scrollRef, setTopicDraft, setConfirmClear, setMentionIdx, setRounds, setNavOpen, setAsideOpen, togglePart, onMsgsScroll, onInputCE, onInputKeyDown, onPasteCE, onDropCE, onDragOverCE, insertChip, insertFileChip, sendMsg, stopRun, action, mutate, setMention, onRetrySpeak } = props;
2033
2714
  const commitTopic = () => {
2034
2715
  if (topicDraft !== null && sess && topicDraft !== sess.topic) mutate({
2035
2716
  op: "setTopic",
@@ -2043,7 +2724,8 @@ window.__ModuleLoader__.load({
2043
2724
  sess,
2044
2725
  busyNow,
2045
2726
  msgById,
2046
- action
2727
+ action,
2728
+ onRetrySpeak
2047
2729
  });
2048
2730
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2049
2731
  className: "dsgc-chat",
@@ -2070,37 +2752,40 @@ window.__ModuleLoader__.load({
2070
2752
  },
2071
2753
  children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, 16)
2072
2754
  }),
2073
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2074
2756
  className: "dsgc-chathead",
2075
- children: [
2076
- sess ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2077
- className: "dsgc-sess-title",
2078
- title: "当前会话:" + sess.name,
2079
- children: sess.name
2080
- }) : null,
2081
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2082
- className: "dsgc-topic",
2083
- value: topicDraft === null ? sess ? sess.topic : "" : topicDraft,
2084
- placeholder: "设置本会话主题(可选)…",
2085
- onChange: (e) => {
2086
- setTopicDraft(e.target.value);
2087
- },
2088
- onBlur: commitTopic,
2089
- onKeyDown: (e) => {
2090
- if (e.key === "Enter") e.target.blur();
2091
- }
2092
- }),
2093
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2094
- variant: "ghost",
2095
- size: "sm",
2096
- className: "dsgc-clearbtn",
2097
- title: "清空当前会话的消息记录",
2098
- onClick: () => {
2099
- if (sess) setConfirmClear(true);
2100
- },
2101
- children: "清空"
2102
- })
2103
- ]
2757
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2758
+ className: "dsgc-chathead-row",
2759
+ children: [
2760
+ sess ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2761
+ className: "dsgc-sess-title",
2762
+ title: "当前会话:" + sess.name,
2763
+ children: sess.name
2764
+ }) : null,
2765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2766
+ className: "dsgc-topic",
2767
+ value: topicDraft === null ? sess ? sess.topic : "" : topicDraft,
2768
+ placeholder: "设置本会话主题(可选)…",
2769
+ onChange: (e) => {
2770
+ setTopicDraft(e.target.value);
2771
+ },
2772
+ onBlur: commitTopic,
2773
+ onKeyDown: (e) => {
2774
+ if (e.key === "Enter") e.target.blur();
2775
+ }
2776
+ }),
2777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2778
+ variant: "ghost",
2779
+ size: "sm",
2780
+ className: "dsgc-clearbtn",
2781
+ title: "清空当前会话的消息记录",
2782
+ onClick: () => {
2783
+ if (sess) setConfirmClear(true);
2784
+ },
2785
+ children: "清空"
2786
+ })
2787
+ ]
2788
+ })
2104
2789
  }),
2105
2790
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2106
2791
  className: "dsgc-msgs",
@@ -2161,6 +2846,41 @@ window.__ModuleLoader__.load({
2161
2846
  });
2162
2847
  }
2163
2848
  //#endregion
2849
+ //#region src/client/lib/composer-draft.ts
2850
+ const LIMIT = 32;
2851
+ const drafts = /* @__PURE__ */ new Map();
2852
+ const empty = () => ({
2853
+ html: "",
2854
+ text: ""
2855
+ });
2856
+ /** 读指定会话草稿;命中则提到 LRU 最近端。 */
2857
+ function readComposerDraft(sessionId) {
2858
+ if (!sessionId) return empty();
2859
+ const hit = drafts.get(sessionId);
2860
+ if (!hit) return empty();
2861
+ drafts.delete(sessionId);
2862
+ drafts.set(sessionId, hit);
2863
+ return hit;
2864
+ }
2865
+ /** 写入;空草稿删槽。超出上限淘汰最旧槽。 */
2866
+ function writeComposerDraft(sessionId, html, text) {
2867
+ if (!sessionId) return;
2868
+ drafts.delete(sessionId);
2869
+ if (!html && !text) return;
2870
+ drafts.set(sessionId, {
2871
+ html,
2872
+ text
2873
+ });
2874
+ if (drafts.size > LIMIT) {
2875
+ const oldest = drafts.keys().next().value;
2876
+ if (oldest !== void 0) drafts.delete(oldest);
2877
+ }
2878
+ }
2879
+ /** 发送成功或明确丢弃时摘槽。 */
2880
+ function clearComposerDraft(sessionId) {
2881
+ if (sessionId) drafts.delete(sessionId);
2882
+ }
2883
+ //#endregion
2164
2884
  //#region src/client/hooks/useGroupChatState.ts
2165
2885
  /**
2166
2886
  * 群聊面板核心状态管理 hook
@@ -2168,8 +2888,8 @@ window.__ModuleLoader__.load({
2168
2888
  */
2169
2889
  /**
2170
2890
  * 面板切换缓存(模块级,跨挂载存活):main 为 keyed 槽,主会话⇄群聊切换会
2171
- * 整体卸载重挂面板组件;缓存最近快照与选中项让重挂载瞬时恢复上次视图,
2172
- * 挂载后的 state 拉取 / SSE 首帧再行校正(本机回路往返仅数毫秒)。
2891
+ * 整体卸载重挂面板组件;缓存最近快照、选中项与按会话分槽的未发送草稿,
2892
+ * 让重挂载瞬时恢复上次视图。挂载后的 state 拉取 / SSE 首帧再行校正。
2173
2893
  */
2174
2894
  let cachedSnap = null;
2175
2895
  let cachedGid = null;
@@ -2204,15 +2924,12 @@ window.__ModuleLoader__.load({
2204
2924
  const [confirmDel, setConfirmDel] = (0, react.useState)(null);
2205
2925
  const [confirmClear, setConfirmClear] = (0, react.useState)(false);
2206
2926
  const [topicDraft, setTopicDraft] = (0, react.useState)(null);
2207
- const [wsDraft, setWsDraft] = (0, react.useState)(null);
2208
2927
  const [roleDraft, setRoleDraft] = (0, react.useState)(null);
2209
- const [roleFormError, setRoleFormError] = (0, react.useState)("");
2210
2928
  const [models, setModels] = (0, react.useState)(null);
2211
2929
  const [modelsError, setModelsError] = (0, react.useState)(null);
2212
- const [fileBrowser, setFileBrowser] = (0, react.useState)(null);
2213
2930
  const [partsSel, setPartsSel] = (0, react.useState)(null);
2214
2931
  const [rounds, setRounds] = (0, react.useState)(1);
2215
- const [input, setInput] = (0, react.useState)("");
2932
+ const [input, setInput] = (0, react.useState)(() => readComposerDraft(cachedSid).text);
2216
2933
  const [mention, setMention] = (0, react.useState)(null);
2217
2934
  const [mentionIdx, setMentionIdx] = (0, react.useState)(0);
2218
2935
  const [err, setErr] = (0, react.useState)("");
@@ -2289,16 +3006,10 @@ window.__ModuleLoader__.load({
2289
3006
  setConfirmClear,
2290
3007
  roleDraft,
2291
3008
  setRoleDraft,
2292
- roleFormError,
2293
- setRoleFormError,
2294
3009
  models,
2295
3010
  setModels,
2296
3011
  modelsError,
2297
3012
  setModelsError,
2298
- fileBrowser,
2299
- setFileBrowser,
2300
- wsDraft,
2301
- setWsDraft,
2302
3013
  partsSel,
2303
3014
  setPartsSel,
2304
3015
  rounds,
@@ -2331,7 +3042,7 @@ window.__ModuleLoader__.load({
2331
3042
  /**
2332
3043
  * Extract active @token at cursor position.
2333
3044
  * Matches @ at word boundary (line start or after whitespace).
2334
- *
3045
+ *
2335
3046
  * @param line - Current line text
2336
3047
  * @param cursor - Cursor offset in line
2337
3048
  * @returns Token or undefined if no active @token at cursor
@@ -2389,10 +3100,10 @@ window.__ModuleLoader__.load({
2389
3100
  };
2390
3101
  return Array.from(root.childNodes).map(walk).join("");
2391
3102
  }
2392
- /** @角色芯片的 HTML(原子元素:contenteditable=false + draggable,退格整删)。 */
2393
- function chipHtml(role) {
3103
+ /** @角色芯片的 HTML(原子元素:contenteditable=false + draggable,退格整删;fresh=插入后需营救选区,加 data-new 标记)。 */
3104
+ function chipHtml(role, fresh = false) {
2394
3105
  const c = escapeHtml(role.color || "#888");
2395
- return "<span class=\"dsgc-chipin\" data-role-id=\"" + escapeHtml(role.id) + "\" data-name=\"" + escapeHtml(role.name) + "\" style=\"--role-color:" + c + "\" contenteditable=\"false\" draggable=\"true\"><span class=\"dsgc-chipdot\" style=\"background:" + c + "\"></span>" + escapeHtml(role.name) + "</span>";
3106
+ return "<span class=\"dsgc-chipin\"" + (fresh ? " data-new=\"\"" : "") + " data-role-id=\"" + escapeHtml(role.id) + "\" data-name=\"" + escapeHtml(role.name) + "\" style=\"--role-color:" + c + "\" contenteditable=\"false\" draggable=\"true\"><span class=\"dsgc-chipdot\" style=\"background:" + c + "\"></span>" + escapeHtml(role.name) + "</span>";
2396
3107
  }
2397
3108
  /** 把宿主 FileTypeIcon 渲成静态 SVG,再塞进 insertHTML(与检索列表同一套字形)。 */
2398
3109
  function fileTypeIconMarkup(path, kind) {
@@ -2414,10 +3125,10 @@ window.__ModuleLoader__.load({
2414
3125
  root.unmount();
2415
3126
  }
2416
3127
  }
2417
- /** @文件芯片的 HTML(原子元素:contenteditable=false + draggable,退格整删)。 */
2418
- function fileChipHtml(path, kind) {
3128
+ /** @文件芯片的 HTML(原子元素:contenteditable=false + draggable,退格整删;fresh 同 chipHtml)。 */
3129
+ function fileChipHtml(path, kind, fresh = false) {
2419
3130
  const basename = path.split("/").pop() || path;
2420
- return "<span class=\"dsgc-chipin dsgc-chipin-file\" data-kind=\"file\"" + (kind === "directory" ? " data-dir=\"1\"" : "") + " data-path=\"" + escapeHtml(path) + "\" contenteditable=\"false\" draggable=\"true\"><span class=\"dsgc-chipglyph\" aria-hidden=\"true\">" + fileTypeIconMarkup(path, kind) + "</span>" + escapeHtml(basename) + "</span>";
3131
+ return "<span class=\"dsgc-chipin dsgc-chipin-file\"" + (fresh ? " data-new=\"\"" : "") + " data-kind=\"file\"" + (kind === "directory" ? " data-dir=\"1\"" : "") + " data-path=\"" + escapeHtml(path) + "\" contenteditable=\"false\" draggable=\"true\"><span class=\"dsgc-chipglyph\" aria-hidden=\"true\">" + fileTypeIconMarkup(path, kind) + "</span>" + escapeHtml(basename) + "</span>";
2421
3132
  }
2422
3133
  /**
2423
3134
  * 光标前的活跃 @token(角色或文件弹层触发判定)。
@@ -2460,12 +3171,16 @@ window.__ModuleLoader__.load({
2460
3171
  }, [input, inputRef]);
2461
3172
  return { onMsgsScroll };
2462
3173
  }
2463
- function useComposerInput(inputRef, setInput, setMention, setMentionIdx) {
2464
- /** DOM 同步 input 状态(序列化) */
3174
+ function useComposerInput(inputRef, setInput, setMention, setMentionIdx, sessionId) {
3175
+ const sessionIdRef = (0, react.useRef)(sessionId);
3176
+ sessionIdRef.current = sessionId;
3177
+ /** 从 DOM 同步 input 状态(序列化),并写入当前会话草稿槽。 */
2465
3178
  const syncFromDOM = (0, react.useCallback)(() => {
2466
3179
  const el = inputRef.current;
2467
3180
  if (!el) return;
2468
- setInput(serializeInput(el));
3181
+ const text = serializeInput(el);
3182
+ setInput(text);
3183
+ writeComposerDraft(sessionIdRef.current, el.innerHTML, text);
2469
3184
  }, [inputRef, setInput]);
2470
3185
  return {
2471
3186
  syncFromDOM,
@@ -2493,98 +3208,89 @@ window.__ModuleLoader__.load({
2493
3208
  }, [])
2494
3209
  };
2495
3210
  }
3211
+ /**
3212
+ * 按会话恢复草稿:切会话时先把当前 HTML 写入旧槽,再灌入新槽;
3213
+ * 面板重挂载(主会话⇄群聊)时 editor 是新节点,从模块缓存灌回。
3214
+ */
3215
+ function useComposerDraft(sessionId, inputRef, setInput, setMention) {
3216
+ const prevIdRef = (0, react.useRef)(null);
3217
+ (0, react.useLayoutEffect)(() => {
3218
+ const el = inputRef.current;
3219
+ const prev = prevIdRef.current;
3220
+ const next = sessionId || null;
3221
+ if (el && prev && prev !== next) writeComposerDraft(prev, el.innerHTML, serializeInput(el));
3222
+ prevIdRef.current = next;
3223
+ if (!next) return;
3224
+ const draft = readComposerDraft(next);
3225
+ if (el) el.innerHTML = draft.html;
3226
+ setInput(draft.text);
3227
+ setMention(null);
3228
+ return () => {
3229
+ const node = inputRef.current;
3230
+ const id = prevIdRef.current;
3231
+ if (node && id) writeComposerDraft(id, node.innerHTML, serializeInput(node));
3232
+ };
3233
+ }, [
3234
+ sessionId,
3235
+ inputRef,
3236
+ setInput,
3237
+ setMention
3238
+ ]);
3239
+ }
2496
3240
  function useMentionChip(inputRef, setMention, setMentionIdx, syncFromDOM) {
2497
- return {
2498
- insertChip: (0, react.useCallback)((role) => {
2499
- const el = inputRef.current;
2500
- const sel = window.getSelection();
2501
- if (!el || !sel) return;
2502
- if (!sel.anchorNode || !el.contains(sel.anchorNode)) return;
2503
- el.focus({ preventScroll: true });
2504
- const node = sel.anchorNode;
2505
- if (node.nodeType === Node.TEXT_NODE) {
2506
- const text = node.nodeValue || "";
2507
- const off = sel.anchorOffset;
2508
- const before = text.slice(0, off);
2509
- const m = /(?:^|\s)@("?)([^"\s@]*)$/.exec(before);
2510
- if (m) {
2511
- const prefixLen = m[1] ? 2 : 1;
2512
- const queryLen = m[2].length;
2513
- const start = off - prefixLen - queryLen;
2514
- if (start >= 0) {
2515
- const range = document.createRange();
2516
- range.setStart(node, start);
2517
- range.setEnd(node, off);
2518
- sel.removeAllRanges();
2519
- sel.addRange(range);
2520
- execCommand("delete");
2521
- }
3241
+ /** 弹层候选 → 删掉光标前的 @词、插入带 data-new 标记的芯片 + 尾随空格。 */
3242
+ const insertChipHtml = (0, react.useCallback)((html) => {
3243
+ const el = inputRef.current;
3244
+ const sel = window.getSelection();
3245
+ if (!el || !sel) return;
3246
+ if (!sel.anchorNode || !el.contains(sel.anchorNode)) return;
3247
+ el.focus({ preventScroll: true });
3248
+ const node = sel.anchorNode;
3249
+ if (node.nodeType === Node.TEXT_NODE) {
3250
+ const text = node.nodeValue || "";
3251
+ const off = sel.anchorOffset;
3252
+ const before = text.slice(0, off);
3253
+ const m = /(?:^|\s)@("?)([^"\s@]*)$/.exec(before);
3254
+ if (m) {
3255
+ const prefixLen = m[1] ? 2 : 1;
3256
+ const queryLen = m[2].length;
3257
+ const start = off - prefixLen - queryLen;
3258
+ if (start >= 0) {
3259
+ const range = document.createRange();
3260
+ range.setStart(node, start);
3261
+ range.setEnd(node, off);
3262
+ sel.removeAllRanges();
3263
+ sel.addRange(range);
3264
+ execCommand("delete");
2522
3265
  }
2523
3266
  }
2524
- execCommand("insertHTML", chipHtml(role).replace("class=\"dsgc-chipin\"", "class=\"dsgc-chipin\" data-new=\"\""));
2525
- const chip = el.querySelector(".dsgc-chipin[data-new]");
2526
- if (!chip) return;
2527
- const after = document.createRange();
2528
- after.setStartAfter(chip);
2529
- after.collapse(true);
2530
- sel.removeAllRanges();
2531
- sel.addRange(after);
2532
- execCommand("insertText", " ");
2533
- chip.removeAttribute("data-new");
2534
- setMention(null);
2535
- setMentionIdx(0);
2536
- syncFromDOM();
2537
- }, [
2538
- inputRef,
2539
- setMention,
2540
- setMentionIdx,
2541
- syncFromDOM
2542
- ]),
3267
+ }
3268
+ execCommand("insertHTML", html);
3269
+ const chip = el.querySelector(".dsgc-chipin[data-new]");
3270
+ if (!chip) return;
3271
+ const after = document.createRange();
3272
+ after.setStartAfter(chip);
3273
+ after.collapse(true);
3274
+ sel.removeAllRanges();
3275
+ sel.addRange(after);
3276
+ execCommand("insertText", " ");
3277
+ chip.removeAttribute("data-new");
3278
+ setMention(null);
3279
+ setMentionIdx(0);
3280
+ syncFromDOM();
3281
+ }, [
3282
+ inputRef,
3283
+ setMention,
3284
+ setMentionIdx,
3285
+ syncFromDOM
3286
+ ]);
3287
+ return {
3288
+ insertChip: (0, react.useCallback)((role) => {
3289
+ insertChipHtml(chipHtml(role, true));
3290
+ }, [insertChipHtml]),
2543
3291
  insertFileChip: (0, react.useCallback)((path, kind) => {
2544
- const el = inputRef.current;
2545
- const sel = window.getSelection();
2546
- if (!el || !sel) return;
2547
- if (!sel.anchorNode || !el.contains(sel.anchorNode)) return;
2548
- el.focus({ preventScroll: true });
2549
- const node = sel.anchorNode;
2550
- if (node.nodeType === Node.TEXT_NODE) {
2551
- const text = node.nodeValue || "";
2552
- const off = sel.anchorOffset;
2553
- const before = text.slice(0, off);
2554
- const m = /(?:^|\s)@("?)([^"\s@]*)$/.exec(before);
2555
- if (m) {
2556
- const prefixLen = m[1] ? 2 : 1;
2557
- const queryLen = m[2].length;
2558
- const start = off - prefixLen - queryLen;
2559
- if (start >= 0) {
2560
- const range = document.createRange();
2561
- range.setStart(node, start);
2562
- range.setEnd(node, off);
2563
- sel.removeAllRanges();
2564
- sel.addRange(range);
2565
- execCommand("delete");
2566
- }
2567
- }
2568
- }
2569
- execCommand("insertHTML", fileChipHtml(path, kind).replace("class=\"dsgc-chipin dsgc-chipin-file\"", "class=\"dsgc-chipin dsgc-chipin-file\" data-new=\"\""));
2570
- const chip = el.querySelector(".dsgc-chipin[data-new]");
2571
- if (!chip) return;
2572
- const after = document.createRange();
2573
- after.setStartAfter(chip);
2574
- after.collapse(true);
2575
- sel.removeAllRanges();
2576
- sel.addRange(after);
2577
- execCommand("insertText", " ");
2578
- chip.removeAttribute("data-new");
2579
- setMention(null);
2580
- setMentionIdx(0);
2581
- syncFromDOM();
2582
- }, [
2583
- inputRef,
2584
- setMention,
2585
- setMentionIdx,
2586
- syncFromDOM
2587
- ])
3292
+ insertChipHtml(fileChipHtml(path, kind, true));
3293
+ }, [insertChipHtml])
2588
3294
  };
2589
3295
  }
2590
3296
  function useInputKeyboard(mention, mentionCandidates, mentionIdxC, setMentionIdx, insertChip, insertFileChip, fileCandidates, setMention, sendMsg) {
@@ -2799,14 +3505,15 @@ window.__ModuleLoader__.load({
2799
3505
  * @module dsh-group-chat/client/panel
2800
3506
  */
2801
3507
  function GroupChatPanel() {
2802
- const { snap, gid, setGid, sid, setSid, search, setSearch, collapsedGroups, setCollapsedGroups, renameDraft, setRenameDraft, confirmDel, setConfirmDel, confirmClear, setConfirmClear, roleDraft, setRoleDraft, roleFormError, setRoleFormError, models, setModels, modelsError, setModelsError, fileBrowser, setFileBrowser, wsDraft, setWsDraft, partsSel, setPartsSel, rounds, setRounds, input, setInput, err, setErr, topicDraft, setTopicDraft, mention, setMention, mentionIdx, setMentionIdx, asideOpen, setAsideOpen, navOpen, setNavOpen, atBottom, setAtBottom, inputRef, sendingRef, scrollRef, action, mutate } = useGroupChatState();
3508
+ const { snap, gid, setGid, sid, setSid, search, setSearch, collapsedGroups, setCollapsedGroups, renameDraft, setRenameDraft, confirmDel, setConfirmDel, confirmClear, setConfirmClear, roleDraft, setRoleDraft, models, setModels, modelsError, setModelsError, partsSel, setPartsSel, rounds, setRounds, input, setInput, err, setErr, topicDraft, setTopicDraft, mention, setMention, mentionIdx, setMentionIdx, asideOpen, setAsideOpen, navOpen, setNavOpen, atBottom, setAtBottom, inputRef, sendingRef, scrollRef, action, mutate } = useGroupChatState();
2803
3509
  const [toast, setToast] = (0, react.useState)(null);
2804
3510
  const { onMsgsScroll } = useComposerEffects(inputRef, scrollRef, input, atBottom, snap);
2805
- const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx);
2806
3511
  let group = snap && gid ? groupById(snap, gid) : null;
2807
3512
  if (!group && snap && snap.groups.length) group = snap.groups[0];
2808
3513
  let sess = snap && sid ? sessById(snap, sid) : null;
2809
3514
  if ((!sess || sess.groupId !== group?.id) && group && snap) sess = group.sessionIds.length ? sessById(snap, group.sessionIds[group.sessionIds.length - 1]) : null;
3515
+ const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx, sess ? sess.id : null);
3516
+ useComposerDraft(sess ? sess.id : null, inputRef, setInput, setMention);
2810
3517
  const enabledRoles = group ? group.roleIds.map((id) => roleById(snap, id)).filter((r) => !!r && r.enabled) : [];
2811
3518
  const participants = partsSel || enabledRoles.map((r) => r.id);
2812
3519
  const busyNow = !!(sess && snap && snap.run.running && snap.run.sessionId === sess.id);
@@ -2823,11 +3530,11 @@ window.__ModuleLoader__.load({
2823
3530
  if (busyNow || !sess || sendingRef.current) return;
2824
3531
  sendingRef.current = true;
2825
3532
  try {
2826
- const parts = mentionedRoles.length ? mentionedRoles.map((r) => r.id) : participants;
2827
- if (!parts.length) {
2828
- setErr(mentionedRoles.length ? "" : "请至少选择一个参与角色(或在消息中 @成员)");
2829
- if (!mentionedRoles.length) return;
3533
+ if (!participants.length && !mentionedRoles.length) {
3534
+ setErr("请至少选择一个参与角色(或在消息中 @成员)");
3535
+ return;
2830
3536
  }
3537
+ const parts = mentionedRoles.length ? mentionedRoles.map((r) => r.id) : participants;
2831
3538
  const res = await action({
2832
3539
  kind: "send",
2833
3540
  sessionId: sess.id,
@@ -2843,6 +3550,7 @@ window.__ModuleLoader__.load({
2843
3550
  setMention(null);
2844
3551
  setErr("");
2845
3552
  setAtBottom(true);
3553
+ clearComposerDraft(sess.id);
2846
3554
  syncFromDOM();
2847
3555
  }
2848
3556
  } finally {
@@ -2885,6 +3593,25 @@ window.__ModuleLoader__.load({
2885
3593
  const has = participants.includes(rid);
2886
3594
  setPartsSel(has ? participants.filter((x) => x !== rid) : participants.concat([rid]));
2887
3595
  };
3596
+ const retrySpeak = async (messageId) => {
3597
+ if (!sess) return;
3598
+ if (busyNow || snap && snap.run.running) {
3599
+ setToast({
3600
+ text: "已有对话进行中,请先停止",
3601
+ seq: Date.now()
3602
+ });
3603
+ return;
3604
+ }
3605
+ const res = await action({
3606
+ kind: "retrySpeak",
3607
+ sessionId: sess.id,
3608
+ messageId
3609
+ });
3610
+ if (res && !res.ok && res.error) setToast({
3611
+ text: res.error,
3612
+ seq: Date.now()
3613
+ });
3614
+ };
2888
3615
  const doClear = async () => {
2889
3616
  if (!sess) return;
2890
3617
  if (busyNow) {
@@ -2922,80 +3649,15 @@ window.__ModuleLoader__.load({
2922
3649
  };
2923
3650
  const openRoleEditor = async (role) => {
2924
3651
  setErr("");
2925
- setRoleFormError("");
2926
3652
  if (!models || modelsError) await fetchModels();
2927
3653
  setRoleDraft(role ? draftFromRole(role) : blankDraft());
2928
3654
  };
2929
- const saveRole = async () => {
2930
- if (!roleDraft) return;
2931
- if (!roleDraft.name.trim()) {
2932
- setRoleFormError("角色名称不能为空");
2933
- return;
2934
- }
2935
- if (!roleDraft.provider || !roleDraft.model) {
2936
- setRoleFormError("请选择角色绑定的模型");
2937
- return;
2938
- }
2939
- const res = await mutate({
2940
- op: "upsertRole",
2941
- groupId: group.id,
2942
- role: roleDraft
2943
- });
2944
- if (res && res.ok && res.snapshot && !res.snapshot.error) {
2945
- setRoleDraft(null);
2946
- setRoleFormError("");
2947
- }
2948
- };
2949
3655
  const stopRun = async () => {
2950
3656
  await action({
2951
3657
  kind: "stop",
2952
3658
  sessionId: sess.id
2953
3659
  });
2954
3660
  };
2955
- const openBrowser = async (path) => {
2956
- setFileBrowser({
2957
- open: true,
2958
- loading: true,
2959
- list: null,
2960
- error: ""
2961
- });
2962
- try {
2963
- const res = await api.action({
2964
- kind: "browse",
2965
- path: path || ""
2966
- });
2967
- if (res && res.ok) setFileBrowser({
2968
- open: true,
2969
- loading: false,
2970
- list: res,
2971
- error: ""
2972
- });
2973
- else setFileBrowser({
2974
- open: true,
2975
- loading: false,
2976
- list: null,
2977
- error: res && res.error || "浏览失败"
2978
- });
2979
- } catch (e) {
2980
- setFileBrowser({
2981
- open: true,
2982
- loading: false,
2983
- list: null,
2984
- error: String(e && e.message || e)
2985
- });
2986
- }
2987
- };
2988
- const selectCurrentDir = () => {
2989
- if (!fileBrowser || !fileBrowser.list || !group) return;
2990
- const path = fileBrowser.list.path;
2991
- mutate({
2992
- op: "setWorkspaceDir",
2993
- groupId: group.id,
2994
- path
2995
- });
2996
- setWsDraft(null);
2997
- setFileBrowser(null);
2998
- };
2999
3661
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3000
3662
  className: "dsgc-root",
3001
3663
  children: [
@@ -3063,20 +3725,17 @@ window.__ModuleLoader__.load({
3063
3725
  stopRun,
3064
3726
  action,
3065
3727
  mutate,
3066
- setMention
3728
+ setMention,
3729
+ onRetrySpeak: (messageId) => {
3730
+ retrySpeak(messageId);
3731
+ }
3067
3732
  }),
3068
3733
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsidePanel, {
3069
3734
  snap,
3070
3735
  group,
3071
3736
  asideOpen,
3072
- wsDraft,
3073
- setWsDraft,
3074
- fileBrowser,
3075
- setFileBrowser,
3076
3737
  mutate,
3077
- openRoleEditor,
3078
- openBrowser,
3079
- selectCurrentDir
3738
+ openRoleEditor
3080
3739
  }),
3081
3740
  sess && confirmClear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3082
3741
  open: true,
@@ -3105,7 +3764,7 @@ window.__ModuleLoader__.load({
3105
3764
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
3106
3765
  className: "dsgc-clearnotes",
3107
3766
  children: [
3108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "删除内容:本会话的用户消息与角色发言(含思考、工具调用记录),确认后立即落盘" }),
3767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "删除内容:本会话的用户消息与角色发言(含思考、工具调用记录)以及本会话结论/约束备忘,确认后立即落盘" }),
3109
3768
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "不可恢复:此操作没有回收站,也没有撤销" }),
3110
3769
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "不受影响:会话本身与主题、群成员角色、工作区目录、权限档位" })
3111
3770
  ]
@@ -3122,19 +3781,16 @@ window.__ModuleLoader__.load({
3122
3781
  roleDraft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleDrawer, {
3123
3782
  draft: roleDraft,
3124
3783
  set: setRoleDraft,
3784
+ groupId: group.id,
3125
3785
  models,
3126
3786
  modelsError,
3127
3787
  onRetryModels: () => {
3128
3788
  fetchModels();
3129
3789
  },
3130
- onSave: () => {
3131
- saveRole();
3132
- },
3790
+ mutate,
3133
3791
  onCancel: () => {
3134
3792
  setRoleDraft(null);
3135
- setRoleFormError("");
3136
- },
3137
- formError: roleFormError
3793
+ }
3138
3794
  }) : null
3139
3795
  ]
3140
3796
  });