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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +3 -2
  2. package/lib/client.js +984 -296
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +524 -52
  5. package/lib/types/client/components/Bubble.d.ts +6 -3
  6. package/lib/types/client/components/ChatPanel.d.ts +1 -0
  7. package/lib/types/client/components/ConstraintList.d.ts +11 -0
  8. package/lib/types/client/components/FailCard.d.ts +9 -0
  9. package/lib/types/client/components/Fold.d.ts +19 -0
  10. package/lib/types/client/components/HoverTip.d.ts +22 -0
  11. package/lib/types/client/components/MessageFlow.d.ts +1 -0
  12. package/lib/types/client/components/MsgActions.d.ts +14 -0
  13. package/lib/types/client/hooks/useComposer.d.ts +6 -1
  14. package/lib/types/client/lib/composer-draft.d.ts +17 -0
  15. package/lib/types/core/constraints.d.ts +61 -0
  16. package/lib/types/core/errors.d.ts +54 -0
  17. package/lib/types/core/types.d.ts +22 -0
  18. package/lib/types/host/api/actions.d.ts +1 -1
  19. package/lib/types/host/engine/conversation.d.ts +5 -3
  20. package/lib/types/host/engine/fold.d.ts +17 -0
  21. package/lib/types/host/engine/index.d.ts +1 -0
  22. package/lib/types/host/engine/retitle.d.ts +5 -5
  23. package/lib/types/host/service.d.ts +1 -1
  24. package/lib/types/host/state.d.ts +2 -0
  25. package/lib/types/index.d.ts +2 -0
  26. package/package.json +1 -1
  27. package/src/client/GroupChatPanel.tsx +18 -3
  28. package/src/client/components/AsidePanel.tsx +10 -8
  29. package/src/client/components/Bubble.tsx +45 -18
  30. package/src/client/components/ChatPanel.tsx +17 -12
  31. package/src/client/components/Composer.tsx +26 -20
  32. package/src/client/components/ConstraintList.tsx +69 -0
  33. package/src/client/components/FailCard.tsx +49 -0
  34. package/src/client/components/Fold.tsx +72 -0
  35. package/src/client/components/HoverTip.tsx +134 -0
  36. package/src/client/components/MessageFlow.tsx +77 -54
  37. package/src/client/components/MsgActions.tsx +85 -0
  38. package/src/client/components/NavPanel.tsx +6 -1
  39. package/src/client/components/ThinkRow.tsx +7 -3
  40. package/src/client/components/ToolRow.tsx +7 -3
  41. package/src/client/hooks/useComposer.ts +40 -3
  42. package/src/client/hooks/useGroupChatState.ts +5 -4
  43. package/src/client/lib/composer-draft.ts +47 -0
  44. package/src/client/lib/styles.ts +73 -12
  45. package/src/client/react-dom-shim.d.ts +2 -0
  46. package/src/core/constraints.ts +210 -0
  47. package/src/core/errors.ts +184 -0
  48. package/src/core/json.ts +1 -0
  49. package/src/core/types.ts +28 -3
  50. package/src/host/api/actions.ts +35 -1
  51. package/src/host/broadcast.ts +3 -3
  52. package/src/host/engine/conversation.ts +103 -38
  53. package/src/host/engine/fold.ts +114 -0
  54. package/src/host/engine/index.ts +1 -0
  55. package/src/host/engine/retitle.ts +16 -11
  56. package/src/host/persistence/persistence.ts +18 -1
  57. package/src/host/service.ts +1 -1
  58. package/src/host/state.ts +5 -4
  59. package/src/index.ts +2 -0
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)}",
@@ -696,6 +752,119 @@ window.__ModuleLoader__.load({
696
752
  });
697
753
  }
698
754
  //#endregion
755
+ //#region src/client/components/HoverTip.tsx
756
+ /**
757
+ * 对齐宿主 Tooltip 的 hover 气泡:portal 到 document.body,躲开
758
+ * `.dsgc-root` 的 container-type 把 position:fixed 按容器定位。
759
+ * @module dsh-group-chat/client/components
760
+ */
761
+ /**
762
+ * @param props.label 气泡正文(pre-line)
763
+ * @param props.side 默认 right(对齐侧栏会话行);composer 轮数用 top
764
+ * @param props.delayMs hover 延迟,默认 500;键盘 focus 立即出
765
+ */
766
+ function HoverTip(props) {
767
+ const { label, side = "right", delayMs = 500, maxWidth, className, children } = props;
768
+ const anchorRef = (0, react.useRef)(null);
769
+ const bubbleRef = (0, react.useRef)(null);
770
+ const showTimer = (0, react.useRef)(null);
771
+ const hover = (0, react.useRef)(false);
772
+ const [pos, setPos] = (0, react.useState)(null);
773
+ const [placement, setPlacement] = (0, react.useState)(side);
774
+ const cancel = () => {
775
+ if (showTimer.current === null) return;
776
+ clearTimeout(showTimer.current);
777
+ showTimer.current = null;
778
+ };
779
+ const show = () => {
780
+ const el = anchorRef.current;
781
+ if (!el || !label) return;
782
+ const r = el.getBoundingClientRect();
783
+ setPlacement(side);
784
+ setPos({
785
+ x: side === "right" ? r.right + 10 : r.left + r.width / 2,
786
+ top: r.top,
787
+ bottom: r.bottom
788
+ });
789
+ };
790
+ const hideIfIdle = () => {
791
+ if (!hover.current && !(anchorRef.current && anchorRef.current.contains(document.activeElement))) setPos(null);
792
+ };
793
+ (0, react.useEffect)(() => cancel, []);
794
+ (0, react.useLayoutEffect)(() => {
795
+ if (!pos) return;
796
+ const fit = () => {
797
+ const el = bubbleRef.current;
798
+ if (!el) return;
799
+ el.style.left = pos.x + "px";
800
+ const r = el.getBoundingClientRect();
801
+ let dx = 0;
802
+ if (r.right > window.innerWidth - 12) dx = window.innerWidth - 12 - r.right;
803
+ if (r.left + dx < 12) dx = 12 - r.left;
804
+ el.style.left = pos.x + dx + "px";
805
+ if (side === "right") return;
806
+ const fitsBelow = pos.bottom + 8 + r.height <= window.innerHeight - 12;
807
+ const fitsAbove = pos.top - 8 - r.height >= 12;
808
+ if (placement === "bottom" && !fitsBelow && fitsAbove) setPlacement("top");
809
+ if (placement === "top" && !fitsAbove && fitsBelow) setPlacement("bottom");
810
+ };
811
+ fit();
812
+ window.addEventListener("resize", fit);
813
+ return () => {
814
+ window.removeEventListener("resize", fit);
815
+ };
816
+ }, [
817
+ pos,
818
+ placement,
819
+ label,
820
+ side
821
+ ]);
822
+ const y = pos === null ? 0 : placement === "right" ? pos.top + (pos.bottom - pos.top) / 2 : placement === "top" ? pos.top - 8 : pos.bottom + 8;
823
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
824
+ ref: anchorRef,
825
+ className,
826
+ onMouseEnter: () => {
827
+ hover.current = true;
828
+ cancel();
829
+ if (delayMs <= 0) {
830
+ show();
831
+ return;
832
+ }
833
+ showTimer.current = setTimeout(() => {
834
+ showTimer.current = null;
835
+ show();
836
+ }, delayMs);
837
+ },
838
+ onMouseLeave: () => {
839
+ hover.current = false;
840
+ cancel();
841
+ hideIfIdle();
842
+ },
843
+ onFocus: () => {
844
+ cancel();
845
+ show();
846
+ },
847
+ onBlur: (e) => {
848
+ const next = e.relatedTarget;
849
+ if (next && anchorRef.current && anchorRef.current.contains(next)) return;
850
+ cancel();
851
+ hideIfIdle();
852
+ },
853
+ children
854
+ }), pos ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
855
+ ref: bubbleRef,
856
+ className: "dsgc-hovertip",
857
+ "data-side": placement,
858
+ role: "tooltip",
859
+ style: {
860
+ left: pos.x,
861
+ top: y,
862
+ ...maxWidth ? { maxWidth } : {}
863
+ },
864
+ children: label
865
+ }), document.body) : null] });
866
+ }
867
+ //#endregion
699
868
  //#region src/client/components/NavPanel.tsx
700
869
  function NavPanel(props) {
701
870
  const { snap, search, setSearch, collapsedGroups, setCollapsedGroups, gid, sid, setGid, setSid, setPartsSel, renameDraft, setRenameDraft, confirmDel, setConfirmDel, mutate, navOpen } = props;
@@ -824,7 +993,10 @@ window.__ModuleLoader__.load({
824
993
  onClick: (e) => {
825
994
  e.stopPropagation();
826
995
  }
827
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
996
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverTip, {
997
+ label: s.name,
998
+ side: "right",
999
+ delayMs: 500,
828
1000
  className: "dsgc-sess-name",
829
1001
  children: s.name
830
1002
  }),
@@ -1101,8 +1273,9 @@ window.__ModuleLoader__.load({
1101
1273
  }) : null,
1102
1274
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1103
1275
  className: "dsgc-rolemenu",
1104
- children: [
1105
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1276
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1277
+ className: "dsgc-rolemeta",
1278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1106
1279
  className: "dsgc-rolemodel",
1107
1280
  title: r.provider + " / " + r.model + (r.thinking ? " · 深度思考" + (r.reasoningEffort && r.reasoningEffort !== "default" ? "(" + r.reasoningEffort + ")" : "") : ""),
1108
1281
  children: [
@@ -1110,42 +1283,36 @@ window.__ModuleLoader__.load({
1110
1283
  " / ",
1111
1284
  r.model
1112
1285
  ]
1113
- }),
1114
- r.thinking ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1286
+ }), r.thinking ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1287
+ className: "dsgc-rolethink",
1115
1288
  title: "深度思考",
1116
- style: {
1117
- display: "inline-flex",
1118
- alignItems: "center",
1119
- color: "var(--dsw-alias-label-tertiary,inherit)"
1120
- },
1121
1289
  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
- ]
1290
+ }) : null]
1291
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1292
+ className: "dsgc-roleops",
1293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1294
+ className: "dsgc-opbtn",
1295
+ title: "编辑角色",
1296
+ "aria-label": "编辑角色",
1297
+ onClick: (e) => {
1298
+ e.stopPropagation();
1299
+ openRoleEditor(r);
1300
+ },
1301
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, 14)
1302
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1303
+ className: "dsgc-opbtn danger",
1304
+ title: "移除角色",
1305
+ "aria-label": "移除角色",
1306
+ onClick: (e) => {
1307
+ e.stopPropagation();
1308
+ mutate({
1309
+ op: "deleteRole",
1310
+ roleId: r.id
1311
+ });
1312
+ },
1313
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, 14)
1314
+ })]
1315
+ })]
1149
1316
  })
1150
1317
  ]
1151
1318
  }, r.id);
@@ -1288,6 +1455,204 @@ window.__ModuleLoader__.load({
1288
1455
  });
1289
1456
  }
1290
1457
  //#endregion
1458
+ //#region src/core/types.ts
1459
+ /** 全部合法档位(展示顺序)。 */
1460
+ const PERMISSION_TIERS = [
1461
+ "view_only",
1462
+ "workspace_write",
1463
+ "full_access"
1464
+ ];
1465
+ /** 档位安全化:合法字符串原样,其余 undefined。 */
1466
+ function asPermissionTier(value) {
1467
+ return typeof value === "string" && PERMISSION_TIERS.includes(value) ? value : void 0;
1468
+ }
1469
+ //#endregion
1470
+ //#region src/core/errors.ts
1471
+ const PREFIX = "模型输出异常终止: ";
1472
+ const LEGACY_ROLE = /^角色「([^」]+)」发言失败[::]\s*/;
1473
+ /** 剥旧系统胶囊与引擎包装前缀,保留供应商原文。 */
1474
+ function unwrapSpeakFailure(raw) {
1475
+ const text = String(raw || "").trim();
1476
+ const legacy = parseLegacyRoleFailure(text);
1477
+ const body = legacy ? legacy.rest : text;
1478
+ return body.startsWith(PREFIX) ? body.slice(10).trim() : body;
1479
+ }
1480
+ /** 旧系统胶囊文案:角色「名」发言失败:原文。对不上则 null。 */
1481
+ function parseLegacyRoleFailure(text) {
1482
+ const m = LEGACY_ROLE.exec(String(text || ""));
1483
+ if (!m) return null;
1484
+ return {
1485
+ roleName: m[1],
1486
+ rest: String(text).slice(m[0].length)
1487
+ };
1488
+ }
1489
+ /** 发言失败卡:error 标记,或旧系统胶囊文案。 */
1490
+ function isSpeakFailure(m) {
1491
+ return !!m.error || !!parseLegacyRoleFailure(m.text);
1492
+ }
1493
+ /** 失败回合对应角色:failedRoleId / speaker / 旧文案里的角色名(恰好一名才命中)。 */
1494
+ function resolveFailedRole(m, roles) {
1495
+ const byId = (id) => {
1496
+ if (!id) return null;
1497
+ for (const r of roles) if (r.id === id) return r;
1498
+ return null;
1499
+ };
1500
+ const direct = byId(m.failedRoleId) || (m.speaker !== "user" && m.speaker !== "system" ? byId(m.speaker) : null);
1501
+ if (direct) return direct;
1502
+ const parsed = parseLegacyRoleFailure(m.text);
1503
+ if (!parsed) return null;
1504
+ const hits = roles.filter((r) => r.name === parsed.roleName);
1505
+ return hits.length === 1 ? hits[0] : null;
1506
+ }
1507
+ function pickJsonBlob(text) {
1508
+ const start = text.indexOf("{");
1509
+ const end = text.lastIndexOf("}");
1510
+ if (start < 0 || end <= start) return null;
1511
+ try {
1512
+ const parsed = JSON.parse(text.slice(start, end + 1));
1513
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1514
+ } catch {
1515
+ return null;
1516
+ }
1517
+ }
1518
+ function httpStatus(text) {
1519
+ const m = text.match(/\b([1-5]\d{2})\b/);
1520
+ if (!m) return null;
1521
+ const n = Number(m[1]);
1522
+ return n >= 100 && n <= 599 ? n : null;
1523
+ }
1524
+ function resetHint(message) {
1525
+ const m = message.match(/reset at ([^.]+\S)/i) || message.match(/将在\s*([^\s。]+)\s*重置/);
1526
+ return m ? "将在 " + m[1] + " 重置" : void 0;
1527
+ }
1528
+ /**
1529
+ * 把供应商错误压成可扫描的标题 + 短因。
1530
+ * 未知形态回退为「发言失败」,原文仍可展开。
1531
+ */
1532
+ function classifySpeakFailure(raw) {
1533
+ const source = unwrapSpeakFailure(raw);
1534
+ const blob = pickJsonBlob(source);
1535
+ const code = blob && typeof blob.code === "string" ? blob.code : "";
1536
+ const type = blob && typeof blob.type === "string" ? blob.type : "";
1537
+ const message = blob && typeof blob.message === "string" ? blob.message : source;
1538
+ const status = httpStatus(source);
1539
+ const joined = (code + " " + type + " " + message + " " + source).toLowerCase();
1540
+ if (/quota|accountquotaexceeded|exceeded the .*quota|额度|配额/.test(joined) || code === "AccountQuotaExceeded") return {
1541
+ title: "额度已用尽",
1542
+ detail: resetHint(message),
1543
+ raw: source
1544
+ };
1545
+ if (status === 429 || /too.?many.?requests|rate.?limit|限流/.test(joined) || type === "TooManyRequests") return {
1546
+ title: "请求过于频繁",
1547
+ detail: "稍后再试,或降低并发",
1548
+ raw: source
1549
+ };
1550
+ if (status === 401 || status === 403 || /unauthorized|forbidden|invalid.?api.?key|鉴权|未授权/.test(joined)) return {
1551
+ title: "模型鉴权失败",
1552
+ detail: "检查该角色绑定的提供方密钥",
1553
+ raw: source
1554
+ };
1555
+ if (status === 404 || /model.?not.?found|unknown.?model|模型不存在/.test(joined)) return {
1556
+ title: "模型不可用",
1557
+ detail: "该角色绑定的模型可能已下线",
1558
+ raw: source
1559
+ };
1560
+ if (/timeout|timed out|etimedout|超时/.test(joined)) return {
1561
+ title: "模型响应超时",
1562
+ raw: source
1563
+ };
1564
+ if (/network|econnreset|econnrefused|enotfound|fetch failed|网络/.test(joined)) return {
1565
+ title: "网络异常",
1566
+ detail: "检查网络后重试",
1567
+ raw: source
1568
+ };
1569
+ if (status !== null && status >= 500) return {
1570
+ title: "模型服务暂时不可用",
1571
+ detail: "HTTP " + status,
1572
+ raw: source
1573
+ };
1574
+ if (status !== null) return {
1575
+ title: "发言失败",
1576
+ detail: "HTTP " + status,
1577
+ raw: source
1578
+ };
1579
+ return {
1580
+ title: "发言失败",
1581
+ raw: source
1582
+ };
1583
+ }
1584
+ /** 失败卡默认复制内容:标题 + 短因 + 原文。 */
1585
+ function formatSpeakFailureCopy(view) {
1586
+ const lines = [view.title];
1587
+ if (view.detail) lines.push(view.detail);
1588
+ if (view.raw && view.raw !== view.title) lines.push(view.raw);
1589
+ return lines.join("\n");
1590
+ }
1591
+ //#endregion
1592
+ //#region src/client/components/Fold.tsx
1593
+ /**
1594
+ * 会话内折叠:高度 0fr→1fr + 溢出滚动遮罩。思考 / 工具 / 失败原文 / 结论备忘共用。
1595
+ * @module dsh-group-chat/client/components
1596
+ */
1597
+ function Fold(props) {
1598
+ const { open, children, className } = props;
1599
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1600
+ className: "dsgc-fold" + (open ? " open" : "") + (className ? " " + className : ""),
1601
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1602
+ className: "dsgc-fold-inner",
1603
+ children
1604
+ })
1605
+ });
1606
+ }
1607
+ function ClipWell(props) {
1608
+ const { children, maxHeight, className, watch } = props;
1609
+ const [clip, setClip] = (0, react.useState)({
1610
+ up: false,
1611
+ down: false
1612
+ });
1613
+ const ref = (0, react.useRef)(null);
1614
+ (0, react.useLayoutEffect)(() => {
1615
+ const el = ref.current;
1616
+ if (!el) return;
1617
+ const measure = () => {
1618
+ if (el.clientHeight < 2) {
1619
+ setClip((cur) => cur.up || cur.down ? {
1620
+ up: false,
1621
+ down: false
1622
+ } : cur);
1623
+ return;
1624
+ }
1625
+ const max = el.scrollHeight - el.clientHeight;
1626
+ const up = el.scrollTop > 1;
1627
+ const down = max > 1 && el.scrollTop < max - 1;
1628
+ setClip((cur) => cur.up === up && cur.down === down ? cur : {
1629
+ up,
1630
+ down
1631
+ });
1632
+ };
1633
+ measure();
1634
+ const ro = new ResizeObserver(measure);
1635
+ ro.observe(el);
1636
+ for (const child of el.children) ro.observe(child);
1637
+ el.addEventListener("scroll", measure, { passive: true });
1638
+ el.addEventListener("transitionend", measure);
1639
+ return () => {
1640
+ ro.disconnect();
1641
+ el.removeEventListener("scroll", measure);
1642
+ el.removeEventListener("transitionend", measure);
1643
+ };
1644
+ }, [watch]);
1645
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1646
+ className: "dsgc-clip" + (clip.up ? " can-up" : "") + (clip.down ? " can-down" : ""),
1647
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1648
+ className: "dsgc-clip-scroll" + (className ? " " + className : ""),
1649
+ ref,
1650
+ style: { maxHeight },
1651
+ children
1652
+ })
1653
+ });
1654
+ }
1655
+ //#endregion
1291
1656
  //#region src/client/components/ThinkRow.tsx
1292
1657
  /**
1293
1658
  * 思考折叠行(对标宿主 ReasoningRow:折叠摘要 / 展开全文)。
@@ -1299,9 +1664,9 @@ window.__ModuleLoader__.load({
1299
1664
  const [expanded, setExpanded] = (0, react.useState)(false);
1300
1665
  const plain = (line) => line.replace(/^(\s*#{1,6}\s+|\s*[-*+]\s+|\s*>\s*)+/, "").replace(/[*`_~]/g, "").replace(/\s+/g, " ").trim();
1301
1666
  const summary = plain(running ? latestLine(text) : firstLine(text));
1302
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1303
1668
  className: "dsgc-think",
1304
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1305
1670
  rowClassName: "dsgc-thinkrow",
1306
1671
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
1307
1672
  title: "思考",
@@ -1314,12 +1679,18 @@ window.__ModuleLoader__.load({
1314
1679
  collapsedContent: text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1315
1680
  className: "dsgc-thinksummary",
1316
1681
  children: summary
1317
- }) : null,
1318
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1319
- className: "dsgc-thinkbody",
1320
- children: text
1682
+ }) : null
1683
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1684
+ open: expanded,
1685
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1686
+ maxHeight: 320,
1687
+ watch: text,
1688
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1689
+ className: "dsgc-thinkbody",
1690
+ children: text
1691
+ })
1321
1692
  })
1322
- })
1693
+ })]
1323
1694
  });
1324
1695
  }
1325
1696
  //#endregion
@@ -1347,9 +1718,9 @@ window.__ModuleLoader__.load({
1347
1718
  const statusText = c.status === "ok" ? "成功" : c.status === "denied" ? "用户拒绝" : "失败";
1348
1719
  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
1720
  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", {
1721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
1722
  className: "dsgc-tool",
1352
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1353
1724
  rowClassName: "dsgc-toolrow",
1354
1725
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconOf, { size: 14 }),
1355
1726
  title: c.tool + (brief ? " " + brief : ""),
@@ -1366,12 +1737,138 @@ window.__ModuleLoader__.load({
1366
1737
  marginRight: 4,
1367
1738
  color: c.status === "ok" ? "var(--dsw-alias-label-tertiary,inherit)" : "var(--dsw-alias-state-error-primary,#e5484d)"
1368
1739
  }), statusText + (dur ? " · " + dur : "")]
1369
- }),
1370
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1371
- className: "dsgc-toolbody",
1372
- children: c.output || "(无输出)"
1373
1740
  })
1374
- })
1741
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1742
+ open: expanded,
1743
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1744
+ maxHeight: 260,
1745
+ watch: c.output,
1746
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1747
+ className: "dsgc-toolbody",
1748
+ children: c.output || "(无输出)"
1749
+ })
1750
+ })
1751
+ })]
1752
+ });
1753
+ }
1754
+ //#endregion
1755
+ //#region src/client/components/FailCard.tsx
1756
+ /**
1757
+ * 角色发言失败卡:人话标题 + 可展开原文。操作条由 Bubble 放在气泡外下方。
1758
+ * @module dsh-group-chat/client/FailCard
1759
+ */
1760
+ function FailCard(props) {
1761
+ const view = classifySpeakFailure(props.raw);
1762
+ const [open, setOpen] = (0, react.useState)(false);
1763
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1764
+ className: "dsgc-fail",
1765
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1766
+ className: "dsgc-failhead",
1767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1768
+ className: "dsgc-failicon",
1769
+ "aria-hidden": "true",
1770
+ children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, 14)
1771
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1772
+ className: "dsgc-failcopy",
1773
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1774
+ className: "dsgc-failtitle",
1775
+ children: view.title
1776
+ }), view.detail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1777
+ className: "dsgc-faildetail",
1778
+ children: view.detail
1779
+ }) : null]
1780
+ })]
1781
+ }), view.raw ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1782
+ type: "button",
1783
+ className: "dsgc-failmore" + (open ? " open" : ""),
1784
+ "aria-expanded": open,
1785
+ onClick: () => {
1786
+ setOpen((v) => !v);
1787
+ },
1788
+ children: open ? "收起原始错误" : "查看原始错误"
1789
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
1790
+ open,
1791
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClipWell, {
1792
+ maxHeight: 220,
1793
+ watch: open,
1794
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1795
+ className: "dsgc-failraw",
1796
+ children: view.raw
1797
+ })
1798
+ })
1799
+ })] }) : null]
1800
+ });
1801
+ }
1802
+ //#endregion
1803
+ //#region src/client/components/MsgActions.tsx
1804
+ /**
1805
+ * 消息操作条:贴在气泡外下方。用户/角色消息悬停出「复制」;失败卡常驻「复制 / 重试」。
1806
+ * @module dsh-group-chat/client/MsgActions
1807
+ */
1808
+ async function writeClipboard(text) {
1809
+ try {
1810
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1811
+ await navigator.clipboard.writeText(text);
1812
+ return true;
1813
+ }
1814
+ } catch {}
1815
+ try {
1816
+ const el = document.createElement("textarea");
1817
+ el.value = text;
1818
+ el.setAttribute("readonly", "");
1819
+ el.style.position = "fixed";
1820
+ el.style.left = "-9999px";
1821
+ document.body.appendChild(el);
1822
+ el.select();
1823
+ const ok = document.execCommand("copy");
1824
+ document.body.removeChild(el);
1825
+ return ok;
1826
+ } catch {
1827
+ return false;
1828
+ }
1829
+ }
1830
+ function MsgActions(props) {
1831
+ const { copyText, onRetry, retryDisabled, retryTitle, always } = props;
1832
+ const [copied, setCopied] = (0, react.useState)(false);
1833
+ const copiedTimer = (0, react.useRef)(null);
1834
+ (0, react.useEffect)(() => () => {
1835
+ if (copiedTimer.current) clearTimeout(copiedTimer.current);
1836
+ }, []);
1837
+ const copy = async () => {
1838
+ if (!copyText) return;
1839
+ if (!await writeClipboard(copyText)) return;
1840
+ setCopied(true);
1841
+ if (copiedTimer.current) clearTimeout(copiedTimer.current);
1842
+ copiedTimer.current = setTimeout(() => {
1843
+ setCopied(false);
1844
+ }, 1400);
1845
+ };
1846
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1847
+ className: "dsgc-msgops" + (always ? " always" : ""),
1848
+ role: "group",
1849
+ "aria-label": "消息操作",
1850
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1851
+ type: "button",
1852
+ className: "dsgc-msgop" + (copied ? " done" : ""),
1853
+ title: copied ? "已复制" : "复制",
1854
+ "aria-label": copied ? "已复制" : "复制",
1855
+ onClick: (e) => {
1856
+ e.stopPropagation();
1857
+ copy();
1858
+ },
1859
+ 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 ? "已复制" : "复制" })]
1860
+ }), onRetry ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1861
+ type: "button",
1862
+ className: "dsgc-msgop retry",
1863
+ title: retryTitle || "重试",
1864
+ "aria-label": retryTitle || "重试",
1865
+ disabled: retryDisabled,
1866
+ onClick: (e) => {
1867
+ e.stopPropagation();
1868
+ if (!retryDisabled) onRetry();
1869
+ },
1870
+ children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, 14), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "重试" })]
1871
+ }) : null]
1375
1872
  });
1376
1873
  }
1377
1874
  //#endregion
@@ -1385,62 +1882,78 @@ window.__ModuleLoader__.load({
1385
1882
  * MarkdownText 重新解析打满主线程导致「卡死后一次性蹦出」。
1386
1883
  * @module dsh-group-chat/client/Bubble
1387
1884
  */
1388
- function BubbleInner({ m, role }) {
1885
+ function BubbleInner({ m, role, busy, onRetry }) {
1389
1886
  const isUser = m.speaker === "user";
1390
- const isSys = m.speaker === "system";
1391
- const name = isUser ? "" : isSys ? "系统" : role ? role.name : "成员";
1887
+ const isFail = isSpeakFailure(m);
1888
+ const isSys = m.speaker === "system" && !isFail && !role;
1889
+ const name = isUser ? "我" : role ? role.name : isSys ? "系统" : "成员";
1392
1890
  if (isSys) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1393
- className: "dsgc-sysmsg" + (m.error ? " err" : ""),
1891
+ className: "dsgc-sysmsg",
1394
1892
  children: m.text
1395
1893
  });
1894
+ const retryTitle = !role ? "失败角色已不存在,无法重试" : !role.enabled ? "该角色已停用,无法重试" : busy ? "已有对话进行中,请先停止" : "重试该角色发言";
1895
+ const copyText = isFail ? formatSpeakFailureCopy(classifySpeakFailure(m.text)) : m.text || "";
1396
1896
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1397
- className: "dsgc-msg" + (isUser ? " mine" : ""),
1897
+ className: "dsgc-msg" + (isUser ? " mine" : "") + (isFail ? " fail" : ""),
1398
1898
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1399
1899
  className: "dsgc-avatar" + (isUser ? " mine" : ""),
1400
1900
  style: isUser || !role ? void 0 : { border: "2px solid " + (role.color || "#888") },
1401
1901
  children: name.slice(0, 1)
1402
1902
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1403
1903
  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
- })]
1904
+ children: [
1905
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1906
+ className: "dsgc-msghead",
1907
+ children: [
1908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1909
+ className: "dsgc-msgname",
1910
+ children: name
1911
+ }),
1912
+ m.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1913
+ className: "dsgc-msgmodel",
1914
+ title: m.model,
1915
+ children: m.model
1916
+ }) : null,
1917
+ m.ts ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1918
+ className: "dsgc-msgtime",
1919
+ children: fmtTime(m.ts)
1920
+ }) : null
1921
+ ]
1922
+ }),
1923
+ isFail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailCard, { raw: m.text }) : isUser ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1924
+ className: "dsgc-msgtext",
1925
+ children: m.text
1926
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1927
+ className: "dsgc-msgtext",
1928
+ children: [
1929
+ m.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, { text: m.reasoning }) : null,
1930
+ (Array.isArray(m.toolCalls) ? m.toolCalls : []).map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolRow, { c }, "tc" + i)),
1931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1932
+ text: m.text || "(无内容)",
1933
+ labels: MD_LABELS
1934
+ })
1935
+ ]
1936
+ }),
1937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MsgActions, {
1938
+ copyText,
1939
+ always: isFail,
1940
+ onRetry: isFail && onRetry ? () => {
1941
+ onRetry(m.id);
1942
+ } : void 0,
1943
+ retryDisabled: busy || !role || !role.enabled,
1944
+ retryTitle
1945
+ })
1946
+ ]
1435
1947
  })]
1436
1948
  });
1437
1949
  }
1438
1950
  /** 渲染相关字段的值比较(消息不可变;角色仅名/色参与渲染)。 */
1439
1951
  function bubblePropsEqual(a, b) {
1952
+ if (a.busy !== b.busy || a.onRetry == null !== (b.onRetry == null)) return false;
1440
1953
  const x = a.m;
1441
1954
  const y = b.m;
1442
1955
  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;
1956
+ 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
1957
  const ta = Array.isArray(x.toolCalls) ? x.toolCalls : [];
1445
1958
  const tb = Array.isArray(y.toolCalls) ? y.toolCalls : [];
1446
1959
  if (ta.length !== tb.length) return false;
@@ -1452,151 +1965,223 @@ window.__ModuleLoader__.load({
1452
1965
  }
1453
1966
  const ra = a.role;
1454
1967
  const rb = b.role;
1455
- return (ra ? ra.name + "\0" + (ra.color || "") : "") === (rb ? rb.name + "\0" + (rb.color || "") : "");
1968
+ 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
1969
  }
1457
1970
  const Bubble = (0, react.memo)(BubbleInner, bubblePropsEqual);
1458
1971
  //#endregion
1972
+ //#region src/client/components/ConstraintList.tsx
1973
+ /**
1974
+ * 会话流折点处:只读结论备忘卡(超过 4 条默认露 3 条)。
1975
+ * @module dsh-group-chat/client/components
1976
+ */
1977
+ const KIND_LABEL = {
1978
+ decided: "已定",
1979
+ rejected: "否决",
1980
+ open: "未决"
1981
+ };
1982
+ function ConstraintRow(props) {
1983
+ const { item } = props;
1984
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1985
+ className: "dsgc-constraint",
1986
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1987
+ className: "dsgc-ckind " + item.kind,
1988
+ children: KIND_LABEL[item.kind]
1989
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1990
+ className: "dsgc-ctext",
1991
+ children: item.text
1992
+ })]
1993
+ });
1994
+ }
1995
+ function ConstraintList(props) {
1996
+ const { items } = props;
1997
+ const [open, setOpen] = (0, react.useState)(false);
1998
+ if (!items.length) return null;
1999
+ const overflow = items.length > 4;
2000
+ const head = overflow ? items.slice(0, 3) : items;
2001
+ const extra = overflow ? items.slice(3) : [];
2002
+ const rest = items.length - 3;
2003
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2004
+ className: "dsgc-constraints",
2005
+ "aria-labelledby": "dsgc-constraints-title",
2006
+ children: [
2007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2008
+ className: "dsgc-chead",
2009
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2010
+ id: "dsgc-constraints-title",
2011
+ className: "dsgc-ctitle",
2012
+ children: "结论备忘"
2013
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2014
+ className: "dsgc-cdesc",
2015
+ children: "窗口外消息折成的已定 / 否决 / 未决,供后续角色接着用。"
2016
+ })]
2017
+ }),
2018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ClipWell, {
2019
+ maxHeight: 160,
2020
+ className: "dsgc-clist",
2021
+ watch: open,
2022
+ children: [head.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintRow, { item: c }, i)), overflow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
2023
+ open,
2024
+ children: extra.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintRow, { item: c }, i + 3))
2025
+ }) : null]
2026
+ }),
2027
+ overflow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2028
+ type: "button",
2029
+ className: "dsgc-cmore",
2030
+ onClick: () => {
2031
+ setOpen(!open);
2032
+ },
2033
+ "aria-expanded": open,
2034
+ children: open ? "收起" : "还有 " + rest + " 条约束"
2035
+ }) : null
2036
+ ]
2037
+ });
2038
+ }
2039
+ //#endregion
1459
2040
  //#region src/client/components/MessageFlow.tsx
1460
2041
  function MessageFlow(props) {
1461
- const { snap, sess, busyNow, msgById, action } = props;
2042
+ const { snap, sess, busyNow, msgById, action, onRetrySpeak } = props;
1462
2043
  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)
2044
+ const replaceId = busyNow ? snap.run.replaceMessageId : null;
2045
+ const replaceMsg = replaceId ? msgById[replaceId] : null;
2046
+ const liveRoleId = snap.run.currentRoleId || replaceMsg && (replaceMsg.failedRoleId || replaceMsg.speaker) || null;
2047
+ const lr = busyNow && liveRoleId ? roleById(snap, liveRoleId) : null;
2048
+ const live = lr ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2049
+ className: "dsgc-msg live",
2050
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2051
+ className: "dsgc-avatar",
2052
+ style: {
2053
+ border: "2px solid " + (lr.color || "#888"),
2054
+ "--role-color": lr.color || "#888"
2055
+ },
2056
+ children: lr.name.slice(0, 1)
2057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2058
+ className: "dsgc-msgbody",
2059
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2060
+ className: "dsgc-msghead",
2061
+ children: [
2062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2063
+ className: "dsgc-msgname",
2064
+ children: lr.name
2065
+ }),
2066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2067
+ className: "dsgc-msgmodel",
2068
+ children: [
2069
+ lr.provider,
2070
+ " / ",
2071
+ lr.model
2072
+ ]
2073
+ }),
2074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2075
+ className: "dsgc-msgtime dsgc-typing",
2076
+ children: "深度求索..."
2077
+ })
2078
+ ]
1483
2079
  }), /* @__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
- })]
2080
+ className: "dsgc-msgtext live",
2081
+ children: [
2082
+ snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkRow, {
2083
+ text: snap.run.partialReasoning,
2084
+ running: true
2085
+ }) : null,
2086
+ snap.run.partial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2087
+ text: snap.run.partial,
2088
+ streaming: true,
2089
+ labels: MD_LABELS
2090
+ }) : null,
2091
+ !snap.run.partial && !snap.run.partialReasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2092
+ className: "dsgc-pending",
2093
+ children: [
2094
+ Icon(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, 14),
2095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "思考中" }),
2096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2097
+ className: "dsgc-pendingdots",
2098
+ "aria-hidden": "true",
2099
+ children: [
2100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
2101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {}),
2102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
2103
+ ]
2104
+ })
2105
+ ]
2106
+ }) : null
2107
+ ]
1535
2108
  })]
1536
- }, "__live"));
1537
- }
2109
+ })]
2110
+ }, "__live") : null;
1538
2111
  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"));
2112
+ const confirm = pc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
+ className: "dsgc-confirm",
2114
+ children: [
2115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2116
+ className: "dsgc-confirmtitle",
2117
+ children: [Icon(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, 14), (lr ? lr.name : "角色") + " 请求执行命令(工作区内)"]
2118
+ }),
2119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2120
+ className: "dsgc-confirmcmd",
2121
+ children: String(pc.args && pc.args.command || "")
2122
+ }),
2123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2124
+ className: "dsgc-hint",
2125
+ children: "允许后将在工作区目录执行;拒绝后角色将继续纯文本讨论"
2126
+ }),
2127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2128
+ className: "dsgc-confirmops",
2129
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2130
+ variant: "primary",
2131
+ size: "sm",
2132
+ onClick: () => {
2133
+ action({
2134
+ kind: "confirmCommand",
2135
+ toolCallId: pc.toolCallId,
2136
+ allow: true
2137
+ });
2138
+ },
2139
+ children: "允许"
2140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2141
+ variant: "outline",
2142
+ size: "sm",
2143
+ onClick: () => {
2144
+ action({
2145
+ kind: "confirmCommand",
2146
+ toolCallId: pc.toolCallId,
2147
+ allow: false
2148
+ });
2149
+ },
2150
+ children: "拒绝"
2151
+ })]
2152
+ })
2153
+ ]
2154
+ }, "__confirm") : null;
2155
+ let livePlaced = false;
2156
+ if (sess) {
2157
+ const ids = sess.messageIds;
2158
+ const memo = sess.constraints && sess.constraints.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConstraintList, { items: sess.constraints }, sess.id + "-constraints") : null;
2159
+ const foldAt = memo && ids.length > 40 ? ids.length - 40 : 0;
2160
+ if (memo && foldAt === 0) bubbles.push(memo);
2161
+ for (let i = 0; i < ids.length; i++) {
2162
+ if (memo && foldAt > 0 && i === foldAt) bubbles.push(memo);
2163
+ const m = msgById[ids[i]];
2164
+ if (!m) continue;
2165
+ const role = resolveFailedRole(m, snap.roles.filter((r) => r.groupId === sess.groupId)) || (m.speaker !== "user" && m.speaker !== "system" ? roleById(snap, m.speaker) : null);
2166
+ if (replaceId && m.id === replaceId) {
2167
+ if (live) bubbles.push(live);
2168
+ if (confirm) bubbles.push(confirm);
2169
+ livePlaced = true;
2170
+ continue;
2171
+ }
2172
+ bubbles.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bubble, {
2173
+ m,
2174
+ role,
2175
+ busy: !!snap.run.running,
2176
+ onRetry: onRetrySpeak
2177
+ }, m.id));
2178
+ }
1584
2179
  }
2180
+ if (live && !livePlaced) bubbles.push(live);
2181
+ if (confirm && !livePlaced) bubbles.push(confirm);
1585
2182
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: bubbles });
1586
2183
  }
1587
2184
  //#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
2185
  //#region src/client/components/PermissionSelect.tsx
1601
2186
  /**
1602
2187
  * 群组权限档位选择器(对齐主会话 composer 左下角的 /permission 选择器):
@@ -1790,6 +2375,8 @@ window.__ModuleLoader__.load({
1790
2375
  }
1791
2376
  //#endregion
1792
2377
  //#region src/client/components/Composer.tsx
2378
+ /** 轮数控件 hover:宿主 Tooltip 三行说明(pre-line)。 */
2379
+ const ROUNDS_HINT = "一轮 = 参与角色各说一次。\n要他们自己互相反驳、你不插话时再加轮。\n要边看边插话,就留 1,再点发送。";
1793
2380
  function Composer(props) {
1794
2381
  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
2382
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1819,26 +2406,29 @@ window.__ModuleLoader__.load({
1819
2406
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1820
2407
  className: "dsgc-partslabel",
1821
2408
  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: "还没有启用的角色,请在右侧添加"
2409
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2410
+ className: "dsgc-partlist",
2411
+ children: mentionedRoles.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2412
+ className: "dsgc-partslabel",
2413
+ children: [
2414
+ "已 @ ",
2415
+ mentionedRoles.map((r) => r.name).join("、"),
2416
+ "(本轮仅被点名成员发言)"
2417
+ ]
2418
+ }) : enabledRoles.length ? enabledRoles.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2419
+ type: "button",
2420
+ className: "dsgc-partchip" + (participants.includes(r.id) ? " on" : ""),
2421
+ onClick: () => togglePart(r.id),
2422
+ disabled: busyNow,
2423
+ title: busyNow ? "对话进行中,暂停调整" : "点击切换本轮是否参与",
2424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2425
+ className: "dsgc-chipdot",
2426
+ style: { background: r.color || "#888" }
2427
+ }), r.name]
2428
+ }, r.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2429
+ className: "dsgc-hint",
2430
+ children: "还没有启用的角色,请在右侧添加"
2431
+ })
1842
2432
  })]
1843
2433
  }),
1844
2434
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1970,9 +2560,12 @@ window.__ModuleLoader__.load({
1970
2560
  }
1971
2561
  }),
1972
2562
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1973
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverTip, {
2564
+ label: ROUNDS_HINT,
2565
+ side: "top",
2566
+ delayMs: 500,
2567
+ maxWidth: 280,
1974
2568
  className: "dsgc-rounds",
1975
- title: "自由讨论的轮数(1–10):一轮 = 全体参与角色按顺序各发言一次",
1976
2569
  children: [
1977
2570
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1978
2571
  type: "button",
@@ -1986,7 +2579,6 @@ window.__ModuleLoader__.load({
1986
2579
  }),
1987
2580
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1988
2581
  className: "dsgc-roundnum",
1989
- title: "轮数",
1990
2582
  children: rounds
1991
2583
  }),
1992
2584
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2029,7 +2621,7 @@ window.__ModuleLoader__.load({
2029
2621
  //#endregion
2030
2622
  //#region src/client/components/ChatPanel.tsx
2031
2623
  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;
2624
+ 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
2625
  const commitTopic = () => {
2034
2626
  if (topicDraft !== null && sess && topicDraft !== sess.topic) mutate({
2035
2627
  op: "setTopic",
@@ -2043,7 +2635,8 @@ window.__ModuleLoader__.load({
2043
2635
  sess,
2044
2636
  busyNow,
2045
2637
  msgById,
2046
- action
2638
+ action,
2639
+ onRetrySpeak
2047
2640
  });
2048
2641
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2049
2642
  className: "dsgc-chat",
@@ -2070,37 +2663,40 @@ window.__ModuleLoader__.load({
2070
2663
  },
2071
2664
  children: Icon(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, 16)
2072
2665
  }),
2073
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2074
2667
  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
- ]
2668
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2669
+ className: "dsgc-chathead-row",
2670
+ children: [
2671
+ sess ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2672
+ className: "dsgc-sess-title",
2673
+ title: "当前会话:" + sess.name,
2674
+ children: sess.name
2675
+ }) : null,
2676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2677
+ className: "dsgc-topic",
2678
+ value: topicDraft === null ? sess ? sess.topic : "" : topicDraft,
2679
+ placeholder: "设置本会话主题(可选)…",
2680
+ onChange: (e) => {
2681
+ setTopicDraft(e.target.value);
2682
+ },
2683
+ onBlur: commitTopic,
2684
+ onKeyDown: (e) => {
2685
+ if (e.key === "Enter") e.target.blur();
2686
+ }
2687
+ }),
2688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2689
+ variant: "ghost",
2690
+ size: "sm",
2691
+ className: "dsgc-clearbtn",
2692
+ title: "清空当前会话的消息记录",
2693
+ onClick: () => {
2694
+ if (sess) setConfirmClear(true);
2695
+ },
2696
+ children: "清空"
2697
+ })
2698
+ ]
2699
+ })
2104
2700
  }),
2105
2701
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2106
2702
  className: "dsgc-msgs",
@@ -2161,6 +2757,41 @@ window.__ModuleLoader__.load({
2161
2757
  });
2162
2758
  }
2163
2759
  //#endregion
2760
+ //#region src/client/lib/composer-draft.ts
2761
+ const LIMIT = 32;
2762
+ const drafts = /* @__PURE__ */ new Map();
2763
+ const empty = () => ({
2764
+ html: "",
2765
+ text: ""
2766
+ });
2767
+ /** 读指定会话草稿;命中则提到 LRU 最近端。 */
2768
+ function readComposerDraft(sessionId) {
2769
+ if (!sessionId) return empty();
2770
+ const hit = drafts.get(sessionId);
2771
+ if (!hit) return empty();
2772
+ drafts.delete(sessionId);
2773
+ drafts.set(sessionId, hit);
2774
+ return hit;
2775
+ }
2776
+ /** 写入;空草稿删槽。超出上限淘汰最旧槽。 */
2777
+ function writeComposerDraft(sessionId, html, text) {
2778
+ if (!sessionId) return;
2779
+ drafts.delete(sessionId);
2780
+ if (!html && !text) return;
2781
+ drafts.set(sessionId, {
2782
+ html,
2783
+ text
2784
+ });
2785
+ if (drafts.size > LIMIT) {
2786
+ const oldest = drafts.keys().next().value;
2787
+ if (oldest !== void 0) drafts.delete(oldest);
2788
+ }
2789
+ }
2790
+ /** 发送成功或明确丢弃时摘槽。 */
2791
+ function clearComposerDraft(sessionId) {
2792
+ if (sessionId) drafts.delete(sessionId);
2793
+ }
2794
+ //#endregion
2164
2795
  //#region src/client/hooks/useGroupChatState.ts
2165
2796
  /**
2166
2797
  * 群聊面板核心状态管理 hook
@@ -2168,8 +2799,8 @@ window.__ModuleLoader__.load({
2168
2799
  */
2169
2800
  /**
2170
2801
  * 面板切换缓存(模块级,跨挂载存活):main 为 keyed 槽,主会话⇄群聊切换会
2171
- * 整体卸载重挂面板组件;缓存最近快照与选中项让重挂载瞬时恢复上次视图,
2172
- * 挂载后的 state 拉取 / SSE 首帧再行校正(本机回路往返仅数毫秒)。
2802
+ * 整体卸载重挂面板组件;缓存最近快照、选中项与按会话分槽的未发送草稿,
2803
+ * 让重挂载瞬时恢复上次视图。挂载后的 state 拉取 / SSE 首帧再行校正。
2173
2804
  */
2174
2805
  let cachedSnap = null;
2175
2806
  let cachedGid = null;
@@ -2212,7 +2843,7 @@ window.__ModuleLoader__.load({
2212
2843
  const [fileBrowser, setFileBrowser] = (0, react.useState)(null);
2213
2844
  const [partsSel, setPartsSel] = (0, react.useState)(null);
2214
2845
  const [rounds, setRounds] = (0, react.useState)(1);
2215
- const [input, setInput] = (0, react.useState)("");
2846
+ const [input, setInput] = (0, react.useState)(() => readComposerDraft(cachedSid).text);
2216
2847
  const [mention, setMention] = (0, react.useState)(null);
2217
2848
  const [mentionIdx, setMentionIdx] = (0, react.useState)(0);
2218
2849
  const [err, setErr] = (0, react.useState)("");
@@ -2460,12 +3091,16 @@ window.__ModuleLoader__.load({
2460
3091
  }, [input, inputRef]);
2461
3092
  return { onMsgsScroll };
2462
3093
  }
2463
- function useComposerInput(inputRef, setInput, setMention, setMentionIdx) {
2464
- /** DOM 同步 input 状态(序列化) */
3094
+ function useComposerInput(inputRef, setInput, setMention, setMentionIdx, sessionId) {
3095
+ const sessionIdRef = (0, react.useRef)(sessionId);
3096
+ sessionIdRef.current = sessionId;
3097
+ /** 从 DOM 同步 input 状态(序列化),并写入当前会话草稿槽。 */
2465
3098
  const syncFromDOM = (0, react.useCallback)(() => {
2466
3099
  const el = inputRef.current;
2467
3100
  if (!el) return;
2468
- setInput(serializeInput(el));
3101
+ const text = serializeInput(el);
3102
+ setInput(text);
3103
+ writeComposerDraft(sessionIdRef.current, el.innerHTML, text);
2469
3104
  }, [inputRef, setInput]);
2470
3105
  return {
2471
3106
  syncFromDOM,
@@ -2493,6 +3128,35 @@ window.__ModuleLoader__.load({
2493
3128
  }, [])
2494
3129
  };
2495
3130
  }
3131
+ /**
3132
+ * 按会话恢复草稿:切会话时先把当前 HTML 写入旧槽,再灌入新槽;
3133
+ * 面板重挂载(主会话⇄群聊)时 editor 是新节点,从模块缓存灌回。
3134
+ */
3135
+ function useComposerDraft(sessionId, inputRef, setInput, setMention) {
3136
+ const prevIdRef = (0, react.useRef)(null);
3137
+ (0, react.useLayoutEffect)(() => {
3138
+ const el = inputRef.current;
3139
+ const prev = prevIdRef.current;
3140
+ const next = sessionId || null;
3141
+ if (el && prev && prev !== next) writeComposerDraft(prev, el.innerHTML, serializeInput(el));
3142
+ prevIdRef.current = next;
3143
+ if (!next) return;
3144
+ const draft = readComposerDraft(next);
3145
+ if (el) el.innerHTML = draft.html;
3146
+ setInput(draft.text);
3147
+ setMention(null);
3148
+ return () => {
3149
+ const node = inputRef.current;
3150
+ const id = prevIdRef.current;
3151
+ if (node && id) writeComposerDraft(id, node.innerHTML, serializeInput(node));
3152
+ };
3153
+ }, [
3154
+ sessionId,
3155
+ inputRef,
3156
+ setInput,
3157
+ setMention
3158
+ ]);
3159
+ }
2496
3160
  function useMentionChip(inputRef, setMention, setMentionIdx, syncFromDOM) {
2497
3161
  return {
2498
3162
  insertChip: (0, react.useCallback)((role) => {
@@ -2802,11 +3466,12 @@ window.__ModuleLoader__.load({
2802
3466
  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();
2803
3467
  const [toast, setToast] = (0, react.useState)(null);
2804
3468
  const { onMsgsScroll } = useComposerEffects(inputRef, scrollRef, input, atBottom, snap);
2805
- const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx);
2806
3469
  let group = snap && gid ? groupById(snap, gid) : null;
2807
3470
  if (!group && snap && snap.groups.length) group = snap.groups[0];
2808
3471
  let sess = snap && sid ? sessById(snap, sid) : null;
2809
3472
  if ((!sess || sess.groupId !== group?.id) && group && snap) sess = group.sessionIds.length ? sessById(snap, group.sessionIds[group.sessionIds.length - 1]) : null;
3473
+ const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx, sess ? sess.id : null);
3474
+ useComposerDraft(sess ? sess.id : null, inputRef, setInput, setMention);
2810
3475
  const enabledRoles = group ? group.roleIds.map((id) => roleById(snap, id)).filter((r) => !!r && r.enabled) : [];
2811
3476
  const participants = partsSel || enabledRoles.map((r) => r.id);
2812
3477
  const busyNow = !!(sess && snap && snap.run.running && snap.run.sessionId === sess.id);
@@ -2843,6 +3508,7 @@ window.__ModuleLoader__.load({
2843
3508
  setMention(null);
2844
3509
  setErr("");
2845
3510
  setAtBottom(true);
3511
+ clearComposerDraft(sess.id);
2846
3512
  syncFromDOM();
2847
3513
  }
2848
3514
  } finally {
@@ -2885,6 +3551,25 @@ window.__ModuleLoader__.load({
2885
3551
  const has = participants.includes(rid);
2886
3552
  setPartsSel(has ? participants.filter((x) => x !== rid) : participants.concat([rid]));
2887
3553
  };
3554
+ const retrySpeak = async (messageId) => {
3555
+ if (!sess) return;
3556
+ if (busyNow || snap && snap.run.running) {
3557
+ setToast({
3558
+ text: "已有对话进行中,请先停止",
3559
+ seq: Date.now()
3560
+ });
3561
+ return;
3562
+ }
3563
+ const res = await action({
3564
+ kind: "retrySpeak",
3565
+ sessionId: sess.id,
3566
+ messageId
3567
+ });
3568
+ if (res && !res.ok && res.error) setToast({
3569
+ text: res.error,
3570
+ seq: Date.now()
3571
+ });
3572
+ };
2888
3573
  const doClear = async () => {
2889
3574
  if (!sess) return;
2890
3575
  if (busyNow) {
@@ -3063,7 +3748,10 @@ window.__ModuleLoader__.load({
3063
3748
  stopRun,
3064
3749
  action,
3065
3750
  mutate,
3066
- setMention
3751
+ setMention,
3752
+ onRetrySpeak: (messageId) => {
3753
+ retrySpeak(messageId);
3754
+ }
3067
3755
  }),
3068
3756
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsidePanel, {
3069
3757
  snap,
@@ -3105,7 +3793,7 @@ window.__ModuleLoader__.load({
3105
3793
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
3106
3794
  className: "dsgc-clearnotes",
3107
3795
  children: [
3108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "删除内容:本会话的用户消息与角色发言(含思考、工具调用记录),确认后立即落盘" }),
3796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "删除内容:本会话的用户消息与角色发言(含思考、工具调用记录)以及本会话结论/约束备忘,确认后立即落盘" }),
3109
3797
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "不可恢复:此操作没有回收站,也没有撤销" }),
3110
3798
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "不受影响:会话本身与主题、群成员角色、工作区目录、权限档位" })
3111
3799
  ]