@researai/deepscientist 1.5.15 → 1.5.17

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 (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -1,4104 +0,0 @@
1
- import { w as createLucideIcon, r as reactExports, aR as reactDomExports, aS as useReducedMotion, j as jsxRuntimeExports, aT as useQueryClient, o as useToast, u as useI18n, aA as useLabCopilotStore, aU as useLabGraphSelectionStore, aV as useChatSessionStore, aW as buildAvatarColorMap, aX as isLabWorkingStatus, aY as getLabAgentDirectSession, aZ as buildAgentDescriptor, a_ as resolveAgentLogo, aF as resolveAgentDisplayName, aG as resolveAgentMentionLabel, a$ as AnimatePresence, b0 as motion, aO as Button, b1 as pickAvatarFrameColor, aH as resolveQuestLabel, d as Check, b as cn, z as useAuthStore, b2 as useCopilotDockHeaderPortal, b3 as assignLabAgent, b4 as ScrollArea, L as LoaderCircle, b5 as likeLabMoment, b6 as unlikeLabMoment, b7 as commentLabMoment, b8 as Ellipsis, b9 as Plus, l as Search, ba as Dialog, bb as DialogContent, bc as DialogHeader, bd as DialogTitle, be as DialogFooter, bf as getLabGroupSession, bg as getLabFriendsSession } from './index-D_E4281X.js';
2
- import { P as Popover, a as PopoverTrigger, b as PopoverContent } from './popover-D3Gg_FoV.js';
3
- import { S as Select, a as SelectTrigger, b as SelectValue, c as SelectContent, d as SelectItem } from './select-CpAK6uWm.js';
4
- import { A as AiManusChatView, D as DEFAULT_AGENT_ID, C as ChatBox, a as applyChatEvent, u as useSSESession, b as ChatMessage, r as renderMarkdown } from './AiManusChatView-DDjbFnbt.js';
5
- import './index-BwRJaoTl.js';
6
- import './file-content-DT24KFma.js';
7
- import './file-jump-queue-r5XKgJEV.js';
8
- import './pdf-effect-queue-BJk5okWJ.js';
9
- import './file-diff-panel-DK13YPql.js';
10
- import './bot-0DYntytV.js';
11
- import './NotebookEditor-C1kWaxKi.js';
12
- import './trash-CXvwwSe8.js';
13
-
14
- /**
15
- * @license lucide-react v0.511.0 - ISC
16
- *
17
- * This source code is licensed under the ISC license.
18
- * See the LICENSE file in the root directory of this source tree.
19
- */
20
-
21
-
22
- const __iconNode$1 = [
23
- ["path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z", key: "vv11sd" }]
24
- ];
25
- const MessageCircle = createLucideIcon("message-circle", __iconNode$1);
26
-
27
- /**
28
- * @license lucide-react v0.511.0 - ISC
29
- *
30
- * This source code is licensed under the ISC license.
31
- * See the LICENSE file in the root directory of this source tree.
32
- */
33
-
34
-
35
- const __iconNode = [
36
- ["path", { d: "M7 10v12", key: "1qc93n" }],
37
- [
38
- "path",
39
- {
40
- d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z",
41
- key: "emmmcr"
42
- }
43
- ]
44
- ];
45
- const ThumbsUp = createLucideIcon("thumbs-up", __iconNode);
46
-
47
- function memo(getDeps, fn, opts) {
48
- let deps = opts.initialDeps ?? [];
49
- let result;
50
- let isInitial = true;
51
- function memoizedFunction() {
52
- var _a, _b, _c;
53
- let depTime;
54
- if (opts.key && ((_a = opts.debug) == null ? void 0 : _a.call(opts))) depTime = Date.now();
55
- const newDeps = getDeps();
56
- const depsChanged = newDeps.length !== deps.length || newDeps.some((dep, index) => deps[index] !== dep);
57
- if (!depsChanged) {
58
- return result;
59
- }
60
- deps = newDeps;
61
- let resultTime;
62
- if (opts.key && ((_b = opts.debug) == null ? void 0 : _b.call(opts))) resultTime = Date.now();
63
- result = fn(...newDeps);
64
- if (opts.key && ((_c = opts.debug) == null ? void 0 : _c.call(opts))) {
65
- const depEndTime = Math.round((Date.now() - depTime) * 100) / 100;
66
- const resultEndTime = Math.round((Date.now() - resultTime) * 100) / 100;
67
- const resultFpsPercentage = resultEndTime / 16;
68
- const pad = (str, num) => {
69
- str = String(str);
70
- while (str.length < num) {
71
- str = " " + str;
72
- }
73
- return str;
74
- };
75
- console.info(
76
- `%c⏱ ${pad(resultEndTime, 5)} /${pad(depEndTime, 5)} ms`,
77
- `
78
- font-size: .6rem;
79
- font-weight: bold;
80
- color: hsl(${Math.max(
81
- 0,
82
- Math.min(120 - 120 * resultFpsPercentage, 120)
83
- )}deg 100% 31%);`,
84
- opts == null ? void 0 : opts.key
85
- );
86
- }
87
- if ((opts == null ? void 0 : opts.onChange) && !(isInitial && opts.skipInitialOnChange)) {
88
- opts.onChange(result);
89
- }
90
- isInitial = false;
91
- return result;
92
- }
93
- memoizedFunction.updateDeps = (newDeps) => {
94
- deps = newDeps;
95
- };
96
- return memoizedFunction;
97
- }
98
- function notUndefined(value, msg) {
99
- if (value === void 0) {
100
- throw new Error(`Unexpected undefined${""}`);
101
- } else {
102
- return value;
103
- }
104
- }
105
- const approxEqual = (a, b) => Math.abs(a - b) < 1.01;
106
- const debounce = (targetWindow, fn, ms) => {
107
- let timeoutId;
108
- return function(...args) {
109
- targetWindow.clearTimeout(timeoutId);
110
- timeoutId = targetWindow.setTimeout(() => fn.apply(this, args), ms);
111
- };
112
- };
113
-
114
- const getRect = (element) => {
115
- const { offsetWidth, offsetHeight } = element;
116
- return { width: offsetWidth, height: offsetHeight };
117
- };
118
- const defaultKeyExtractor = (index) => index;
119
- const defaultRangeExtractor = (range) => {
120
- const start = Math.max(range.startIndex - range.overscan, 0);
121
- const end = Math.min(range.endIndex + range.overscan, range.count - 1);
122
- const arr = [];
123
- for (let i = start; i <= end; i++) {
124
- arr.push(i);
125
- }
126
- return arr;
127
- };
128
- const observeElementRect = (instance, cb) => {
129
- const element = instance.scrollElement;
130
- if (!element) {
131
- return;
132
- }
133
- const targetWindow = instance.targetWindow;
134
- if (!targetWindow) {
135
- return;
136
- }
137
- const handler = (rect) => {
138
- const { width, height } = rect;
139
- cb({ width: Math.round(width), height: Math.round(height) });
140
- };
141
- handler(getRect(element));
142
- if (!targetWindow.ResizeObserver) {
143
- return () => {
144
- };
145
- }
146
- const observer = new targetWindow.ResizeObserver((entries) => {
147
- const run = () => {
148
- const entry = entries[0];
149
- if (entry == null ? void 0 : entry.borderBoxSize) {
150
- const box = entry.borderBoxSize[0];
151
- if (box) {
152
- handler({ width: box.inlineSize, height: box.blockSize });
153
- return;
154
- }
155
- }
156
- handler(getRect(element));
157
- };
158
- instance.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
159
- });
160
- observer.observe(element, { box: "border-box" });
161
- return () => {
162
- observer.unobserve(element);
163
- };
164
- };
165
- const addEventListenerOptions = {
166
- passive: true
167
- };
168
- const supportsScrollend = typeof window == "undefined" ? true : "onscrollend" in window;
169
- const observeElementOffset = (instance, cb) => {
170
- const element = instance.scrollElement;
171
- if (!element) {
172
- return;
173
- }
174
- const targetWindow = instance.targetWindow;
175
- if (!targetWindow) {
176
- return;
177
- }
178
- let offset = 0;
179
- const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : debounce(
180
- targetWindow,
181
- () => {
182
- cb(offset, false);
183
- },
184
- instance.options.isScrollingResetDelay
185
- );
186
- const createHandler = (isScrolling) => () => {
187
- const { horizontal, isRtl } = instance.options;
188
- offset = horizontal ? element["scrollLeft"] * (isRtl && -1 || 1) : element["scrollTop"];
189
- fallback();
190
- cb(offset, isScrolling);
191
- };
192
- const handler = createHandler(true);
193
- const endHandler = createHandler(false);
194
- element.addEventListener("scroll", handler, addEventListenerOptions);
195
- const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend;
196
- if (registerScrollendEvent) {
197
- element.addEventListener("scrollend", endHandler, addEventListenerOptions);
198
- }
199
- return () => {
200
- element.removeEventListener("scroll", handler);
201
- if (registerScrollendEvent) {
202
- element.removeEventListener("scrollend", endHandler);
203
- }
204
- };
205
- };
206
- const measureElement = (element, entry, instance) => {
207
- if (entry == null ? void 0 : entry.borderBoxSize) {
208
- const box = entry.borderBoxSize[0];
209
- if (box) {
210
- const size = Math.round(
211
- box[instance.options.horizontal ? "inlineSize" : "blockSize"]
212
- );
213
- return size;
214
- }
215
- }
216
- return element[instance.options.horizontal ? "offsetWidth" : "offsetHeight"];
217
- };
218
- const elementScroll = (offset, {
219
- adjustments = 0,
220
- behavior
221
- }, instance) => {
222
- var _a, _b;
223
- const toOffset = offset + adjustments;
224
- (_b = (_a = instance.scrollElement) == null ? void 0 : _a.scrollTo) == null ? void 0 : _b.call(_a, {
225
- [instance.options.horizontal ? "left" : "top"]: toOffset,
226
- behavior
227
- });
228
- };
229
- class Virtualizer {
230
- constructor(opts) {
231
- this.unsubs = [];
232
- this.scrollElement = null;
233
- this.targetWindow = null;
234
- this.isScrolling = false;
235
- this.scrollState = null;
236
- this.measurementsCache = [];
237
- this.itemSizeCache = /* @__PURE__ */ new Map();
238
- this.laneAssignments = /* @__PURE__ */ new Map();
239
- this.pendingMeasuredCacheIndexes = [];
240
- this.prevLanes = void 0;
241
- this.lanesChangedFlag = false;
242
- this.lanesSettling = false;
243
- this.scrollRect = null;
244
- this.scrollOffset = null;
245
- this.scrollDirection = null;
246
- this.scrollAdjustments = 0;
247
- this.elementsCache = /* @__PURE__ */ new Map();
248
- this.now = () => {
249
- var _a, _b, _c;
250
- return ((_c = (_b = (_a = this.targetWindow) == null ? void 0 : _a.performance) == null ? void 0 : _b.now) == null ? void 0 : _c.call(_b)) ?? Date.now();
251
- };
252
- this.observer = /* @__PURE__ */ (() => {
253
- let _ro = null;
254
- const get = () => {
255
- if (_ro) {
256
- return _ro;
257
- }
258
- if (!this.targetWindow || !this.targetWindow.ResizeObserver) {
259
- return null;
260
- }
261
- return _ro = new this.targetWindow.ResizeObserver((entries) => {
262
- entries.forEach((entry) => {
263
- const run = () => {
264
- this._measureElement(entry.target, entry);
265
- };
266
- this.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
267
- });
268
- });
269
- };
270
- return {
271
- disconnect: () => {
272
- var _a;
273
- (_a = get()) == null ? void 0 : _a.disconnect();
274
- _ro = null;
275
- },
276
- observe: (target) => {
277
- var _a;
278
- return (_a = get()) == null ? void 0 : _a.observe(target, { box: "border-box" });
279
- },
280
- unobserve: (target) => {
281
- var _a;
282
- return (_a = get()) == null ? void 0 : _a.unobserve(target);
283
- }
284
- };
285
- })();
286
- this.range = null;
287
- this.setOptions = (opts2) => {
288
- Object.entries(opts2).forEach(([key, value]) => {
289
- if (typeof value === "undefined") delete opts2[key];
290
- });
291
- this.options = {
292
- debug: false,
293
- initialOffset: 0,
294
- overscan: 1,
295
- paddingStart: 0,
296
- paddingEnd: 0,
297
- scrollPaddingStart: 0,
298
- scrollPaddingEnd: 0,
299
- horizontal: false,
300
- getItemKey: defaultKeyExtractor,
301
- rangeExtractor: defaultRangeExtractor,
302
- onChange: () => {
303
- },
304
- measureElement,
305
- initialRect: { width: 0, height: 0 },
306
- scrollMargin: 0,
307
- gap: 0,
308
- indexAttribute: "data-index",
309
- initialMeasurementsCache: [],
310
- lanes: 1,
311
- isScrollingResetDelay: 150,
312
- enabled: true,
313
- isRtl: false,
314
- useScrollendEvent: false,
315
- useAnimationFrameWithResizeObserver: false,
316
- ...opts2
317
- };
318
- };
319
- this.notify = (sync) => {
320
- var _a, _b;
321
- (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, sync);
322
- };
323
- this.maybeNotify = memo(
324
- () => {
325
- this.calculateRange();
326
- return [
327
- this.isScrolling,
328
- this.range ? this.range.startIndex : null,
329
- this.range ? this.range.endIndex : null
330
- ];
331
- },
332
- (isScrolling) => {
333
- this.notify(isScrolling);
334
- },
335
- {
336
- key: false,
337
- debug: () => this.options.debug,
338
- initialDeps: [
339
- this.isScrolling,
340
- this.range ? this.range.startIndex : null,
341
- this.range ? this.range.endIndex : null
342
- ]
343
- }
344
- );
345
- this.cleanup = () => {
346
- this.unsubs.filter(Boolean).forEach((d) => d());
347
- this.unsubs = [];
348
- this.observer.disconnect();
349
- if (this.rafId != null && this.targetWindow) {
350
- this.targetWindow.cancelAnimationFrame(this.rafId);
351
- this.rafId = null;
352
- }
353
- this.scrollState = null;
354
- this.scrollElement = null;
355
- this.targetWindow = null;
356
- };
357
- this._didMount = () => {
358
- return () => {
359
- this.cleanup();
360
- };
361
- };
362
- this._willUpdate = () => {
363
- var _a;
364
- const scrollElement = this.options.enabled ? this.options.getScrollElement() : null;
365
- if (this.scrollElement !== scrollElement) {
366
- this.cleanup();
367
- if (!scrollElement) {
368
- this.maybeNotify();
369
- return;
370
- }
371
- this.scrollElement = scrollElement;
372
- if (this.scrollElement && "ownerDocument" in this.scrollElement) {
373
- this.targetWindow = this.scrollElement.ownerDocument.defaultView;
374
- } else {
375
- this.targetWindow = ((_a = this.scrollElement) == null ? void 0 : _a.window) ?? null;
376
- }
377
- this.elementsCache.forEach((cached) => {
378
- this.observer.observe(cached);
379
- });
380
- this.unsubs.push(
381
- this.options.observeElementRect(this, (rect) => {
382
- this.scrollRect = rect;
383
- this.maybeNotify();
384
- })
385
- );
386
- this.unsubs.push(
387
- this.options.observeElementOffset(this, (offset, isScrolling) => {
388
- this.scrollAdjustments = 0;
389
- this.scrollDirection = isScrolling ? this.getScrollOffset() < offset ? "forward" : "backward" : null;
390
- this.scrollOffset = offset;
391
- this.isScrolling = isScrolling;
392
- if (this.scrollState) {
393
- this.scheduleScrollReconcile();
394
- }
395
- this.maybeNotify();
396
- })
397
- );
398
- this._scrollToOffset(this.getScrollOffset(), {
399
- adjustments: void 0,
400
- behavior: void 0
401
- });
402
- }
403
- };
404
- this.rafId = null;
405
- this.getSize = () => {
406
- if (!this.options.enabled) {
407
- this.scrollRect = null;
408
- return 0;
409
- }
410
- this.scrollRect = this.scrollRect ?? this.options.initialRect;
411
- return this.scrollRect[this.options.horizontal ? "width" : "height"];
412
- };
413
- this.getScrollOffset = () => {
414
- if (!this.options.enabled) {
415
- this.scrollOffset = null;
416
- return 0;
417
- }
418
- this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
419
- return this.scrollOffset;
420
- };
421
- this.getFurthestMeasurement = (measurements, index) => {
422
- const furthestMeasurementsFound = /* @__PURE__ */ new Map();
423
- const furthestMeasurements = /* @__PURE__ */ new Map();
424
- for (let m = index - 1; m >= 0; m--) {
425
- const measurement = measurements[m];
426
- if (furthestMeasurementsFound.has(measurement.lane)) {
427
- continue;
428
- }
429
- const previousFurthestMeasurement = furthestMeasurements.get(
430
- measurement.lane
431
- );
432
- if (previousFurthestMeasurement == null || measurement.end > previousFurthestMeasurement.end) {
433
- furthestMeasurements.set(measurement.lane, measurement);
434
- } else if (measurement.end < previousFurthestMeasurement.end) {
435
- furthestMeasurementsFound.set(measurement.lane, true);
436
- }
437
- if (furthestMeasurementsFound.size === this.options.lanes) {
438
- break;
439
- }
440
- }
441
- return furthestMeasurements.size === this.options.lanes ? Array.from(furthestMeasurements.values()).sort((a, b) => {
442
- if (a.end === b.end) {
443
- return a.index - b.index;
444
- }
445
- return a.end - b.end;
446
- })[0] : void 0;
447
- };
448
- this.getMeasurementOptions = memo(
449
- () => [
450
- this.options.count,
451
- this.options.paddingStart,
452
- this.options.scrollMargin,
453
- this.options.getItemKey,
454
- this.options.enabled,
455
- this.options.lanes
456
- ],
457
- (count, paddingStart, scrollMargin, getItemKey, enabled, lanes) => {
458
- const lanesChanged = this.prevLanes !== void 0 && this.prevLanes !== lanes;
459
- if (lanesChanged) {
460
- this.lanesChangedFlag = true;
461
- }
462
- this.prevLanes = lanes;
463
- this.pendingMeasuredCacheIndexes = [];
464
- return {
465
- count,
466
- paddingStart,
467
- scrollMargin,
468
- getItemKey,
469
- enabled,
470
- lanes
471
- };
472
- },
473
- {
474
- key: false
475
- }
476
- );
477
- this.getMeasurements = memo(
478
- () => [this.getMeasurementOptions(), this.itemSizeCache],
479
- ({ count, paddingStart, scrollMargin, getItemKey, enabled, lanes }, itemSizeCache) => {
480
- if (!enabled) {
481
- this.measurementsCache = [];
482
- this.itemSizeCache.clear();
483
- this.laneAssignments.clear();
484
- return [];
485
- }
486
- if (this.laneAssignments.size > count) {
487
- for (const index of this.laneAssignments.keys()) {
488
- if (index >= count) {
489
- this.laneAssignments.delete(index);
490
- }
491
- }
492
- }
493
- if (this.lanesChangedFlag) {
494
- this.lanesChangedFlag = false;
495
- this.lanesSettling = true;
496
- this.measurementsCache = [];
497
- this.itemSizeCache.clear();
498
- this.laneAssignments.clear();
499
- this.pendingMeasuredCacheIndexes = [];
500
- }
501
- if (this.measurementsCache.length === 0 && !this.lanesSettling) {
502
- this.measurementsCache = this.options.initialMeasurementsCache;
503
- this.measurementsCache.forEach((item) => {
504
- this.itemSizeCache.set(item.key, item.size);
505
- });
506
- }
507
- const min = this.lanesSettling ? 0 : this.pendingMeasuredCacheIndexes.length > 0 ? Math.min(...this.pendingMeasuredCacheIndexes) : 0;
508
- this.pendingMeasuredCacheIndexes = [];
509
- if (this.lanesSettling && this.measurementsCache.length === count) {
510
- this.lanesSettling = false;
511
- }
512
- const measurements = this.measurementsCache.slice(0, min);
513
- const laneLastIndex = new Array(lanes).fill(
514
- void 0
515
- );
516
- for (let m = 0; m < min; m++) {
517
- const item = measurements[m];
518
- if (item) {
519
- laneLastIndex[item.lane] = m;
520
- }
521
- }
522
- for (let i = min; i < count; i++) {
523
- const key = getItemKey(i);
524
- const cachedLane = this.laneAssignments.get(i);
525
- let lane;
526
- let start;
527
- if (cachedLane !== void 0 && this.options.lanes > 1) {
528
- lane = cachedLane;
529
- const prevIndex = laneLastIndex[lane];
530
- const prevInLane = prevIndex !== void 0 ? measurements[prevIndex] : void 0;
531
- start = prevInLane ? prevInLane.end + this.options.gap : paddingStart + scrollMargin;
532
- } else {
533
- const furthestMeasurement = this.options.lanes === 1 ? measurements[i - 1] : this.getFurthestMeasurement(measurements, i);
534
- start = furthestMeasurement ? furthestMeasurement.end + this.options.gap : paddingStart + scrollMargin;
535
- lane = furthestMeasurement ? furthestMeasurement.lane : i % this.options.lanes;
536
- if (this.options.lanes > 1) {
537
- this.laneAssignments.set(i, lane);
538
- }
539
- }
540
- const measuredSize = itemSizeCache.get(key);
541
- const size = typeof measuredSize === "number" ? measuredSize : this.options.estimateSize(i);
542
- const end = start + size;
543
- measurements[i] = {
544
- index: i,
545
- start,
546
- size,
547
- end,
548
- key,
549
- lane
550
- };
551
- laneLastIndex[lane] = i;
552
- }
553
- this.measurementsCache = measurements;
554
- return measurements;
555
- },
556
- {
557
- key: false,
558
- debug: () => this.options.debug
559
- }
560
- );
561
- this.calculateRange = memo(
562
- () => [
563
- this.getMeasurements(),
564
- this.getSize(),
565
- this.getScrollOffset(),
566
- this.options.lanes
567
- ],
568
- (measurements, outerSize, scrollOffset, lanes) => {
569
- return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
570
- measurements,
571
- outerSize,
572
- scrollOffset,
573
- lanes
574
- }) : null;
575
- },
576
- {
577
- key: false,
578
- debug: () => this.options.debug
579
- }
580
- );
581
- this.getVirtualIndexes = memo(
582
- () => {
583
- let startIndex = null;
584
- let endIndex = null;
585
- const range = this.calculateRange();
586
- if (range) {
587
- startIndex = range.startIndex;
588
- endIndex = range.endIndex;
589
- }
590
- this.maybeNotify.updateDeps([this.isScrolling, startIndex, endIndex]);
591
- return [
592
- this.options.rangeExtractor,
593
- this.options.overscan,
594
- this.options.count,
595
- startIndex,
596
- endIndex
597
- ];
598
- },
599
- (rangeExtractor, overscan, count, startIndex, endIndex) => {
600
- return startIndex === null || endIndex === null ? [] : rangeExtractor({
601
- startIndex,
602
- endIndex,
603
- overscan,
604
- count
605
- });
606
- },
607
- {
608
- key: false,
609
- debug: () => this.options.debug
610
- }
611
- );
612
- this.indexFromElement = (node) => {
613
- const attributeName = this.options.indexAttribute;
614
- const indexStr = node.getAttribute(attributeName);
615
- if (!indexStr) {
616
- console.warn(
617
- `Missing attribute name '${attributeName}={index}' on measured element.`
618
- );
619
- return -1;
620
- }
621
- return parseInt(indexStr, 10);
622
- };
623
- this.shouldMeasureDuringScroll = (index) => {
624
- var _a;
625
- if (!this.scrollState || this.scrollState.behavior !== "smooth") {
626
- return true;
627
- }
628
- const scrollIndex = this.scrollState.index ?? ((_a = this.getVirtualItemForOffset(this.scrollState.lastTargetOffset)) == null ? void 0 : _a.index);
629
- if (scrollIndex !== void 0 && this.range) {
630
- const bufferSize = Math.max(
631
- this.options.overscan,
632
- Math.ceil((this.range.endIndex - this.range.startIndex) / 2)
633
- );
634
- const minIndex = Math.max(0, scrollIndex - bufferSize);
635
- const maxIndex = Math.min(
636
- this.options.count - 1,
637
- scrollIndex + bufferSize
638
- );
639
- return index >= minIndex && index <= maxIndex;
640
- }
641
- return true;
642
- };
643
- this._measureElement = (node, entry) => {
644
- if (!node.isConnected) {
645
- this.observer.unobserve(node);
646
- return;
647
- }
648
- const index = this.indexFromElement(node);
649
- const item = this.measurementsCache[index];
650
- if (!item) {
651
- return;
652
- }
653
- const key = item.key;
654
- const prevNode = this.elementsCache.get(key);
655
- if (prevNode !== node) {
656
- if (prevNode) {
657
- this.observer.unobserve(prevNode);
658
- }
659
- this.observer.observe(node);
660
- this.elementsCache.set(key, node);
661
- }
662
- if (this.shouldMeasureDuringScroll(index)) {
663
- this.resizeItem(index, this.options.measureElement(node, entry, this));
664
- }
665
- };
666
- this.resizeItem = (index, size) => {
667
- var _a;
668
- const item = this.measurementsCache[index];
669
- if (!item) {
670
- return;
671
- }
672
- const itemSize = this.itemSizeCache.get(item.key) ?? item.size;
673
- const delta = size - itemSize;
674
- if (delta !== 0) {
675
- if (((_a = this.scrollState) == null ? void 0 : _a.behavior) !== "smooth" && (this.shouldAdjustScrollPositionOnItemSizeChange !== void 0 ? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this) : item.start < this.getScrollOffset() + this.scrollAdjustments)) {
676
- this._scrollToOffset(this.getScrollOffset(), {
677
- adjustments: this.scrollAdjustments += delta,
678
- behavior: void 0
679
- });
680
- }
681
- this.pendingMeasuredCacheIndexes.push(item.index);
682
- this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size));
683
- this.notify(false);
684
- }
685
- };
686
- this.measureElement = (node) => {
687
- if (!node) {
688
- this.elementsCache.forEach((cached, key) => {
689
- if (!cached.isConnected) {
690
- this.observer.unobserve(cached);
691
- this.elementsCache.delete(key);
692
- }
693
- });
694
- return;
695
- }
696
- this._measureElement(node, void 0);
697
- };
698
- this.getVirtualItems = memo(
699
- () => [this.getVirtualIndexes(), this.getMeasurements()],
700
- (indexes, measurements) => {
701
- const virtualItems = [];
702
- for (let k = 0, len = indexes.length; k < len; k++) {
703
- const i = indexes[k];
704
- const measurement = measurements[i];
705
- virtualItems.push(measurement);
706
- }
707
- return virtualItems;
708
- },
709
- {
710
- key: false,
711
- debug: () => this.options.debug
712
- }
713
- );
714
- this.getVirtualItemForOffset = (offset) => {
715
- const measurements = this.getMeasurements();
716
- if (measurements.length === 0) {
717
- return void 0;
718
- }
719
- return notUndefined(
720
- measurements[findNearestBinarySearch(
721
- 0,
722
- measurements.length - 1,
723
- (index) => notUndefined(measurements[index]).start,
724
- offset
725
- )]
726
- );
727
- };
728
- this.getMaxScrollOffset = () => {
729
- if (!this.scrollElement) return 0;
730
- if ("scrollHeight" in this.scrollElement) {
731
- return this.options.horizontal ? this.scrollElement.scrollWidth - this.scrollElement.clientWidth : this.scrollElement.scrollHeight - this.scrollElement.clientHeight;
732
- } else {
733
- const doc = this.scrollElement.document.documentElement;
734
- return this.options.horizontal ? doc.scrollWidth - this.scrollElement.innerWidth : doc.scrollHeight - this.scrollElement.innerHeight;
735
- }
736
- };
737
- this.getOffsetForAlignment = (toOffset, align, itemSize = 0) => {
738
- if (!this.scrollElement) return 0;
739
- const size = this.getSize();
740
- const scrollOffset = this.getScrollOffset();
741
- if (align === "auto") {
742
- align = toOffset >= scrollOffset + size ? "end" : "start";
743
- }
744
- if (align === "center") {
745
- toOffset += (itemSize - size) / 2;
746
- } else if (align === "end") {
747
- toOffset -= size;
748
- }
749
- const maxOffset = this.getMaxScrollOffset();
750
- return Math.max(Math.min(maxOffset, toOffset), 0);
751
- };
752
- this.getOffsetForIndex = (index, align = "auto") => {
753
- index = Math.max(0, Math.min(index, this.options.count - 1));
754
- const size = this.getSize();
755
- const scrollOffset = this.getScrollOffset();
756
- const item = this.measurementsCache[index];
757
- if (!item) return;
758
- if (align === "auto") {
759
- if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) {
760
- align = "end";
761
- } else if (item.start <= scrollOffset + this.options.scrollPaddingStart) {
762
- align = "start";
763
- } else {
764
- return [scrollOffset, align];
765
- }
766
- }
767
- if (align === "end" && index === this.options.count - 1) {
768
- return [this.getMaxScrollOffset(), align];
769
- }
770
- const toOffset = align === "end" ? item.end + this.options.scrollPaddingEnd : item.start - this.options.scrollPaddingStart;
771
- return [
772
- this.getOffsetForAlignment(toOffset, align, item.size),
773
- align
774
- ];
775
- };
776
- this.scrollToOffset = (toOffset, { align = "start", behavior = "auto" } = {}) => {
777
- const offset = this.getOffsetForAlignment(toOffset, align);
778
- const now = this.now();
779
- this.scrollState = {
780
- index: null,
781
- align,
782
- behavior,
783
- startedAt: now,
784
- lastTargetOffset: offset,
785
- stableFrames: 0
786
- };
787
- this._scrollToOffset(offset, { adjustments: void 0, behavior });
788
- this.scheduleScrollReconcile();
789
- };
790
- this.scrollToIndex = (index, {
791
- align: initialAlign = "auto",
792
- behavior = "auto"
793
- } = {}) => {
794
- index = Math.max(0, Math.min(index, this.options.count - 1));
795
- const offsetInfo = this.getOffsetForIndex(index, initialAlign);
796
- if (!offsetInfo) {
797
- return;
798
- }
799
- const [offset, align] = offsetInfo;
800
- const now = this.now();
801
- this.scrollState = {
802
- index,
803
- align,
804
- behavior,
805
- startedAt: now,
806
- lastTargetOffset: offset,
807
- stableFrames: 0
808
- };
809
- this._scrollToOffset(offset, { adjustments: void 0, behavior });
810
- this.scheduleScrollReconcile();
811
- };
812
- this.scrollBy = (delta, { behavior = "auto" } = {}) => {
813
- const offset = this.getScrollOffset() + delta;
814
- const now = this.now();
815
- this.scrollState = {
816
- index: null,
817
- align: "start",
818
- behavior,
819
- startedAt: now,
820
- lastTargetOffset: offset,
821
- stableFrames: 0
822
- };
823
- this._scrollToOffset(offset, { adjustments: void 0, behavior });
824
- this.scheduleScrollReconcile();
825
- };
826
- this.getTotalSize = () => {
827
- var _a;
828
- const measurements = this.getMeasurements();
829
- let end;
830
- if (measurements.length === 0) {
831
- end = this.options.paddingStart;
832
- } else if (this.options.lanes === 1) {
833
- end = ((_a = measurements[measurements.length - 1]) == null ? void 0 : _a.end) ?? 0;
834
- } else {
835
- const endByLane = Array(this.options.lanes).fill(null);
836
- let endIndex = measurements.length - 1;
837
- while (endIndex >= 0 && endByLane.some((val) => val === null)) {
838
- const item = measurements[endIndex];
839
- if (endByLane[item.lane] === null) {
840
- endByLane[item.lane] = item.end;
841
- }
842
- endIndex--;
843
- }
844
- end = Math.max(...endByLane.filter((val) => val !== null));
845
- }
846
- return Math.max(
847
- end - this.options.scrollMargin + this.options.paddingEnd,
848
- 0
849
- );
850
- };
851
- this._scrollToOffset = (offset, {
852
- adjustments,
853
- behavior
854
- }) => {
855
- this.options.scrollToFn(offset, { behavior, adjustments }, this);
856
- };
857
- this.measure = () => {
858
- this.itemSizeCache = /* @__PURE__ */ new Map();
859
- this.laneAssignments = /* @__PURE__ */ new Map();
860
- this.notify(false);
861
- };
862
- this.setOptions(opts);
863
- }
864
- scheduleScrollReconcile() {
865
- if (!this.targetWindow) {
866
- this.scrollState = null;
867
- return;
868
- }
869
- if (this.rafId != null) return;
870
- this.rafId = this.targetWindow.requestAnimationFrame(() => {
871
- this.rafId = null;
872
- this.reconcileScroll();
873
- });
874
- }
875
- reconcileScroll() {
876
- if (!this.scrollState) return;
877
- const el = this.scrollElement;
878
- if (!el) return;
879
- const MAX_RECONCILE_MS = 5e3;
880
- if (this.now() - this.scrollState.startedAt > MAX_RECONCILE_MS) {
881
- this.scrollState = null;
882
- return;
883
- }
884
- const offsetInfo = this.scrollState.index != null ? this.getOffsetForIndex(this.scrollState.index, this.scrollState.align) : void 0;
885
- const targetOffset = offsetInfo ? offsetInfo[0] : this.scrollState.lastTargetOffset;
886
- const STABLE_FRAMES = 1;
887
- const targetChanged = targetOffset !== this.scrollState.lastTargetOffset;
888
- if (!targetChanged && approxEqual(targetOffset, this.getScrollOffset())) {
889
- this.scrollState.stableFrames++;
890
- if (this.scrollState.stableFrames >= STABLE_FRAMES) {
891
- this.scrollState = null;
892
- return;
893
- }
894
- } else {
895
- this.scrollState.stableFrames = 0;
896
- if (targetChanged) {
897
- this.scrollState.lastTargetOffset = targetOffset;
898
- this.scrollState.behavior = "auto";
899
- this._scrollToOffset(targetOffset, {
900
- adjustments: void 0,
901
- behavior: "auto"
902
- });
903
- }
904
- }
905
- this.scheduleScrollReconcile();
906
- }
907
- }
908
- const findNearestBinarySearch = (low, high, getCurrentValue, value) => {
909
- while (low <= high) {
910
- const middle = (low + high) / 2 | 0;
911
- const currentValue = getCurrentValue(middle);
912
- if (currentValue < value) {
913
- low = middle + 1;
914
- } else if (currentValue > value) {
915
- high = middle - 1;
916
- } else {
917
- return middle;
918
- }
919
- }
920
- if (low > 0) {
921
- return low - 1;
922
- } else {
923
- return 0;
924
- }
925
- };
926
- function calculateRange({
927
- measurements,
928
- outerSize,
929
- scrollOffset,
930
- lanes
931
- }) {
932
- const lastIndex = measurements.length - 1;
933
- const getOffset = (index) => measurements[index].start;
934
- if (measurements.length <= lanes) {
935
- return {
936
- startIndex: 0,
937
- endIndex: lastIndex
938
- };
939
- }
940
- let startIndex = findNearestBinarySearch(
941
- 0,
942
- lastIndex,
943
- getOffset,
944
- scrollOffset
945
- );
946
- let endIndex = startIndex;
947
- if (lanes === 1) {
948
- while (endIndex < lastIndex && measurements[endIndex].end < scrollOffset + outerSize) {
949
- endIndex++;
950
- }
951
- } else if (lanes > 1) {
952
- const endPerLane = Array(lanes).fill(0);
953
- while (endIndex < lastIndex && endPerLane.some((pos) => pos < scrollOffset + outerSize)) {
954
- const item = measurements[endIndex];
955
- endPerLane[item.lane] = item.end;
956
- endIndex++;
957
- }
958
- const startPerLane = Array(lanes).fill(scrollOffset + outerSize);
959
- while (startIndex >= 0 && startPerLane.some((pos) => pos >= scrollOffset)) {
960
- const item = measurements[startIndex];
961
- startPerLane[item.lane] = item.start;
962
- startIndex--;
963
- }
964
- startIndex = Math.max(0, startIndex - startIndex % lanes);
965
- endIndex = Math.min(lastIndex, endIndex + (lanes - 1 - endIndex % lanes));
966
- }
967
- return { startIndex, endIndex };
968
- }
969
-
970
- const useIsomorphicLayoutEffect = typeof document !== "undefined" ? reactExports.useLayoutEffect : reactExports.useEffect;
971
- function useVirtualizerBase({
972
- useFlushSync = true,
973
- ...options
974
- }) {
975
- const rerender = reactExports.useReducer(() => ({}), {})[1];
976
- const resolvedOptions = {
977
- ...options,
978
- onChange: (instance2, sync) => {
979
- var _a;
980
- if (useFlushSync && sync) {
981
- reactDomExports.flushSync(rerender);
982
- } else {
983
- rerender();
984
- }
985
- (_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
986
- }
987
- };
988
- const [instance] = reactExports.useState(
989
- () => new Virtualizer(resolvedOptions)
990
- );
991
- instance.setOptions(resolvedOptions);
992
- useIsomorphicLayoutEffect(() => {
993
- return instance._didMount();
994
- }, []);
995
- useIsomorphicLayoutEffect(() => {
996
- return instance._willUpdate();
997
- });
998
- return instance;
999
- }
1000
- function useVirtualizer(options) {
1001
- return useVirtualizerBase({
1002
- observeElementRect,
1003
- observeElementOffset,
1004
- scrollToFn: elementScroll,
1005
- ...options
1006
- });
1007
- }
1008
-
1009
- const STORAGE_KEY = "lab-animation-level";
1010
- function useLabAnimationLevel() {
1011
- const prefersReducedMotion = useReducedMotion();
1012
- const [level, setLevel] = reactExports.useState("full");
1013
- reactExports.useEffect(() => {
1014
- if (typeof window === "undefined") return;
1015
- const stored = window.localStorage.getItem(STORAGE_KEY);
1016
- if (stored) {
1017
- setLevel(stored);
1018
- return;
1019
- }
1020
- if (prefersReducedMotion) {
1021
- setLevel("off");
1022
- return;
1023
- }
1024
- const lowEnd = typeof navigator !== "undefined" && navigator.hardwareConcurrency <= 4;
1025
- setLevel(lowEnd ? "simple" : "full");
1026
- }, [prefersReducedMotion]);
1027
- const update = (next) => {
1028
- setLevel(next);
1029
- if (typeof window !== "undefined") {
1030
- window.localStorage.setItem(STORAGE_KEY, next);
1031
- }
1032
- };
1033
- return { level, setLevel: update };
1034
- }
1035
-
1036
- function LabDirectChatView({
1037
- projectId,
1038
- readOnly,
1039
- visible,
1040
- prefill,
1041
- leadMessage,
1042
- hideCopilotGreeting,
1043
- mentionablesOverride,
1044
- defaultAgentOverride,
1045
- mentionsEnabledOverride,
1046
- enforcedMentionPrefix,
1047
- lockedMentionPrefix,
1048
- messageMetadata,
1049
- composerMode,
1050
- composerFooter,
1051
- busyOverride,
1052
- onActionsChange,
1053
- onMetaChange,
1054
- onUserSubmit
1055
- }) {
1056
- const normalizedPrefill = reactExports.useMemo(() => {
1057
- if (!prefill?.text) return prefill ?? null;
1058
- const raw = prefill.text;
1059
- if (!raw.startsWith("@")) return prefill;
1060
- const match = raw.match(/^@[^\s]+/);
1061
- if (!match) return prefill;
1062
- const end = match[0].length;
1063
- if (raw[end] === " ") return prefill;
1064
- return { ...prefill, text: `${raw.slice(0, end)} ${raw.slice(end)}` };
1065
- }, [prefill]);
1066
- const [historyOpenOverride, setHistoryOpenOverride] = reactExports.useState(false);
1067
- const historyPanelId = reactExports.useId();
1068
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
1069
- AiManusChatView,
1070
- {
1071
- mode: "lab-direct",
1072
- uiMode: "copilot",
1073
- projectId,
1074
- readOnly,
1075
- visible,
1076
- prefill: normalizedPrefill,
1077
- embedded: true,
1078
- historyMode: "overlay",
1079
- layoutPadding: "flush",
1080
- sessionListEnabled: false,
1081
- historyPanelId,
1082
- historyOpenOverride,
1083
- onHistoryOpenChange: setHistoryOpenOverride,
1084
- onActionsChange,
1085
- onMetaChange,
1086
- mentionablesOverride,
1087
- defaultAgentOverride,
1088
- mentionsEnabledOverride,
1089
- enforcedMentionPrefix,
1090
- lockedMentionPrefix,
1091
- lockLeadingMentionSpace: true,
1092
- messageMetadata,
1093
- hideCopilotGreeting,
1094
- composerMode,
1095
- composerFooter,
1096
- leadMessage,
1097
- busyOverride,
1098
- onUserSubmit
1099
- }
1100
- );
1101
- }
1102
-
1103
- const GROUP_FOLLOW_THRESHOLD_PX = 10;
1104
- const LAB_SESSION_EVENT_LIMIT = 300;
1105
- const buildTextDeltaId = (eventId, seq) => {
1106
- const base = eventId.trim() ? eventId.trim() : "text";
1107
- return `text-${base}-${seq}`;
1108
- };
1109
- const isScrolledToBottom = (container, threshold = GROUP_FOLLOW_THRESHOLD_PX) => {
1110
- const { scrollTop, scrollHeight, clientHeight } = container;
1111
- return scrollHeight - scrollTop - clientHeight <= threshold;
1112
- };
1113
- const isScrolledToTop = (container, threshold = GROUP_FOLLOW_THRESHOLD_PX) => {
1114
- return container.scrollTop <= threshold;
1115
- };
1116
- const useLabSurfaceSession = ({
1117
- projectId,
1118
- questId,
1119
- surface,
1120
- enabled
1121
- }) => {
1122
- const [sessionId, setSessionId] = reactExports.useState(null);
1123
- const [messages, setMessages] = reactExports.useState([]);
1124
- const [historyTruncated, setHistoryTruncated] = reactExports.useState(false);
1125
- const [historyLimit, setHistoryLimit] = reactExports.useState(null);
1126
- const [historyLoadingFull, setHistoryLoadingFull] = reactExports.useState(false);
1127
- const [historyLoading, setHistoryLoading] = reactExports.useState(false);
1128
- const [hasLoadedOnce, setHasLoadedOnce] = reactExports.useState(false);
1129
- const [restoreToken, setRestoreToken] = reactExports.useState(0);
1130
- const sessionIdRef = reactExports.useRef(null);
1131
- const previousSessionIdRef = reactExports.useRef(null);
1132
- const messagesRef = reactExports.useRef([]);
1133
- const assistantMessageIndexRef = reactExports.useRef(/* @__PURE__ */ new Map());
1134
- const attachmentsSeenRef = reactExports.useRef(/* @__PURE__ */ new Set());
1135
- const lastAssistantSegmentIdRef = reactExports.useRef(null);
1136
- const timelineSeqRef = reactExports.useRef(0);
1137
- const fullHistoryRef = reactExports.useRef(false);
1138
- const fullHistoryRequestRef = reactExports.useRef(false);
1139
- const setSessionIdForSurface = useChatSessionStore((state) => state.setSessionIdForSurface);
1140
- const surfaceKey = `lab-${surface}`;
1141
- const resolveTimelineSeq = reactExports.useCallback((candidate) => {
1142
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
1143
- if (candidate > timelineSeqRef.current) {
1144
- timelineSeqRef.current = candidate;
1145
- }
1146
- return candidate;
1147
- }
1148
- timelineSeqRef.current += 1;
1149
- return timelineSeqRef.current;
1150
- }, []);
1151
- const updateMessages = reactExports.useCallback((next) => {
1152
- messagesRef.current = next;
1153
- setMessages(next);
1154
- }, []);
1155
- const appendMessage = reactExports.useCallback(
1156
- (message) => {
1157
- updateMessages([...messagesRef.current, message]);
1158
- },
1159
- [updateMessages]
1160
- );
1161
- const resetState = reactExports.useCallback(() => {
1162
- messagesRef.current = [];
1163
- setMessages([]);
1164
- assistantMessageIndexRef.current = /* @__PURE__ */ new Map();
1165
- attachmentsSeenRef.current = /* @__PURE__ */ new Set();
1166
- lastAssistantSegmentIdRef.current = null;
1167
- timelineSeqRef.current = 0;
1168
- }, []);
1169
- const resetHistoryState = reactExports.useCallback(() => {
1170
- setHistoryTruncated(false);
1171
- setHistoryLimit(null);
1172
- setHistoryLoadingFull(false);
1173
- setHistoryLoading(false);
1174
- setHasLoadedOnce(false);
1175
- fullHistoryRef.current = false;
1176
- fullHistoryRequestRef.current = false;
1177
- setRestoreToken(0);
1178
- }, []);
1179
- const handleEvent = reactExports.useCallback(
1180
- (event) => {
1181
- if (!sessionId) return;
1182
- applyChatEvent(event, {
1183
- sessionId,
1184
- messagesRef,
1185
- assistantMessageIndexRef,
1186
- lastAssistantSegmentIdRef,
1187
- attachmentsSeenRef,
1188
- resolveTimelineSeq,
1189
- buildTextDeltaId,
1190
- appendMessage,
1191
- updateMessages
1192
- });
1193
- },
1194
- [appendMessage, resolveTimelineSeq, sessionId, updateMessages]
1195
- );
1196
- const { sendMessage, restoreSession, stop, connection } = useSSESession({
1197
- sessionId,
1198
- projectId,
1199
- onEvent: handleEvent
1200
- });
1201
- const pingTimerRef = reactExports.useRef(null);
1202
- const pingInFlightRef = reactExports.useRef(false);
1203
- reactExports.useEffect(() => {
1204
- if (!enabled || !projectId || !questId) {
1205
- setSessionId(null);
1206
- resetState();
1207
- resetHistoryState();
1208
- return;
1209
- }
1210
- if (sessionIdRef.current) {
1211
- setSessionId(null);
1212
- }
1213
- let active = true;
1214
- const fetchSession = async () => {
1215
- resetState();
1216
- resetHistoryState();
1217
- setHistoryLoading(true);
1218
- try {
1219
- const response = surface === "group" ? await getLabGroupSession(projectId, questId) : await getLabFriendsSession(projectId, questId);
1220
- if (!active) return;
1221
- const nextSessionId = response?.session_id?.trim?.() ?? response?.session_id;
1222
- if (!nextSessionId) {
1223
- setSessionId(null);
1224
- setHistoryLoading(false);
1225
- setHasLoadedOnce(true);
1226
- return;
1227
- }
1228
- setSessionId(nextSessionId);
1229
- setSessionIdForSurface(projectId, `lab-${surface}`, nextSessionId);
1230
- } catch {
1231
- if (active) {
1232
- setHistoryLoading(false);
1233
- }
1234
- }
1235
- };
1236
- void fetchSession();
1237
- return () => {
1238
- active = false;
1239
- };
1240
- }, [enabled, projectId, questId, resetHistoryState, resetState, setSessionIdForSurface, surface]);
1241
- reactExports.useEffect(() => {
1242
- if (!enabled || !sessionId) return;
1243
- let active = true;
1244
- const restore = async () => {
1245
- const requestFull = fullHistoryRequestRef.current;
1246
- const wantsFull = fullHistoryRef.current || requestFull;
1247
- if (requestFull) {
1248
- fullHistoryRequestRef.current = false;
1249
- }
1250
- resetState();
1251
- setHistoryLoading(true);
1252
- try {
1253
- const session = await restoreSession(
1254
- wantsFull ? { full: true } : { limit: LAB_SESSION_EVENT_LIMIT }
1255
- );
1256
- if (!active) return;
1257
- const events = Array.isArray(session?.events) ? session.events : [];
1258
- let lastEventId = null;
1259
- events.forEach((record) => {
1260
- if (!record || !record.event || !record.data) return;
1261
- if (record.data?.event_id) {
1262
- lastEventId = record.data.event_id;
1263
- }
1264
- applyChatEvent(
1265
- { event: record.event, data: record.data },
1266
- {
1267
- sessionId,
1268
- messagesRef,
1269
- assistantMessageIndexRef,
1270
- lastAssistantSegmentIdRef,
1271
- attachmentsSeenRef,
1272
- resolveTimelineSeq,
1273
- buildTextDeltaId,
1274
- appendMessage,
1275
- updateMessages
1276
- }
1277
- );
1278
- });
1279
- const eventsTruncated = Boolean(session?.events_truncated);
1280
- const eventLimit = typeof session?.event_limit === "number" ? session.event_limit : null;
1281
- setHistoryTruncated(eventsTruncated);
1282
- setHistoryLimit(eventLimit);
1283
- if (wantsFull) {
1284
- fullHistoryRef.current = !eventsTruncated;
1285
- } else {
1286
- fullHistoryRef.current = false;
1287
- }
1288
- if (lastEventId) {
1289
- useChatSessionStore.getState().setLastEventId(sessionId, lastEventId);
1290
- }
1291
- if (!active) return;
1292
- try {
1293
- await sendMessage({
1294
- sessionId,
1295
- message: "",
1296
- surface: `lab-${surface}`,
1297
- replayFromLastEvent: true
1298
- });
1299
- } catch {
1300
- }
1301
- } finally {
1302
- if (requestFull && active) {
1303
- setHistoryLoadingFull(false);
1304
- }
1305
- if (active) {
1306
- setHistoryLoading(false);
1307
- setHasLoadedOnce(true);
1308
- }
1309
- }
1310
- };
1311
- void restore();
1312
- return () => {
1313
- active = false;
1314
- };
1315
- }, [
1316
- appendMessage,
1317
- enabled,
1318
- resolveTimelineSeq,
1319
- resetState,
1320
- restoreSession,
1321
- restoreToken,
1322
- sendMessage,
1323
- sessionId,
1324
- surface,
1325
- updateMessages
1326
- ]);
1327
- reactExports.useEffect(() => {
1328
- if (!enabled || !sessionId) return;
1329
- let isMounted = true;
1330
- const schedulePing = () => {
1331
- const delay = 2e4 + Math.floor(Math.random() * 1e4);
1332
- pingTimerRef.current = window.setTimeout(async () => {
1333
- if (!isMounted || !sessionId || pingInFlightRef.current) {
1334
- schedulePing();
1335
- return;
1336
- }
1337
- pingInFlightRef.current = true;
1338
- try {
1339
- await sendMessage({
1340
- sessionId,
1341
- message: "",
1342
- surface: surfaceKey,
1343
- replayFromLastEvent: true
1344
- });
1345
- } catch {
1346
- } finally {
1347
- pingInFlightRef.current = false;
1348
- }
1349
- schedulePing();
1350
- }, delay);
1351
- };
1352
- schedulePing();
1353
- return () => {
1354
- isMounted = false;
1355
- if (pingTimerRef.current) {
1356
- window.clearTimeout(pingTimerRef.current);
1357
- pingTimerRef.current = null;
1358
- }
1359
- };
1360
- }, [enabled, sendMessage, sessionId, surfaceKey]);
1361
- reactExports.useEffect(() => {
1362
- if (!enabled || !sessionId) return;
1363
- if (connection.status === "closed" || connection.status === "error") {
1364
- if (pingInFlightRef.current) return;
1365
- pingInFlightRef.current = true;
1366
- void sendMessage({
1367
- sessionId,
1368
- message: "",
1369
- surface: surfaceKey,
1370
- replayFromLastEvent: true
1371
- }).finally(() => {
1372
- pingInFlightRef.current = false;
1373
- });
1374
- }
1375
- }, [connection.status, enabled, sendMessage, sessionId, surfaceKey]);
1376
- reactExports.useEffect(() => {
1377
- sessionIdRef.current = sessionId;
1378
- }, [sessionId]);
1379
- reactExports.useEffect(() => {
1380
- if (previousSessionIdRef.current && previousSessionIdRef.current !== sessionId) {
1381
- stop(previousSessionIdRef.current);
1382
- }
1383
- previousSessionIdRef.current = sessionId;
1384
- }, [sessionId, stop]);
1385
- reactExports.useEffect(() => {
1386
- return () => {
1387
- if (sessionIdRef.current) {
1388
- stop(sessionIdRef.current);
1389
- }
1390
- };
1391
- }, [stop]);
1392
- const loadFullHistory = reactExports.useCallback(() => {
1393
- if (!sessionId || !enabled) return;
1394
- if (historyLoadingFull || fullHistoryRef.current) return;
1395
- fullHistoryRequestRef.current = true;
1396
- setHistoryLoadingFull(true);
1397
- setRestoreToken((value) => value + 1);
1398
- }, [enabled, historyLoadingFull, sessionId]);
1399
- return {
1400
- sessionId,
1401
- messages,
1402
- connection,
1403
- sendMessage,
1404
- historyTruncated,
1405
- historyLimit,
1406
- historyLoadingFull,
1407
- historyLoading,
1408
- hasLoadedOnce,
1409
- loadFullHistory
1410
- };
1411
- };
1412
- const getMessageText = (message) => {
1413
- const content = message.content;
1414
- return typeof content?.content === "string" ? content.content : "";
1415
- };
1416
- const getMessageMetadata = (message) => {
1417
- const content = message.content;
1418
- return content?.metadata ?? null;
1419
- };
1420
- const toSelectionMetadata = (selection) => {
1421
- if (!selection) return void 0;
1422
- return {
1423
- selection_type: selection.selection_type,
1424
- selection_ref: selection.selection_ref,
1425
- quest_id: selection.quest_id,
1426
- branch_name: selection.branch_name ?? void 0,
1427
- edge_id: selection.edge_id ?? void 0,
1428
- agent_instance_id: selection.agent_instance_id ?? void 0,
1429
- worktree_rel_path: selection.worktree_rel_path ?? void 0
1430
- };
1431
- };
1432
- const resolveSelectionLabel = (selection) => {
1433
- if (!selection) return "";
1434
- if (selection.label?.trim()) return selection.label.trim();
1435
- return selection.branch_name?.trim() || selection.edge_id?.trim() || selection.agent_instance_id?.trim() || selection.selection_ref;
1436
- };
1437
- const resolveReplyStateLabel = (t, replyState) => {
1438
- const normalized = typeof replyState === "string" ? replyState.trim().toLowerCase() : "";
1439
- if (!normalized) return null;
1440
- if (normalized === "queued") {
1441
- return t("copilot_group_reply_state_queued", void 0, "Queued");
1442
- }
1443
- if (normalized === "acked") {
1444
- return t("copilot_group_reply_state_acked", void 0, "Acknowledged");
1445
- }
1446
- if (normalized === "final") {
1447
- return t("copilot_group_reply_state_final", void 0, "Final");
1448
- }
1449
- return normalized;
1450
- };
1451
- function LabControlReferenceChips({
1452
- selection,
1453
- proposal,
1454
- onClearSelection,
1455
- onClearProposal,
1456
- selectionPrefix,
1457
- proposalPrefix
1458
- }) {
1459
- if (!selection && !proposal) return null;
1460
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-[var(--lab-border)] bg-[var(--lab-background)] px-4 py-3", children: [
1461
- selection ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
1462
- "button",
1463
- {
1464
- type: "button",
1465
- onClick: onClearSelection,
1466
- className: "inline-flex items-center gap-2 rounded-full border border-[var(--lab-border)] bg-[var(--lab-surface)] px-3 py-1 text-[11px] font-medium text-[var(--lab-text-primary)]",
1467
- children: [
1468
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[var(--lab-text-secondary)]", children: selectionPrefix }),
1469
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: resolveSelectionLabel(selection) }),
1470
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[var(--lab-text-muted)]", children: "×" })
1471
- ]
1472
- }
1473
- ) : null,
1474
- proposal ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
1475
- "button",
1476
- {
1477
- type: "button",
1478
- onClick: onClearProposal,
1479
- className: "inline-flex items-center gap-2 rounded-full border border-[rgba(83,176,174,0.28)] bg-[rgba(83,176,174,0.12)] px-3 py-1 text-[11px] font-medium text-[var(--lab-text-primary)]",
1480
- children: [
1481
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[var(--lab-text-secondary)]", children: proposalPrefix }),
1482
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: proposal.action_type }),
1483
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[var(--lab-text-muted)]", children: [
1484
- "· ",
1485
- proposal.status
1486
- ] }),
1487
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[var(--lab-text-muted)]", children: "×" })
1488
- ]
1489
- }
1490
- ) : null
1491
- ] });
1492
- }
1493
- const getMessageRole = (message) => {
1494
- if (message.type !== "text_delta") return null;
1495
- const content = message.content;
1496
- return content.role;
1497
- };
1498
- const formatAbsoluteTimestamp = (value) => {
1499
- if (value === null || value === void 0) return "";
1500
- const date = typeof value === "number" ? new Date(value < 1e12 ? value * 1e3 : value) : new Date(value);
1501
- if (Number.isNaN(date.getTime())) return "";
1502
- const pad = (item) => String(item).padStart(2, "0");
1503
- const year = pad(date.getFullYear() % 100);
1504
- const month = pad(date.getMonth() + 1);
1505
- const day = pad(date.getDate());
1506
- const hour = pad(date.getHours());
1507
- const minute = pad(date.getMinutes());
1508
- const second = pad(date.getSeconds());
1509
- return `${year}-${month}-${day} ${hour}-${minute}-${second}`;
1510
- };
1511
- const resolveMessageTitle = (message) => {
1512
- const role = getMessageRole(message);
1513
- if (role === "user") return "You";
1514
- const metadata = getMessageMetadata(message);
1515
- if (metadata?.sender_name) return metadata.sender_name;
1516
- if (metadata?.agent_display_name) return metadata.agent_display_name;
1517
- if (metadata?.sender_label) return metadata.sender_label;
1518
- if (metadata?.agent_label) return metadata.agent_label;
1519
- if (metadata?.agent_id) return `@${metadata.agent_id}`;
1520
- return "Agent";
1521
- };
1522
- const resolveMomentMedia = (raw) => {
1523
- if (!raw) return [];
1524
- const normalizeEntry = (entry) => {
1525
- if (typeof entry === "string") {
1526
- const trimmed = entry.trim();
1527
- return trimmed ? { url: trimmed } : null;
1528
- }
1529
- if (!entry || typeof entry !== "object") return null;
1530
- const record = entry;
1531
- const urlCandidate = typeof record.url === "string" && record.url || typeof record.src === "string" && record.src || typeof record.file_url === "string" && record.file_url || typeof record.preview_url === "string" && record.preview_url || typeof record.path === "string" && record.path || "";
1532
- if (!urlCandidate) return null;
1533
- const label = typeof record.label === "string" ? record.label : void 0;
1534
- return { url: urlCandidate, label };
1535
- };
1536
- const rawList = Array.isArray(raw) ? raw : typeof raw === "object" ? raw.items || raw.images || raw.files || [] : [];
1537
- if (!Array.isArray(rawList)) return [];
1538
- return rawList.map((entry) => normalizeEntry(entry)).filter((entry) => Boolean(entry)).slice(0, 9);
1539
- };
1540
- const resolveMomentLikes = (raw) => {
1541
- if (!raw) return [];
1542
- const normalizeEntry = (entry) => {
1543
- if (typeof entry === "string") {
1544
- const trimmed2 = entry.trim();
1545
- return trimmed2 ? { name: trimmed2 } : null;
1546
- }
1547
- if (!entry || typeof entry !== "object") return null;
1548
- const record = entry;
1549
- const nameCandidate = typeof record.name === "string" && record.name || typeof record.user_name === "string" && record.user_name || typeof record.username === "string" && record.username || typeof record.display_name === "string" && record.display_name || typeof record.label === "string" && record.label || typeof record.user_id === "string" && record.user_id || "";
1550
- const trimmed = nameCandidate.trim();
1551
- if (!trimmed) return null;
1552
- return { name: trimmed };
1553
- };
1554
- const rawListValue = Array.isArray(raw) ? raw : typeof raw === "object" ? raw.items || raw.users || raw.likes || [] : [];
1555
- const rawList = Array.isArray(rawListValue) ? rawListValue : [];
1556
- return rawList.map((entry) => normalizeEntry(entry)).filter((entry) => Boolean(entry));
1557
- };
1558
- const resolveMomentComments = (raw) => {
1559
- if (!raw) return [];
1560
- const normalizeEntry = (entry) => {
1561
- if (typeof entry === "string") {
1562
- const trimmed = entry.trim();
1563
- return trimmed ? { name: "Unknown", content: trimmed } : null;
1564
- }
1565
- if (!entry || typeof entry !== "object") return null;
1566
- const record = entry;
1567
- const nameCandidate = typeof record.name === "string" && record.name || typeof record.user_name === "string" && record.user_name || typeof record.username === "string" && record.username || typeof record.display_name === "string" && record.display_name || typeof record.label === "string" && record.label || typeof record.user_id === "string" && record.user_id || "";
1568
- const contentCandidate = typeof record.content === "string" && record.content || typeof record.text === "string" && record.text || typeof record.message === "string" && record.message || typeof record.body === "string" && record.body || "";
1569
- const name = nameCandidate.trim() || "Unknown";
1570
- const content = contentCandidate.trim();
1571
- if (!content) return null;
1572
- return { name, content };
1573
- };
1574
- const rawListValue = Array.isArray(raw) ? raw : typeof raw === "object" ? raw.items || raw.comments || raw.entries || [] : [];
1575
- const rawList = Array.isArray(rawListValue) ? rawListValue : [];
1576
- return rawList.map((entry) => normalizeEntry(entry)).filter((entry) => Boolean(entry));
1577
- };
1578
- const mergeMomentLikes = (base, extra) => {
1579
- const seen = /* @__PURE__ */ new Map();
1580
- base.forEach((item) => {
1581
- const key = item.name.trim().toLowerCase();
1582
- if (!key) return;
1583
- if (!seen.has(key)) seen.set(key, item.name);
1584
- });
1585
- extra.forEach((item) => {
1586
- const key = item.name.trim().toLowerCase();
1587
- if (!key) return;
1588
- if (!seen.has(key)) seen.set(key, item.name);
1589
- });
1590
- return Array.from(seen.values());
1591
- };
1592
- const mergeMomentComments = (base, extra) => {
1593
- const seen = /* @__PURE__ */ new Map();
1594
- base.forEach((item) => {
1595
- const key = `${item.name.trim().toLowerCase()}::${item.content.trim().toLowerCase()}`;
1596
- if (seen.has(key)) return;
1597
- seen.set(key, item);
1598
- });
1599
- extra.forEach((item) => {
1600
- const key = `${item.name.trim().toLowerCase()}::${item.content.trim().toLowerCase()}`;
1601
- if (seen.has(key)) return;
1602
- seen.set(key, item);
1603
- });
1604
- return Array.from(seen.values());
1605
- };
1606
- const resolveMomentTimestamp = (message) => {
1607
- const content = message.content;
1608
- if (typeof content?.timestamp === "number") {
1609
- return new Date(content.timestamp * 1e3).toISOString();
1610
- }
1611
- const metadata = content?.metadata;
1612
- if (typeof metadata?.source_ts === "string") {
1613
- return metadata.source_ts;
1614
- }
1615
- return null;
1616
- };
1617
- function LabCopilotHeader({
1618
- disabled,
1619
- agents,
1620
- templates,
1621
- quests,
1622
- onClearChat,
1623
- clearChatDisabled
1624
- }) {
1625
- const mode = useLabCopilotStore((state) => state.mode);
1626
- const setMode = useLabCopilotStore((state) => state.setMode);
1627
- const activeAgentId = useLabCopilotStore((state) => state.activeAgentId);
1628
- const activeQuestId = useLabCopilotStore((state) => state.activeQuestId);
1629
- const followEffects = useLabCopilotStore((state) => state.followEffects);
1630
- const setFollowEffects = useLabCopilotStore((state) => state.setFollowEffects);
1631
- const agentStatusOverrides = useLabCopilotStore((state) => state.agentStatusOverrides);
1632
- const piOnboardingActive = useLabCopilotStore((state) => state.piOnboardingActive);
1633
- const endPiOnboarding = useLabCopilotStore((state) => state.endPiOnboarding);
1634
- const templatesById = reactExports.useMemo(() => {
1635
- return new Map(templates.map((template) => [template.template_id, template]));
1636
- }, [templates]);
1637
- const avatarColors = reactExports.useMemo(() => buildAvatarColorMap(agents), [agents]);
1638
- const activeAgent = reactExports.useMemo(() => {
1639
- if (!activeAgentId) return null;
1640
- return agents.find((agent) => agent.instance_id === activeAgentId) ?? null;
1641
- }, [activeAgentId, agents]);
1642
- const activeTemplate = activeAgent?.template_id ? templatesById.get(activeAgent.template_id) ?? null : null;
1643
- const resolvedQuestId = activeAgent?.active_quest_id ?? activeQuestId ?? null;
1644
- const activeQuest = resolvedQuestId ? quests.find((quest) => quest.quest_id === resolvedQuestId) ?? null : null;
1645
- const questLabel = activeQuest ? resolveQuestLabel(activeQuest) : "Not joined yet";
1646
- const agentDisplayName = activeAgent ? resolveAgentDisplayName(activeAgent) : "";
1647
- const agentLogo = activeAgent ? resolveAgentLogo(activeAgent, activeTemplate) : "";
1648
- const agentAvatarColor = activeAgent ? avatarColors.get(activeAgent.instance_id) || pickAvatarFrameColor(activeAgent.instance_id) : null;
1649
- const overrideStatus = activeAgent ? agentStatusOverrides[activeAgent.instance_id] ?? null : null;
1650
- const rawAgentStatus = overrideStatus ?? activeAgent?.status ?? "";
1651
- const activeAgentStatus = typeof rawAgentStatus === "string" ? rawAgentStatus.trim() : "";
1652
- const showRunningStatus = mode === "direct" && activeAgent && Boolean(activeAgentStatus) && isLabWorkingStatus(activeAgentStatus) && activeAgentStatus.toLowerCase() !== "waiting";
1653
- const [contextMenu, setContextMenu] = reactExports.useState(null);
1654
- const clearChatBlocked = Boolean(clearChatDisabled || !onClearChat);
1655
- const items = [
1656
- { value: "direct", label: "Direct" },
1657
- { value: "group", label: "Group" },
1658
- { value: "friends", label: "Friends" }
1659
- ];
1660
- const handleAvatarContextMenu = reactExports.useCallback(
1661
- (event) => {
1662
- if (!onClearChat) return;
1663
- event.preventDefault();
1664
- event.stopPropagation();
1665
- setContextMenu({ x: event.clientX, y: event.clientY });
1666
- },
1667
- [onClearChat]
1668
- );
1669
- const handleClearChat = reactExports.useCallback(() => {
1670
- if (clearChatBlocked || !onClearChat) return;
1671
- onClearChat();
1672
- setContextMenu(null);
1673
- }, [clearChatBlocked, onClearChat]);
1674
- const handleToggleFollow = reactExports.useCallback(() => {
1675
- setFollowEffects(!followEffects);
1676
- setContextMenu(null);
1677
- }, [followEffects, setFollowEffects]);
1678
- reactExports.useEffect(() => {
1679
- if (!contextMenu) return;
1680
- const handleDismiss = () => setContextMenu(null);
1681
- const handleKey = (event) => {
1682
- if (event.key === "Escape") {
1683
- setContextMenu(null);
1684
- }
1685
- };
1686
- window.addEventListener("click", handleDismiss);
1687
- window.addEventListener("contextmenu", handleDismiss);
1688
- window.addEventListener("keydown", handleKey);
1689
- window.addEventListener("scroll", handleDismiss, true);
1690
- return () => {
1691
- window.removeEventListener("click", handleDismiss);
1692
- window.removeEventListener("contextmenu", handleDismiss);
1693
- window.removeEventListener("keydown", handleKey);
1694
- window.removeEventListener("scroll", handleDismiss, true);
1695
- };
1696
- }, [contextMenu]);
1697
- const contextMenuPortal = contextMenu && typeof document !== "undefined" ? reactDomExports.createPortal(
1698
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-context-menu", style: { left: contextMenu.x, top: contextMenu.y }, children: [
1699
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-context-item", children: [
1700
- "Quest: ",
1701
- questLabel
1702
- ] }),
1703
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: handleClearChat, disabled: clearChatBlocked, children: "Clear chat" }),
1704
- /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", onClick: handleToggleFollow, children: [
1705
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1706
- Check,
1707
- {
1708
- size: 14,
1709
- className: cn("shrink-0", followEffects ? "opacity-100" : "opacity-0")
1710
- }
1711
- ),
1712
- "Follow effects"
1713
- ] })
1714
- ] }),
1715
- document.body
1716
- ) : null;
1717
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1718
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-header", children: [
1719
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-section-tabs lab-copilot-tabs", role: "tablist", "aria-label": "Copilot modes", children: items.map((item) => {
1720
- const isActive = mode === item.value;
1721
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1722
- "button",
1723
- {
1724
- type: "button",
1725
- role: "tab",
1726
- "aria-selected": isActive,
1727
- disabled,
1728
- onClick: () => setMode(item.value),
1729
- className: cn(
1730
- "lab-section-tab",
1731
- isActive && "lab-section-tab-active",
1732
- disabled && "cursor-not-allowed opacity-60"
1733
- ),
1734
- children: [
1735
- item.label,
1736
- isActive ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-section-tab-indicator" }) : null
1737
- ]
1738
- },
1739
- item.value
1740
- );
1741
- }) }),
1742
- mode === "direct" && activeAgent ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-agent", onContextMenu: handleAvatarContextMenu, children: [
1743
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-avatar lab-avatar-sm", children: [
1744
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1745
- "span",
1746
- {
1747
- className: "lab-avatar-ring",
1748
- style: { borderColor: agentAvatarColor || pickAvatarFrameColor(activeAgent.instance_id) }
1749
- }
1750
- ),
1751
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: agentLogo, alt: agentDisplayName || "Agent" })
1752
- ] }),
1753
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-agent-meta", children: [
1754
- agentDisplayName ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-agent-name", children: agentDisplayName }) : null,
1755
- showRunningStatus ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-agent-status", children: [
1756
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-status-dot lab-status-dot-running" }),
1757
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Working" })
1758
- ] }) : null
1759
- ] }),
1760
- piOnboardingActive ? /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "lab-onboarding-exit", onClick: endPiOnboarding, children: "Exit onboarding" }) : null
1761
- ] }) : null
1762
- ] }),
1763
- contextMenuPortal
1764
- ] });
1765
- }
1766
- function LabCopilotPanel({
1767
- projectId,
1768
- readOnly,
1769
- cliStatus,
1770
- templates,
1771
- agents,
1772
- quests,
1773
- prefill: externalPrefill,
1774
- onActionsChange
1775
- }) {
1776
- const queryClient = useQueryClient();
1777
- const { addToast } = useToast();
1778
- const { t } = useI18n("lab");
1779
- const mode = useLabCopilotStore((state) => state.mode);
1780
- const activeAgentId = useLabCopilotStore((state) => state.activeAgentId);
1781
- const activeQuestId = useLabCopilotStore((state) => state.activeQuestId);
1782
- const directPrefill = useLabCopilotStore((state) => state.directPrefill);
1783
- const directComposeRequest = useLabCopilotStore((state) => state.directComposeRequest);
1784
- const setActiveQuest = useLabCopilotStore((state) => state.setActiveQuest);
1785
- const setActiveAgent = useLabCopilotStore((state) => state.setActiveAgent);
1786
- const setDirectPrefill = useLabCopilotStore((state) => state.setDirectPrefill);
1787
- const clearDirectComposeRequest = useLabCopilotStore((state) => state.clearDirectComposeRequest);
1788
- const agentStatusOverrides = useLabCopilotStore((state) => state.agentStatusOverrides);
1789
- const setAgentStatusOverride = useLabCopilotStore((state) => state.setAgentStatusOverride);
1790
- const piOnboardingActive = useLabCopilotStore((state) => state.piOnboardingActive);
1791
- const piOnboardingQuestId = useLabCopilotStore((state) => state.piOnboardingQuestId);
1792
- const piOnboardingKind = useLabCopilotStore((state) => state.piOnboardingKind);
1793
- const endPiOnboarding = useLabCopilotStore((state) => state.endPiOnboarding);
1794
- const selection = useLabGraphSelectionStore((state) => state.selection);
1795
- const activeProposal = useLabGraphSelectionStore((state) => state.activeProposal);
1796
- const setSelection = useLabGraphSelectionStore((state) => state.setSelection);
1797
- const setActiveProposal = useLabGraphSelectionStore((state) => state.setActiveProposal);
1798
- const setSessionIdForSurface = useChatSessionStore((state) => state.setSessionIdForSurface);
1799
- const clearSessionIdForSurface = useChatSessionStore((state) => state.clearSessionIdForSurface);
1800
- const currentSessionId = useChatSessionStore(
1801
- (state) => projectId ? state.sessionIdsByProjectSurface[projectId]?.["lab-direct"] ?? null : null
1802
- );
1803
- const cliServerId = useChatSessionStore(
1804
- (state) => projectId ? state.cliServerIdsByProject[projectId] ?? null : null
1805
- );
1806
- const [agentPrefill, setAgentPrefill] = reactExports.useState(null);
1807
- const [copilotActions, setCopilotActions] = reactExports.useState(null);
1808
- const [copilotMeta, setCopilotMeta] = reactExports.useState(null);
1809
- const autoSubmittedComposeTokenRef = reactExports.useRef(null);
1810
- const resetKeyRef = reactExports.useRef("init");
1811
- const prevAgentIdRef = reactExports.useRef(null);
1812
- const prevRespondingRef = reactExports.useRef(false);
1813
- const { level: animationLevel } = useLabAnimationLevel();
1814
- const prefersReducedMotion = useReducedMotion();
1815
- const allowMotion = animationLevel === "full" && !prefersReducedMotion;
1816
- const isCopilotMetaEqual = reactExports.useCallback(
1817
- (prev, next) => {
1818
- if (!prev) return false;
1819
- return prev.threadId === next.threadId && prev.historyOpen === next.historyOpen && prev.isResponding === next.isResponding && prev.ready === next.ready && prev.isRestoring === next.isRestoring && prev.restoreAttempted === next.restoreAttempted && prev.hasHistory === next.hasHistory && prev.error === next.error && prev.title === next.title && prev.statusText === next.statusText && prev.statusPrevText === next.statusPrevText && prev.statusKey === next.statusKey && prev.toolPanelVisible === next.toolPanelVisible && prev.toolToggleVisible === next.toolToggleVisible && prev.attachmentsDrawerOpen === next.attachmentsDrawerOpen && prev.fixWithAiRunning === next.fixWithAiRunning;
1820
- },
1821
- []
1822
- );
1823
- reactExports.useEffect(() => {
1824
- if (mode !== "direct") {
1825
- resetKeyRef.current = "init";
1826
- }
1827
- }, [mode]);
1828
- const templatesById = reactExports.useMemo(() => {
1829
- return new Map(templates.map((template) => [template.template_id, template]));
1830
- }, [templates]);
1831
- const agentsById = reactExports.useMemo(() => {
1832
- return new Map(agents.map((agent) => [agent.instance_id, agent]));
1833
- }, [agents]);
1834
- const avatarColors = reactExports.useMemo(() => buildAvatarColorMap(agents), [agents]);
1835
- const resolvedAgentId = activeAgentId ?? null;
1836
- const activeAgent = resolvedAgentId ? agentsById.get(resolvedAgentId) ?? null : null;
1837
- const isResponding = Boolean(copilotMeta?.isResponding);
1838
- const rawActiveAgentStatus = activeAgent && agentStatusOverrides[activeAgent.instance_id] ? agentStatusOverrides[activeAgent.instance_id] : activeAgent?.status ?? "";
1839
- const activeAgentStatusKey = typeof rawActiveAgentStatus === "string" ? rawActiveAgentStatus.toLowerCase() : "";
1840
- const activeAgentWorking = Boolean(activeAgent) && Boolean(activeAgentStatusKey) && isLabWorkingStatus(activeAgentStatusKey) && activeAgentStatusKey !== "waiting";
1841
- const activeAgentStatusLabel = typeof activeAgent?.status === "string" ? activeAgent.status.toLowerCase() : "";
1842
- const shouldOverrideWorking = mode === "direct" && Boolean(activeAgent) && isResponding && activeAgentStatusLabel !== "waiting";
1843
- const desiredStatusOverride = shouldOverrideWorking ? "working" : null;
1844
- const currentStatusOverride = activeAgent ? agentStatusOverrides[activeAgent.instance_id] ?? null : null;
1845
- reactExports.useEffect(() => {
1846
- const prev = prevAgentIdRef.current;
1847
- const next = activeAgent?.instance_id ?? null;
1848
- if (prev && prev !== next) {
1849
- setAgentStatusOverride(prev, null);
1850
- }
1851
- prevAgentIdRef.current = next;
1852
- }, [activeAgent?.instance_id, setAgentStatusOverride]);
1853
- reactExports.useEffect(() => {
1854
- if (!activeAgent?.instance_id) return;
1855
- if (currentStatusOverride === desiredStatusOverride) return;
1856
- setAgentStatusOverride(activeAgent.instance_id, desiredStatusOverride);
1857
- }, [activeAgent?.instance_id, currentStatusOverride, desiredStatusOverride, setAgentStatusOverride]);
1858
- reactExports.useEffect(() => {
1859
- if (!projectId || mode !== "direct" || !activeAgent?.instance_id) {
1860
- prevRespondingRef.current = false;
1861
- return;
1862
- }
1863
- const wasResponding = prevRespondingRef.current;
1864
- prevRespondingRef.current = isResponding;
1865
- if (wasResponding && !isResponding) {
1866
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
1867
- }
1868
- }, [activeAgent?.instance_id, isResponding, mode, projectId, queryClient]);
1869
- const activeTemplate = activeAgent?.template_id ? templatesById.get(activeAgent.template_id) ?? null : null;
1870
- const initQuestion = activeTemplate?.init_question?.trim() || "";
1871
- const hasInitQuestion = Boolean(initQuestion);
1872
- const activeQuest = (activeAgent?.active_quest_id ? quests.find((quest) => quest.quest_id === activeAgent.active_quest_id) : quests.find((quest) => quest.quest_id === activeQuestId)) ?? null;
1873
- const initTemplateEligible = mode === "direct" && hasInitQuestion;
1874
- const initTemplateReady = Boolean(
1875
- copilotMeta?.ready && copilotMeta?.restoreAttempted && !copilotMeta?.isRestoring
1876
- );
1877
- const shouldShowInitTemplate = initTemplateEligible && initTemplateReady && !copilotMeta?.hasHistory;
1878
- const shouldHoldInitPrefill = initTemplateEligible && !initTemplateReady;
1879
- const [directSessionId, setDirectSessionId] = reactExports.useState(null);
1880
- const [directSessionRetryToken, setDirectSessionRetryToken] = reactExports.useState(0);
1881
- const directSessionRetryRef = reactExports.useRef(null);
1882
- const directSessionRetryCountRef = reactExports.useRef(0);
1883
- const resetDirectSessionRetry = reactExports.useCallback(() => {
1884
- if (directSessionRetryRef.current) {
1885
- window.clearTimeout(directSessionRetryRef.current);
1886
- }
1887
- directSessionRetryRef.current = null;
1888
- directSessionRetryCountRef.current = 0;
1889
- }, []);
1890
- const scheduleDirectSessionRetry = reactExports.useCallback(() => {
1891
- if (directSessionRetryRef.current) return;
1892
- const attempt = Math.min(directSessionRetryCountRef.current, 4);
1893
- const delayMs = Math.min(15e3, 1500 * 2 ** attempt);
1894
- directSessionRetryCountRef.current = attempt + 1;
1895
- directSessionRetryRef.current = window.setTimeout(() => {
1896
- directSessionRetryRef.current = null;
1897
- setDirectSessionRetryToken((value) => value + 1);
1898
- }, delayMs);
1899
- }, []);
1900
- reactExports.useEffect(() => {
1901
- return () => resetDirectSessionRetry();
1902
- }, [resetDirectSessionRetry]);
1903
- reactExports.useEffect(() => {
1904
- if (activeQuestId || quests.length !== 1) return;
1905
- setActiveQuest(quests[0].quest_id);
1906
- }, [activeQuestId, quests, setActiveQuest]);
1907
- reactExports.useEffect(() => {
1908
- if (!projectId || mode !== "direct") return;
1909
- if (!activeAgent?.instance_id) {
1910
- setDirectSessionId(null);
1911
- resetDirectSessionRetry();
1912
- clearSessionIdForSurface(projectId, "lab-direct");
1913
- return;
1914
- }
1915
- const fallbackSessionId = activeAgent.direct_session_id ?? null;
1916
- if (fallbackSessionId) {
1917
- setDirectSessionId((prev) => prev === fallbackSessionId ? prev : fallbackSessionId);
1918
- if (currentSessionId !== fallbackSessionId) {
1919
- setSessionIdForSurface(projectId, "lab-direct", fallbackSessionId);
1920
- }
1921
- }
1922
- let cancelled = false;
1923
- getLabAgentDirectSession(projectId, activeAgent.instance_id).then((response) => {
1924
- if (cancelled) return;
1925
- resetDirectSessionRetry();
1926
- setDirectSessionId(response.session_id);
1927
- if (currentSessionId !== response.session_id) {
1928
- setSessionIdForSurface(projectId, "lab-direct", response.session_id);
1929
- }
1930
- }).catch((error) => {
1931
- if (cancelled) return;
1932
- const detail = typeof error?.response?.data?.detail === "string" ? error.response.data.detail : null;
1933
- if (detail && ["cli_server_required", "cli_offline", "cli_server_not_bound"].includes(detail)) {
1934
- scheduleDirectSessionRetry();
1935
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
1936
- return;
1937
- }
1938
- if (!fallbackSessionId) {
1939
- setDirectSessionId(null);
1940
- }
1941
- });
1942
- return () => {
1943
- cancelled = true;
1944
- };
1945
- }, [
1946
- activeAgent?.direct_session_id,
1947
- activeAgent?.instance_id,
1948
- activeAgent?.cli_server_id,
1949
- activeAgent?.active_quest_id,
1950
- activeAgent?.active_quest_node_id,
1951
- clearSessionIdForSurface,
1952
- currentSessionId,
1953
- directSessionRetryToken,
1954
- mode,
1955
- projectId,
1956
- queryClient,
1957
- resetDirectSessionRetry,
1958
- scheduleDirectSessionRetry,
1959
- setSessionIdForSurface
1960
- ]);
1961
- reactExports.useEffect(() => {
1962
- if (!piOnboardingActive) return;
1963
- if (!activeAgent) {
1964
- endPiOnboarding();
1965
- return;
1966
- }
1967
- if (activeTemplate?.template_key !== "pi" && activeAgent.agent_id !== "pi") {
1968
- endPiOnboarding();
1969
- }
1970
- }, [activeAgent, activeTemplate?.template_key, endPiOnboarding, piOnboardingActive]);
1971
- reactExports.useEffect(() => {
1972
- if (!piOnboardingActive) return;
1973
- if (copilotMeta?.hasHistory) {
1974
- endPiOnboarding();
1975
- }
1976
- }, [copilotMeta?.hasHistory, endPiOnboarding, piOnboardingActive]);
1977
- const cliReadOnly = cliStatus !== "online";
1978
- const labReadOnly = readOnly || cliReadOnly;
1979
- const mentionsEnabled = !labReadOnly && !(mode === "direct" && shouldShowInitTemplate);
1980
- const mentionablesOverride = reactExports.useMemo(() => {
1981
- return agents.map((agent) => {
1982
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
1983
- return buildAgentDescriptor(agent, template);
1984
- });
1985
- }, [agents, templatesById]);
1986
- const defaultAgentOverride = reactExports.useMemo(() => {
1987
- if (!activeAgent) return void 0;
1988
- const template = activeAgent.template_id ? templatesById.get(activeAgent.template_id) ?? null : null;
1989
- return buildAgentDescriptor(activeAgent, template);
1990
- }, [activeAgent, templatesById]);
1991
- reactExports.useEffect(() => {
1992
- if (!activeAgent) {
1993
- setAgentPrefill(null);
1994
- return;
1995
- }
1996
- if (shouldShowInitTemplate) {
1997
- setAgentPrefill(null);
1998
- return;
1999
- }
2000
- if (shouldHoldInitPrefill) {
2001
- setAgentPrefill(null);
2002
- return;
2003
- }
2004
- const rawLabel = activeAgent.mention_label?.trim() || activeAgent.agent_id;
2005
- const label = rawLabel.startsWith("@") ? rawLabel : `@${rawLabel}`;
2006
- setAgentPrefill({ text: `${label} `, focus: false, token: Date.now() });
2007
- }, [activeAgent, shouldHoldInitPrefill, shouldShowInitTemplate]);
2008
- const enforcedMentionLabel = reactExports.useMemo(() => {
2009
- if (activeAgent) {
2010
- const rawLabel = activeAgent.mention_label?.trim() || activeAgent.agent_id;
2011
- return rawLabel.startsWith("@") ? rawLabel : `@${rawLabel}`;
2012
- }
2013
- return `@${DEFAULT_AGENT_ID}`;
2014
- }, [activeAgent]);
2015
- const useStarterPrompt = shouldShowInitTemplate;
2016
- const enforcedMentionPrefix = mode === "direct" ? enforcedMentionLabel : void 0;
2017
- const lockedMentionPrefix = activeAgent && mode === "direct" ? enforcedMentionLabel : void 0;
2018
- const handleOpenRecruit = reactExports.useCallback(() => {
2019
- if (labReadOnly) {
2020
- addToast({
2021
- type: "warning",
2022
- title: "Recruitment unavailable",
2023
- description: cliStatus === "online" ? "Recruitment is disabled in read-only mode." : "Bind an execution server to recruit agents."
2024
- });
2025
- return;
2026
- }
2027
- if (typeof window !== "undefined") {
2028
- window.dispatchEvent(new CustomEvent("lab:open-recruit"));
2029
- }
2030
- }, [addToast, cliStatus, labReadOnly]);
2031
- const handleSelectAgent = reactExports.useCallback(
2032
- (agentId) => {
2033
- setActiveAgent(agentId);
2034
- },
2035
- [setActiveAgent]
2036
- );
2037
- const starterMetadata = reactExports.useMemo(() => {
2038
- if (!activeAgent) {
2039
- return { agent_label: enforcedMentionLabel };
2040
- }
2041
- const logo = resolveAgentLogo(activeAgent, activeTemplate);
2042
- return {
2043
- agent_label: enforcedMentionLabel,
2044
- agent_id: activeAgent.agent_id,
2045
- agent_instance_id: activeAgent.instance_id,
2046
- agent_display_name: resolveAgentDisplayName(activeAgent),
2047
- agent_logo: logo || void 0
2048
- };
2049
- }, [activeAgent, activeTemplate, enforcedMentionLabel]);
2050
- const starterMessage = reactExports.useMemo(() => {
2051
- if (!useStarterPrompt) return null;
2052
- const timestamp = Math.floor(Date.now() / 1e3);
2053
- return {
2054
- id: `lab-starter-${resolvedAgentId ?? "unknown"}`,
2055
- type: "text_delta",
2056
- seq: 0,
2057
- ts: timestamp,
2058
- content: {
2059
- content: initQuestion,
2060
- role: "assistant",
2061
- status: "completed",
2062
- timestamp,
2063
- metadata: starterMetadata
2064
- }
2065
- };
2066
- }, [initQuestion, resolvedAgentId, starterMetadata, useStarterPrompt]);
2067
- const handleActionsChange = reactExports.useCallback(
2068
- (actions) => {
2069
- setCopilotActions(actions);
2070
- onActionsChange?.(actions);
2071
- },
2072
- [onActionsChange]
2073
- );
2074
- const handleMetaChange = reactExports.useCallback(
2075
- (meta) => {
2076
- setCopilotMeta((prev) => isCopilotMetaEqual(prev, meta) ? prev : meta);
2077
- },
2078
- [isCopilotMetaEqual]
2079
- );
2080
- const handleDirectSubmit = reactExports.useCallback(
2081
- (_message) => {
2082
- if (directComposeRequest) {
2083
- clearDirectComposeRequest();
2084
- } else if (directPrefill) {
2085
- setDirectPrefill(null);
2086
- }
2087
- if (piOnboardingActive) {
2088
- endPiOnboarding();
2089
- }
2090
- },
2091
- [
2092
- clearDirectComposeRequest,
2093
- directComposeRequest,
2094
- directPrefill,
2095
- endPiOnboarding,
2096
- piOnboardingActive,
2097
- setDirectPrefill
2098
- ]
2099
- );
2100
- const resolvedDirectPrefill = reactExports.useMemo(() => {
2101
- if (externalPrefill) return externalPrefill;
2102
- if (directComposeRequest?.text?.trim()) {
2103
- return {
2104
- text: directComposeRequest.text,
2105
- focus: true,
2106
- token: directComposeRequest.token
2107
- };
2108
- }
2109
- if (directPrefill && directPrefill.trim()) {
2110
- return {
2111
- text: directPrefill,
2112
- focus: true,
2113
- token: Date.now()
2114
- };
2115
- }
2116
- return agentPrefill;
2117
- }, [agentPrefill, directComposeRequest, directPrefill, externalPrefill]);
2118
- reactExports.useEffect(() => {
2119
- const request = directComposeRequest;
2120
- if (!request || request.submitMode !== "auto") return;
2121
- if (mode !== "direct") return;
2122
- if (!copilotActions?.setComposerValue || !copilotActions.submitComposer) return;
2123
- if (copilotMeta?.isResponding) return;
2124
- if (autoSubmittedComposeTokenRef.current === request.token) return;
2125
- autoSubmittedComposeTokenRef.current = request.token;
2126
- copilotActions.setComposerValue(request.text, true);
2127
- const timer = window.setTimeout(() => {
2128
- copilotActions.submitComposer?.();
2129
- }, 30);
2130
- return () => {
2131
- window.clearTimeout(timer);
2132
- };
2133
- }, [copilotActions, copilotMeta?.isResponding, directComposeRequest, mode]);
2134
- reactExports.useEffect(() => {
2135
- if (mode !== "direct") return;
2136
- if (!copilotActions) return;
2137
- const key = activeAgent?.instance_id ?? "none";
2138
- const sessionId = directSessionId ?? activeAgent?.direct_session_id ?? currentSessionId ?? null;
2139
- if (sessionId) {
2140
- if (currentSessionId !== sessionId) {
2141
- copilotActions.clearThread?.();
2142
- copilotActions.setThreadId(sessionId);
2143
- }
2144
- resetKeyRef.current = key;
2145
- return;
2146
- }
2147
- if (resetKeyRef.current === key) return;
2148
- resetKeyRef.current = key;
2149
- copilotActions.setThreadId(null);
2150
- }, [
2151
- activeAgent?.direct_session_id,
2152
- activeAgent?.instance_id,
2153
- copilotActions,
2154
- currentSessionId,
2155
- directSessionId,
2156
- labReadOnly,
2157
- mode
2158
- ]);
2159
- const directMetadata = reactExports.useMemo(() => {
2160
- if (!activeAgent) return null;
2161
- const questOverride = piOnboardingActive && piOnboardingQuestId ? piOnboardingQuestId : null;
2162
- const logo = resolveAgentLogo(activeAgent, activeTemplate);
2163
- const selectionContext = toSelectionMetadata(selection);
2164
- return {
2165
- lab_mode: "direct",
2166
- quest_id: questOverride ?? activeAgent.active_quest_id ?? void 0,
2167
- quest_node_id: activeAgent.active_quest_node_id ?? void 0,
2168
- agent_id: activeAgent.agent_id,
2169
- agent_label: resolveAgentMentionLabel(activeAgent),
2170
- agent_display_name: resolveAgentDisplayName(activeAgent),
2171
- agent_logo: logo || null,
2172
- agent_avatar_color: activeAgent.avatar_frame_color ?? void 0,
2173
- agent_instance_id: activeAgent.instance_id,
2174
- cli_server_id: activeAgent.cli_server_id ?? cliServerId ?? void 0,
2175
- pi_onboarding_kind: piOnboardingActive ? piOnboardingKind ?? void 0 : void 0,
2176
- selection_context: selectionContext,
2177
- proposal_id: activeProposal?.proposal_id ?? void 0,
2178
- target_label: resolveAgentMentionLabel(activeAgent),
2179
- message_kind: selectionContext || activeProposal ? "user_control" : piOnboardingActive ? "pi_onboarding" : "text"
2180
- };
2181
- }, [
2182
- activeAgent,
2183
- activeTemplate,
2184
- activeProposal,
2185
- cliServerId,
2186
- piOnboardingActive,
2187
- piOnboardingKind,
2188
- piOnboardingQuestId,
2189
- selection
2190
- ]);
2191
- const controlReferenceFooter = reactExports.useMemo(
2192
- () => /* @__PURE__ */ jsxRuntimeExports.jsx(
2193
- LabControlReferenceChips,
2194
- {
2195
- selection,
2196
- proposal: activeProposal ? {
2197
- proposal_id: activeProposal.proposal_id,
2198
- action_type: activeProposal.action_type,
2199
- status: activeProposal.status
2200
- } : null,
2201
- onClearSelection: () => setSelection(null),
2202
- onClearProposal: () => setActiveProposal(null),
2203
- selectionPrefix: t("copilot_selection_chip", void 0, "Ref"),
2204
- proposalPrefix: t("copilot_proposal_chip", void 0, "Proposal")
2205
- }
2206
- ),
2207
- [activeProposal, selection, setActiveProposal, setSelection, t]
2208
- );
2209
- const EASE_OUT = [0, 0, 0.2, 1];
2210
- const modeMotion = allowMotion ? {
2211
- initial: { opacity: 0, y: 12 },
2212
- animate: { opacity: 1, y: 0 },
2213
- exit: { opacity: 0, y: -12 },
2214
- transition: { duration: 0.2, ease: EASE_OUT }
2215
- } : {
2216
- initial: false,
2217
- animate: { opacity: 1 },
2218
- exit: { opacity: 1 },
2219
- transition: { duration: 0 }
2220
- };
2221
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full min-h-0 flex-col", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2222
- cliReadOnly ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-[var(--lab-border)] px-5 py-2 text-xs text-[var(--lab-text-secondary)]", children: cliStatus === "unbound" ? "Bind an execution server to activate Lab Copilot." : "Your execution server is offline. Messages will be sent once it reconnects." }) : null,
2223
- /* @__PURE__ */ jsxRuntimeExports.jsxs(AnimatePresence, { mode: "wait", initial: false, children: [
2224
- mode === "direct" ? /* @__PURE__ */ jsxRuntimeExports.jsx(motion.div, { className: "flex flex-1 min-h-0 flex-col overflow-hidden", ...modeMotion, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-hidden", children: [
2225
- piOnboardingActive ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-pi-onboarding-banner", children: [
2226
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2227
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-pi-onboarding-title", children: "Chat with PI" }),
2228
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-pi-onboarding-subtitle", children: "Send your message to start the conversation." })
2229
- ] }),
2230
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: endPiOnboarding, children: "Exit" })
2231
- ] }) : null,
2232
- !activeAgent ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full flex-col items-center justify-center px-6 py-8", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-3xl", children: [
2233
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
2234
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2235
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-base font-semibold text-[var(--lab-text-primary)]", children: "Choose an agent" }),
2236
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-[var(--lab-text-secondary)]", children: "Select an agent to start a direct chat." })
2237
- ] }),
2238
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2239
- Button,
2240
- {
2241
- type: "button",
2242
- size: "sm",
2243
- className: "h-8 px-3 text-xs",
2244
- onClick: handleOpenRecruit,
2245
- disabled: labReadOnly,
2246
- children: "Recruit Agent"
2247
- }
2248
- )
2249
- ] }),
2250
- agents.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 rounded-xl border border-[var(--lab-border)] bg-[var(--lab-surface)] p-4 text-xs text-[var(--lab-text-secondary)]", children: "No agents yet. Recruit an agent to begin a direct chat." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2", children: agents.map((agent, index) => {
2251
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
2252
- const displayName = resolveAgentDisplayName(agent);
2253
- const mentionLabel = resolveAgentMentionLabel(agent);
2254
- const avatarColor = avatarColors.get(agent.instance_id) ?? pickAvatarFrameColor(agent.instance_id, index);
2255
- const logoPath = resolveAgentLogo(agent, template);
2256
- const statusLabel = typeof agent.status === "string" ? agent.status.toLowerCase() : "idle";
2257
- const isWorking = statusLabel !== "waiting" && isLabWorkingStatus(statusLabel);
2258
- const statusClass = isWorking ? "lab-status-dot-running" : statusLabel === "waiting" ? "lab-status-dot-busy" : "lab-status-dot-idle";
2259
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
2260
- "button",
2261
- {
2262
- type: "button",
2263
- className: "text-left",
2264
- onClick: () => handleSelectAgent(agent.instance_id),
2265
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-card lab-card-hover flex items-center gap-3 rounded-xl border border-[var(--lab-border)] bg-[var(--lab-surface)] p-3", children: [
2266
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-avatar lab-avatar-sm", children: [
2267
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-avatar-ring", style: { borderColor: avatarColor } }),
2268
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: logoPath, alt: displayName })
2269
- ] }),
2270
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
2271
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-semibold text-[var(--lab-text-primary)] truncate", children: displayName }),
2272
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-[var(--lab-text-secondary)] truncate", children: mentionLabel })
2273
- ] }),
2274
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-[10px] uppercase text-[var(--lab-text-muted)]", children: [
2275
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `lab-status-dot ${statusClass}` }),
2276
- isWorking ? "Working" : statusLabel === "waiting" ? "Waiting" : "Idle"
2277
- ] })
2278
- ] })
2279
- },
2280
- agent.instance_id
2281
- );
2282
- }) })
2283
- ] }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
2284
- LabDirectChatView,
2285
- {
2286
- projectId,
2287
- readOnly: labReadOnly,
2288
- prefill: resolvedDirectPrefill,
2289
- leadMessage: useStarterPrompt ? starterMessage : null,
2290
- mentionablesOverride,
2291
- defaultAgentOverride,
2292
- mentionsEnabledOverride: mentionsEnabled,
2293
- enforcedMentionPrefix,
2294
- lockedMentionPrefix,
2295
- messageMetadata: directMetadata ?? void 0,
2296
- composerFooter: controlReferenceFooter,
2297
- hideCopilotGreeting: useStarterPrompt,
2298
- busyOverride: activeAgentWorking,
2299
- onActionsChange: handleActionsChange,
2300
- onMetaChange: handleMetaChange,
2301
- onUserSubmit: handleDirectSubmit
2302
- }
2303
- )
2304
- ] }) }, "direct") : null,
2305
- mode === "group" ? /* @__PURE__ */ jsxRuntimeExports.jsx(motion.div, { className: "flex flex-1 min-h-0 flex-col overflow-hidden", ...modeMotion, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2306
- LabGroupChatView,
2307
- {
2308
- projectId,
2309
- quest: activeQuest,
2310
- quests,
2311
- agents,
2312
- templatesById,
2313
- readOnly: labReadOnly,
2314
- onQuestChange: setActiveQuest
2315
- }
2316
- ) }, "group") : null,
2317
- mode === "friends" ? /* @__PURE__ */ jsxRuntimeExports.jsx(motion.div, { className: "flex flex-1 min-h-0 flex-col overflow-hidden", ...modeMotion, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2318
- LabFriendsFeed,
2319
- {
2320
- projectId,
2321
- agents,
2322
- templatesById,
2323
- readOnly: labReadOnly,
2324
- quest: activeQuest,
2325
- quests,
2326
- onQuestChange: setActiveQuest
2327
- }
2328
- ) }, "friends") : null
2329
- ] })
2330
- ] }) });
2331
- }
2332
- const buildSearchSnippet = (content, query, maxLength = 90) => {
2333
- const trimmed = content.trim();
2334
- if (!trimmed) return "";
2335
- const normalizedQuery = query.trim().toLowerCase();
2336
- if (!normalizedQuery) return trimmed.slice(0, maxLength);
2337
- const lower = trimmed.toLowerCase();
2338
- const matchIndex = lower.indexOf(normalizedQuery);
2339
- if (matchIndex === -1) return trimmed.slice(0, maxLength);
2340
- const half = Math.floor(maxLength / 2);
2341
- const start = Math.max(0, matchIndex - half);
2342
- const end = Math.min(trimmed.length, matchIndex + normalizedQuery.length + half);
2343
- const prefix = start > 0 ? "..." : "";
2344
- const suffix = end < trimmed.length ? "..." : "";
2345
- return `${prefix}${trimmed.slice(start, end)}${suffix}`;
2346
- };
2347
- function LabCopilotOverflowMenu({
2348
- label,
2349
- entities,
2350
- emptyEntitiesLabel,
2351
- searchValue,
2352
- onSearchChange,
2353
- searchResults,
2354
- onSearchSelect,
2355
- searchPlaceholder,
2356
- questId,
2357
- quests,
2358
- onQuestChange,
2359
- canManageRoster,
2360
- rosterBusy,
2361
- availableAgents,
2362
- onAddAgents,
2363
- onRemoveAgent
2364
- }) {
2365
- const hasSearch = searchValue.trim().length > 0;
2366
- const manageEnabled = Boolean(canManageRoster && !rosterBusy);
2367
- const availableList = availableAgents ?? [];
2368
- const [addOpen, setAddOpen] = reactExports.useState(false);
2369
- const [selectedAgentIds, setSelectedAgentIds] = reactExports.useState(/* @__PURE__ */ new Set());
2370
- const [isAdding, setIsAdding] = reactExports.useState(false);
2371
- const [contextMenu, setContextMenu] = reactExports.useState(null);
2372
- reactExports.useEffect(() => {
2373
- if (addOpen) return;
2374
- setSelectedAgentIds(/* @__PURE__ */ new Set());
2375
- }, [addOpen]);
2376
- reactExports.useEffect(() => {
2377
- setSelectedAgentIds(/* @__PURE__ */ new Set());
2378
- }, [questId]);
2379
- reactExports.useEffect(() => {
2380
- if (!contextMenu) return;
2381
- const handleDismiss = () => setContextMenu(null);
2382
- const handleKey = (event) => {
2383
- if (event.key === "Escape") {
2384
- setContextMenu(null);
2385
- }
2386
- };
2387
- window.addEventListener("click", handleDismiss);
2388
- window.addEventListener("contextmenu", handleDismiss);
2389
- window.addEventListener("keydown", handleKey);
2390
- window.addEventListener("scroll", handleDismiss, true);
2391
- return () => {
2392
- window.removeEventListener("click", handleDismiss);
2393
- window.removeEventListener("contextmenu", handleDismiss);
2394
- window.removeEventListener("keydown", handleKey);
2395
- window.removeEventListener("scroll", handleDismiss, true);
2396
- };
2397
- }, [contextMenu]);
2398
- const toggleAgentSelection = reactExports.useCallback((agentId) => {
2399
- setSelectedAgentIds((prev) => {
2400
- const next = new Set(prev);
2401
- if (next.has(agentId)) {
2402
- next.delete(agentId);
2403
- } else {
2404
- next.add(agentId);
2405
- }
2406
- return next;
2407
- });
2408
- }, []);
2409
- const handleAddConfirm = reactExports.useCallback(async () => {
2410
- if (!manageEnabled || !onAddAgents) return;
2411
- const agentIds = Array.from(selectedAgentIds);
2412
- if (agentIds.length === 0) return;
2413
- setIsAdding(true);
2414
- try {
2415
- await onAddAgents(agentIds);
2416
- setAddOpen(false);
2417
- setSelectedAgentIds(/* @__PURE__ */ new Set());
2418
- } finally {
2419
- setIsAdding(false);
2420
- }
2421
- }, [manageEnabled, onAddAgents, selectedAgentIds]);
2422
- const handleContextMenu = reactExports.useCallback(
2423
- (event, agentId) => {
2424
- if (!manageEnabled || !onRemoveAgent) return;
2425
- event.preventDefault();
2426
- event.stopPropagation();
2427
- setContextMenu({ agentId, x: event.clientX, y: event.clientY });
2428
- },
2429
- [manageEnabled, onRemoveAgent]
2430
- );
2431
- const handleRemove = reactExports.useCallback(() => {
2432
- if (!contextMenu || !onRemoveAgent || !manageEnabled) return;
2433
- onRemoveAgent(contextMenu.agentId);
2434
- setContextMenu(null);
2435
- }, [contextMenu, manageEnabled, onRemoveAgent]);
2436
- const contextMenuPortal = contextMenu && typeof document !== "undefined" ? reactDomExports.createPortal(
2437
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-context-menu", style: { left: contextMenu.x, top: contextMenu.y }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: handleRemove, disabled: !manageEnabled, children: "Remove from quest" }) }),
2438
- document.body
2439
- ) : null;
2440
- const canShowAdd = Boolean(onAddAgents);
2441
- const addDisabled = !manageEnabled || availableList.length === 0;
2442
- const addHint = !questId ? "Select a quest to add agents." : availableList.length === 0 ? "All available agents are already in this quest." : "Add agents to this quest.";
2443
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Popover, { modal: false, children: [
2444
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2445
- "button",
2446
- {
2447
- type: "button",
2448
- className: "ds-copilot-icon-btn lab-copilot-overflow-trigger",
2449
- "aria-label": `${label} options`,
2450
- "data-tooltip": `${label} options`,
2451
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Ellipsis, { size: 16 })
2452
- }
2453
- ) }),
2454
- /* @__PURE__ */ jsxRuntimeExports.jsxs(PopoverContent, { align: "end", sideOffset: 12, className: "lab-copilot-menu", children: [
2455
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-menu-section", children: [
2456
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-menu-title", children: [
2457
- label,
2458
- " roster"
2459
- ] }),
2460
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-entity-list", children: [
2461
- entities.map((entity) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2462
- "div",
2463
- {
2464
- className: cn("lab-copilot-entity", manageEnabled && "is-manageable"),
2465
- onContextMenu: (event) => handleContextMenu(event, entity.id),
2466
- children: [
2467
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-avatar lab-avatar-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: entity.avatar, alt: entity.name }) }),
2468
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-copilot-entity-name", title: entity.name, children: entity.name })
2469
- ]
2470
- },
2471
- entity.id
2472
- )),
2473
- canShowAdd ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2474
- "button",
2475
- {
2476
- type: "button",
2477
- className: "lab-copilot-entity lab-copilot-entity-add",
2478
- onClick: () => setAddOpen(true),
2479
- disabled: addDisabled,
2480
- title: addHint,
2481
- "aria-label": "Add agents",
2482
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 14 })
2483
- }
2484
- ) : null
2485
- ] }),
2486
- !entities.length ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-muted", children: emptyEntitiesLabel }) : null
2487
- ] }),
2488
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-menu-section", children: [
2489
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-title", children: "Search chat" }),
2490
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-search", children: [
2491
- /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 14 }),
2492
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2493
- "input",
2494
- {
2495
- type: "text",
2496
- value: searchValue,
2497
- onChange: (event) => onSearchChange(event.target.value),
2498
- placeholder: searchPlaceholder,
2499
- className: "lab-copilot-search-input"
2500
- }
2501
- )
2502
- ] }),
2503
- hasSearch ? searchResults.length ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-search-results", children: searchResults.map((result) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2504
- "button",
2505
- {
2506
- type: "button",
2507
- className: "lab-copilot-search-result",
2508
- onClick: () => onSearchSelect(result),
2509
- children: [
2510
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-search-title", children: result.title }),
2511
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-search-excerpt", children: result.excerpt })
2512
- ]
2513
- },
2514
- result.id
2515
- )) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-muted", children: "No matches found." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-muted", children: "Type to search this chat." })
2516
- ] }),
2517
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-copilot-menu-section", children: [
2518
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-title", children: "Switch quest" }),
2519
- quests.length ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Select, { value: questId ?? "", onValueChange: (value) => onQuestChange(value || null), children: [
2520
- /* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, { className: "lab-copilot-menu-select", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, { placeholder: "Select quest" }) }),
2521
- /* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, { children: quests.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: item.quest_id, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block max-w-[var(--radix-select-trigger-width)] line-clamp-2", children: resolveQuestLabel(item) }) }, item.quest_id)) })
2522
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-menu-muted", children: "No quests available." })
2523
- ] })
2524
- ] }),
2525
- contextMenuPortal,
2526
- canShowAdd ? /* @__PURE__ */ jsxRuntimeExports.jsx(Dialog, { open: addOpen, onOpenChange: setAddOpen, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { className: "lab-copilot-add-dialog", showCloseButton: true, children: [
2527
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogHeader, { children: [
2528
- /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { className: "lab-copilot-add-title", children: "Add agents" }),
2529
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-add-subtitle", children: questId ? "Select agents to join this quest." : "Select a quest first to add agents." })
2530
- ] }),
2531
- availableList.length ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-add-list", children: availableList.map((agent) => {
2532
- const selected = selectedAgentIds.has(agent.id);
2533
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
2534
- "button",
2535
- {
2536
- type: "button",
2537
- className: cn("lab-copilot-add-row", selected && "is-selected"),
2538
- onClick: () => toggleAgentSelection(agent.id),
2539
- children: [
2540
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-copilot-add-check", children: selected ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12 }) : null }),
2541
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-avatar lab-avatar-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: agent.avatar, alt: agent.name }) }),
2542
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-copilot-add-name", title: agent.name, children: agent.name })
2543
- ]
2544
- },
2545
- agent.id
2546
- );
2547
- }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-copilot-add-empty", children: questId ? "No available agents to add right now." : "Select a quest to see available agents." }),
2548
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogFooter, { className: "lab-copilot-add-footer", children: [
2549
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2550
- Button,
2551
- {
2552
- variant: "outline",
2553
- size: "sm",
2554
- className: "min-h-[40px] px-4 text-xs",
2555
- onClick: () => setAddOpen(false),
2556
- children: "Cancel"
2557
- }
2558
- ),
2559
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2560
- Button,
2561
- {
2562
- size: "sm",
2563
- className: "min-h-[40px] px-4 text-xs",
2564
- onClick: handleAddConfirm,
2565
- disabled: addDisabled || selectedAgentIds.size === 0 || isAdding,
2566
- children: isAdding ? "Adding..." : "Add to quest"
2567
- }
2568
- )
2569
- ] })
2570
- ] }) }) : null
2571
- ] });
2572
- }
2573
- function LabGroupChatView({
2574
- projectId,
2575
- quest,
2576
- quests,
2577
- agents,
2578
- templatesById,
2579
- readOnly,
2580
- onQuestChange
2581
- }) {
2582
- const { t } = useI18n("lab");
2583
- const { addToast } = useToast();
2584
- const queryClient = useQueryClient();
2585
- const [input, setInput] = reactExports.useState("");
2586
- const [attachments, setAttachments] = reactExports.useState([]);
2587
- const [rosterBusy, setRosterBusy] = reactExports.useState(false);
2588
- const [highlightedMessageId, setHighlightedMessageId] = reactExports.useState(null);
2589
- const [messageContextMenu, setMessageContextMenu] = reactExports.useState(null);
2590
- const lastSeenMessageIdRef = reactExports.useRef(null);
2591
- const scrollRef = reactExports.useRef(null);
2592
- const [follow, setFollow] = reactExports.useState(true);
2593
- const groupPrefill = useLabCopilotStore((state) => state.groupPrefill);
2594
- const setGroupPrefill = useLabCopilotStore((state) => state.setGroupPrefill);
2595
- const selection = useLabGraphSelectionStore((state) => state.selection);
2596
- const activeProposal = useLabGraphSelectionStore((state) => state.activeProposal);
2597
- const setSelection = useLabGraphSelectionStore((state) => state.setSelection);
2598
- const setActiveProposal = useLabGraphSelectionStore((state) => state.setActiveProposal);
2599
- const setMode = useLabCopilotStore((state) => state.setMode);
2600
- const setActiveAgent = useLabCopilotStore((state) => state.setActiveAgent);
2601
- const setSessionIdForSurface = useChatSessionStore((state) => state.setSessionIdForSurface);
2602
- const user = useAuthStore((state) => state.user);
2603
- const headerPortalTarget = useCopilotDockHeaderPortal();
2604
- const [searchQuery, setSearchQuery] = reactExports.useState("");
2605
- const questId = quest?.quest_id ?? null;
2606
- const {
2607
- sessionId,
2608
- messages,
2609
- connection,
2610
- sendMessage,
2611
- historyTruncated,
2612
- historyLimit,
2613
- historyLoadingFull,
2614
- historyLoading,
2615
- hasLoadedOnce,
2616
- loadFullHistory
2617
- } = useLabSurfaceSession({
2618
- projectId,
2619
- questId,
2620
- surface: "group",
2621
- enabled: Boolean(questId)
2622
- });
2623
- const agentsById = reactExports.useMemo(() => new Map(agents.map((agent) => [agent.instance_id, agent])), [agents]);
2624
- const questAgents = reactExports.useMemo(() => {
2625
- if (!questId) return [];
2626
- return agents.filter((agent) => agent.active_quest_id === questId);
2627
- }, [agents, questId]);
2628
- const questAgentIds = reactExports.useMemo(
2629
- () => questAgents.map((agent) => agent.instance_id),
2630
- [questAgents]
2631
- );
2632
- const mentionLookup = reactExports.useMemo(() => {
2633
- const map = /* @__PURE__ */ new Map();
2634
- questAgents.forEach((agent) => {
2635
- const mentionLabel = resolveAgentMentionLabel(agent);
2636
- const candidates = /* @__PURE__ */ new Set();
2637
- if (mentionLabel) {
2638
- candidates.add(mentionLabel);
2639
- candidates.add(mentionLabel.replace(/^@/, ""));
2640
- }
2641
- if (agent.agent_id) {
2642
- candidates.add(agent.agent_id);
2643
- }
2644
- if (agent.display_name) {
2645
- candidates.add(agent.display_name);
2646
- }
2647
- candidates.forEach((candidate) => {
2648
- const normalized = candidate.trim().toLowerCase();
2649
- if (!normalized) return;
2650
- if (!map.has(normalized)) {
2651
- map.set(normalized, agent.instance_id);
2652
- }
2653
- });
2654
- });
2655
- return map;
2656
- }, [questAgents]);
2657
- const mentionables = reactExports.useMemo(() => {
2658
- if (!questId) return [];
2659
- const allDescriptor = {
2660
- id: "all",
2661
- label: "@ALL",
2662
- description: "Notify all agents in this quest",
2663
- role: "broadcast",
2664
- source: "lab"
2665
- };
2666
- const entries = questAgents.map((agent) => {
2667
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
2668
- return buildAgentDescriptor(agent, template);
2669
- });
2670
- return [allDescriptor, ...entries];
2671
- }, [questAgents, questId, templatesById]);
2672
- const menuEntities = reactExports.useMemo(() => {
2673
- return questAgents.map((agent) => {
2674
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
2675
- return {
2676
- id: agent.instance_id,
2677
- name: resolveAgentDisplayName(agent),
2678
- avatar: resolveAgentLogo(agent, template)
2679
- };
2680
- });
2681
- }, [questAgents, templatesById]);
2682
- const availableAgents = reactExports.useMemo(() => {
2683
- if (!questId) return [];
2684
- return agents.filter((agent) => agent.active_quest_id !== questId).map((agent) => {
2685
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
2686
- return {
2687
- id: agent.instance_id,
2688
- name: resolveAgentDisplayName(agent),
2689
- avatar: resolveAgentLogo(agent, template)
2690
- };
2691
- });
2692
- }, [agents, questId, templatesById]);
2693
- const emptyStateLabel = reactExports.useMemo(() => {
2694
- if (quests.length === 0) return "Create a quest to get started.";
2695
- return "Select a quest to start collaborating.";
2696
- }, [quests.length]);
2697
- const emptyRosterLabel = questId ? "No agents assigned to this quest yet." : "Select a quest to see its agents.";
2698
- const connectionStatus = reactExports.useMemo(() => {
2699
- if (connection.status === "rate_limited") return "Rate limited. Retrying...";
2700
- if (connection.status === "reconnecting") return "Reconnecting...";
2701
- if (connection.status === "error") return connection.error || "Connection error";
2702
- return null;
2703
- }, [connection.error, connection.status]);
2704
- const showLoadFullHistory = historyTruncated && Boolean(sessionId);
2705
- const showHistoryLoadingOverlay = Boolean(questId) && historyLoading && messages.length === 0 && !hasLoadedOnce;
2706
- const historyLabel = typeof historyLimit === "number" && historyLimit > 0 ? `Showing latest ${historyLimit} messages.` : "Showing recent messages.";
2707
- reactExports.useMemo(() => {
2708
- const raw = user?.username || user?.email || user?.id || "You";
2709
- const trimmed = raw?.trim?.() ?? "";
2710
- return trimmed || "You";
2711
- }, [user?.email, user?.id, user?.username]);
2712
- reactExports.useEffect(() => {
2713
- setSearchQuery("");
2714
- setHighlightedMessageId(null);
2715
- lastSeenMessageIdRef.current = null;
2716
- setFollow(true);
2717
- setInput("");
2718
- }, [questId]);
2719
- reactExports.useEffect(() => {
2720
- if (!messageContextMenu) return;
2721
- const handleDismiss = () => setMessageContextMenu(null);
2722
- const handleKey = (event) => {
2723
- if (event.key === "Escape") {
2724
- setMessageContextMenu(null);
2725
- }
2726
- };
2727
- window.addEventListener("click", handleDismiss);
2728
- window.addEventListener("contextmenu", handleDismiss);
2729
- window.addEventListener("keydown", handleKey);
2730
- window.addEventListener("scroll", handleDismiss, true);
2731
- return () => {
2732
- window.removeEventListener("click", handleDismiss);
2733
- window.removeEventListener("contextmenu", handleDismiss);
2734
- window.removeEventListener("keydown", handleKey);
2735
- window.removeEventListener("scroll", handleDismiss, true);
2736
- };
2737
- }, [messageContextMenu]);
2738
- reactExports.useEffect(() => {
2739
- if (!groupPrefill) return;
2740
- if (!input.trim()) {
2741
- setInput(groupPrefill.trim() ? `${groupPrefill} ` : "");
2742
- }
2743
- setGroupPrefill(null);
2744
- }, [groupPrefill, input, setGroupPrefill]);
2745
- const stripLeadingMentions = reactExports.useCallback((raw) => {
2746
- const trimmed = raw.trim();
2747
- if (!trimmed) return "";
2748
- const parts = trimmed.split(/\s+/);
2749
- let index = 0;
2750
- while (index < parts.length) {
2751
- const token = parts[index];
2752
- if (!token || !token.startsWith("@") || token.length === 1) break;
2753
- index += 1;
2754
- }
2755
- return parts.slice(index).join(" ").trim();
2756
- }, []);
2757
- const parseGroupInput = reactExports.useCallback(
2758
- (raw) => {
2759
- const text = raw.trim();
2760
- const mentionPattern = /@([A-Za-z0-9_.-]+)/g;
2761
- const targets = /* @__PURE__ */ new Set();
2762
- const missing = /* @__PURE__ */ new Set();
2763
- let hasAll = false;
2764
- let match;
2765
- while ((match = mentionPattern.exec(text)) !== null) {
2766
- const token = match[1]?.trim();
2767
- if (!token) continue;
2768
- const lowered = token.toLowerCase();
2769
- if (lowered === "all") {
2770
- hasAll = true;
2771
- continue;
2772
- }
2773
- const directKey = lowered.startsWith("@") ? lowered : `@${lowered}`;
2774
- const targetId = mentionLookup.get(directKey) ?? mentionLookup.get(lowered);
2775
- if (targetId) {
2776
- targets.add(targetId);
2777
- } else {
2778
- missing.add(`@${token}`);
2779
- }
2780
- }
2781
- if (hasAll) {
2782
- questAgentIds.forEach((agentId) => targets.add(agentId));
2783
- missing.clear();
2784
- }
2785
- const stripped = stripLeadingMentions(text);
2786
- const content = stripped || (targets.size > 0 || hasAll ? "" : text);
2787
- return {
2788
- content,
2789
- targets: Array.from(targets),
2790
- missing: Array.from(missing)
2791
- };
2792
- },
2793
- [mentionLookup, questAgentIds, stripLeadingMentions]
2794
- );
2795
- const resolveMentionLabel = reactExports.useCallback(
2796
- (agentInstanceId) => {
2797
- const agent = agentsById.get(agentInstanceId);
2798
- if (!agent) return "";
2799
- return resolveAgentMentionLabel(agent);
2800
- },
2801
- [agentsById]
2802
- );
2803
- const handleSend = async () => {
2804
- if (!questId || !input.trim() || readOnly || !sessionId) return;
2805
- try {
2806
- const parsed = parseGroupInput(input);
2807
- if (parsed.missing.length > 0) {
2808
- addToast({
2809
- type: "error",
2810
- title: "Unknown agent",
2811
- description: `Unknown mention: ${parsed.missing.join(", ")}`
2812
- });
2813
- return;
2814
- }
2815
- if (parsed.targets.length === 0) {
2816
- addToast({
2817
- type: "error",
2818
- title: "Mention required",
2819
- description: "Mention a quest agent to start a group request."
2820
- });
2821
- return;
2822
- }
2823
- setFollow(true);
2824
- await sendMessage({
2825
- sessionId,
2826
- message: parsed.content,
2827
- surface: "lab-group",
2828
- mentionTargets: parsed.targets,
2829
- metadata: {
2830
- selection_context: toSelectionMetadata(selection),
2831
- proposal_id: activeProposal?.proposal_id ?? void 0,
2832
- message_kind: activeProposal || selection ? "user_control" : parsed.targets.length > 0 ? "group_request" : "text",
2833
- target_label: parsed.targets.length === 1 ? resolveMentionLabel(parsed.targets[0]) || void 0 : void 0
2834
- }
2835
- });
2836
- setInput("");
2837
- setAttachments([]);
2838
- } catch {
2839
- addToast({
2840
- type: "error",
2841
- title: "Message failed",
2842
- description: "Message couldn't be sent. Please check your connection and try again."
2843
- });
2844
- }
2845
- };
2846
- reactExports.useEffect(() => {
2847
- const latest = messages[messages.length - 1];
2848
- if (!latest?.id) return;
2849
- if (!lastSeenMessageIdRef.current) {
2850
- lastSeenMessageIdRef.current = latest.id;
2851
- return;
2852
- }
2853
- if (latest.id !== lastSeenMessageIdRef.current) {
2854
- lastSeenMessageIdRef.current = latest.id;
2855
- setHighlightedMessageId(latest.id);
2856
- const timer = window.setTimeout(() => setHighlightedMessageId(null), 1200);
2857
- return () => window.clearTimeout(timer);
2858
- }
2859
- }, [messages]);
2860
- const resolveGroupMentionPrefix = reactExports.useCallback(
2861
- (message) => {
2862
- const metadata = getMessageMetadata(message);
2863
- const targets = Array.isArray(metadata?.mention_targets) ? metadata?.mention_targets : [];
2864
- if (!targets.length) return "";
2865
- const labels = targets.map((agentId) => resolveMentionLabel(agentId)).filter((label) => Boolean(label));
2866
- if (!labels.length) return "";
2867
- return Array.from(new Set(labels)).join(" ");
2868
- },
2869
- [resolveMentionLabel]
2870
- );
2871
- const resolveGroupDisplayText = reactExports.useCallback(
2872
- (message) => {
2873
- const base = getMessageText(message);
2874
- if (getMessageRole(message) !== "user") return base;
2875
- const prefix = resolveGroupMentionPrefix(message);
2876
- if (!prefix) return base;
2877
- return base ? `${prefix} ${base}` : prefix;
2878
- },
2879
- [resolveGroupMentionPrefix]
2880
- );
2881
- const resolveGroupDisplayMessage = reactExports.useCallback(
2882
- (message) => {
2883
- if (getMessageRole(message) !== "user") return message;
2884
- const base = getMessageText(message);
2885
- const nextText = resolveGroupDisplayText(message);
2886
- if (!nextText || nextText === base) return message;
2887
- const content = message.content;
2888
- if (!content || typeof content !== "object" || typeof content.content !== "string") {
2889
- return message;
2890
- }
2891
- return {
2892
- ...message,
2893
- content: {
2894
- ...content,
2895
- content: nextText
2896
- }
2897
- };
2898
- },
2899
- [resolveGroupDisplayText]
2900
- );
2901
- const messageContentById = reactExports.useMemo(() => {
2902
- const map = /* @__PURE__ */ new Map();
2903
- messages.forEach((message) => {
2904
- const content = resolveGroupDisplayText(message);
2905
- if (content) {
2906
- map.set(message.id, content);
2907
- }
2908
- });
2909
- return map;
2910
- }, [messages, resolveGroupDisplayText]);
2911
- const messagesById = reactExports.useMemo(() => {
2912
- return new Map(messages.map((message) => [message.id, message]));
2913
- }, [messages]);
2914
- const messageContentByGroupId = reactExports.useMemo(() => {
2915
- const map = /* @__PURE__ */ new Map();
2916
- messages.forEach((message) => {
2917
- const metadata = getMessageMetadata(message);
2918
- const groupId = typeof metadata?.group_message_id === "string" ? metadata.group_message_id : "";
2919
- if (!groupId) return;
2920
- const content = resolveGroupDisplayText(message);
2921
- if (content) {
2922
- map.set(groupId, content);
2923
- }
2924
- });
2925
- return map;
2926
- }, [messages, resolveGroupDisplayText]);
2927
- const messagesByGroupId = reactExports.useMemo(() => {
2928
- const map = /* @__PURE__ */ new Map();
2929
- messages.forEach((message) => {
2930
- const metadata = getMessageMetadata(message);
2931
- const groupId = typeof metadata?.group_message_id === "string" ? metadata.group_message_id : "";
2932
- if (groupId) {
2933
- map.set(groupId, message);
2934
- }
2935
- });
2936
- return map;
2937
- }, [messages]);
2938
- const resolveGroupQuote = reactExports.useCallback(
2939
- (message) => {
2940
- const metadata = getMessageMetadata(message);
2941
- if (!metadata) return null;
2942
- const metaRecord = metadata;
2943
- const context = metaRecord.context && typeof metaRecord.context === "object" ? metaRecord.context : null;
2944
- const replyToMessageId = typeof metaRecord.reply_to_message_id === "string" && metaRecord.reply_to_message_id || typeof metaRecord.quote_message_id === "string" && metaRecord.quote_message_id || typeof context?.reply_to_message_id === "string" && context.reply_to_message_id || typeof context?.quote_message_id === "string" && context.quote_message_id || "";
2945
- const rawSnapshot = metaRecord.quote_snapshot && typeof metaRecord.quote_snapshot === "object" ? metaRecord.quote_snapshot : context?.quote_snapshot && typeof context.quote_snapshot === "object" ? context.quote_snapshot : null;
2946
- if (!replyToMessageId && !rawSnapshot) return null;
2947
- const snapshotMessageId = rawSnapshot && typeof rawSnapshot.group_message_id === "string" ? rawSnapshot.group_message_id : "";
2948
- const lookupMessageId = replyToMessageId || snapshotMessageId;
2949
- const quotedMessage = lookupMessageId ? messagesByGroupId.get(lookupMessageId) ?? null : null;
2950
- const snapshotContent = rawSnapshot && typeof rawSnapshot.content === "string" ? rawSnapshot.content : "";
2951
- const fallbackContent = lookupMessageId ? messageContentByGroupId.get(lookupMessageId) ?? "" : "";
2952
- const quoteContent = (snapshotContent || fallbackContent).replace(/\s+/g, " ").trim();
2953
- if (!quoteContent) return null;
2954
- let sender = "";
2955
- if (quotedMessage) {
2956
- sender = resolveMessageTitle(quotedMessage);
2957
- } else if (rawSnapshot) {
2958
- sender = typeof rawSnapshot.sender_name === "string" && rawSnapshot.sender_name || typeof rawSnapshot.agent_display_name === "string" && rawSnapshot.agent_display_name || typeof rawSnapshot.sender_label === "string" && rawSnapshot.sender_label || typeof rawSnapshot.agent_label === "string" && rawSnapshot.agent_label || "";
2959
- }
2960
- if (!sender) sender = "User";
2961
- return { sender, content: quoteContent };
2962
- },
2963
- [messageContentByGroupId, messagesByGroupId]
2964
- );
2965
- const handleAvatarContextMenu = reactExports.useCallback(
2966
- (event, message) => {
2967
- const metadata = getMessageMetadata(message);
2968
- const agentId = metadata?.agent_instance_id ?? metadata?.sender_instance_id;
2969
- const sessionId2 = metadata?.session_id;
2970
- if (!agentId && !sessionId2) return;
2971
- setMessageContextMenu({ messageId: message.id, x: event.clientX, y: event.clientY });
2972
- },
2973
- []
2974
- );
2975
- const contextTarget = messageContextMenu ? messagesById.get(messageContextMenu.messageId) ?? null : null;
2976
- const contextMetadata = contextTarget ? getMessageMetadata(contextTarget) : null;
2977
- const contextAgentId = contextMetadata?.agent_instance_id ?? null;
2978
- const contextSessionId = typeof contextMetadata?.session_id === "string" ? contextMetadata.session_id : null;
2979
- const canOpenDirect = Boolean(contextAgentId);
2980
- const handleOpenDirect = reactExports.useCallback(() => {
2981
- if (!contextAgentId) return;
2982
- setActiveAgent(contextAgentId);
2983
- if (contextSessionId) {
2984
- setSessionIdForSurface(projectId, "lab-direct", contextSessionId);
2985
- }
2986
- setMode("direct");
2987
- setMessageContextMenu(null);
2988
- }, [contextAgentId, contextSessionId, projectId, setActiveAgent, setMode, setSessionIdForSurface]);
2989
- const scrollToBottom = reactExports.useCallback(() => {
2990
- const node = scrollRef.current;
2991
- if (!node) return;
2992
- if (typeof node.scrollTo === "function") {
2993
- node.scrollTo({ top: node.scrollHeight, behavior: "auto" });
2994
- } else {
2995
- node.scrollTop = node.scrollHeight;
2996
- }
2997
- }, []);
2998
- const handleScroll = reactExports.useCallback(
2999
- (event) => {
3000
- const target = event.currentTarget;
3001
- const nextFollow = isScrolledToBottom(target);
3002
- setFollow((prev) => prev === nextFollow ? prev : nextFollow);
3003
- },
3004
- [isScrolledToBottom]
3005
- );
3006
- reactExports.useCallback(() => {
3007
- setFollow(false);
3008
- loadFullHistory();
3009
- }, [loadFullHistory]);
3010
- reactExports.useEffect(() => {
3011
- if (!questId || messages.length === 0) return;
3012
- if (!follow) return;
3013
- window.requestAnimationFrame(() => scrollToBottom());
3014
- }, [follow, messages, questId, scrollToBottom]);
3015
- const listOffset = showLoadFullHistory ? 1 : 0;
3016
- const listCount = messages.length + listOffset;
3017
- const shouldVirtualize = listCount > 20;
3018
- const rowVirtualizer = useVirtualizer({
3019
- count: listCount,
3020
- getScrollElement: () => scrollRef.current,
3021
- estimateSize: (index) => showLoadFullHistory && index === 0 ? 56 : 160,
3022
- getItemKey: (index) => {
3023
- if (showLoadFullHistory && index === 0) return "lab-group-history-banner";
3024
- const message = messages[index - listOffset];
3025
- return message?.id ?? `lab-group-${index}`;
3026
- },
3027
- overscan: 6
3028
- });
3029
- const groupSearchResults = reactExports.useMemo(() => {
3030
- const query = searchQuery.trim().toLowerCase();
3031
- if (!query) return [];
3032
- const results = [];
3033
- messages.forEach((item, index) => {
3034
- const content = messageContentById.get(item.id) ?? "";
3035
- if (!content) return;
3036
- if (!content.toLowerCase().includes(query)) return;
3037
- const title = resolveMessageTitle(item);
3038
- results.push({
3039
- id: item.id,
3040
- index,
3041
- title,
3042
- excerpt: buildSearchSnippet(content, query)
3043
- });
3044
- });
3045
- return results;
3046
- }, [messageContentById, messages, searchQuery]);
3047
- const handleAddAgents = reactExports.useCallback(
3048
- async (agentIds) => {
3049
- if (!questId || readOnly || agentIds.length === 0) return;
3050
- setRosterBusy(true);
3051
- try {
3052
- const results = await Promise.allSettled(
3053
- agentIds.map(
3054
- (agentId) => assignLabAgent(projectId, agentId, { quest_id: questId, quest_node_id: null })
3055
- )
3056
- );
3057
- const failed = [];
3058
- results.forEach((result, index) => {
3059
- if (result.status !== "fulfilled") {
3060
- failed.push(agentIds[index]);
3061
- }
3062
- });
3063
- if (failed.length) {
3064
- const names = failed.map((agentId) => {
3065
- const agent = agentsById.get(agentId);
3066
- return agent ? resolveAgentDisplayName(agent) : agentId;
3067
- });
3068
- addToast({
3069
- type: "error",
3070
- title: "Unable to add agents",
3071
- description: `Failed to add: ${names.join(", ")}`
3072
- });
3073
- }
3074
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
3075
- } finally {
3076
- setRosterBusy(false);
3077
- }
3078
- },
3079
- [addToast, agentsById, projectId, queryClient, questId, readOnly]
3080
- );
3081
- const handleRemoveAgent = reactExports.useCallback(
3082
- async (agentId) => {
3083
- if (!questId || readOnly) return;
3084
- setRosterBusy(true);
3085
- try {
3086
- await assignLabAgent(projectId, agentId, { quest_id: null, quest_node_id: null });
3087
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
3088
- } catch (error) {
3089
- const agent = agentsById.get(agentId);
3090
- const label = agent ? resolveAgentDisplayName(agent) : agentId;
3091
- addToast({
3092
- type: "error",
3093
- title: "Unable to remove agent",
3094
- description: `Failed to remove ${label} from this quest.`
3095
- });
3096
- } finally {
3097
- setRosterBusy(false);
3098
- }
3099
- },
3100
- [addToast, agentsById, projectId, queryClient, questId, readOnly]
3101
- );
3102
- const flashMessageHighlight = reactExports.useCallback((messageId) => {
3103
- setHighlightedMessageId(messageId);
3104
- if (typeof window !== "undefined") {
3105
- window.setTimeout(() => setHighlightedMessageId(null), 1200);
3106
- }
3107
- }, []);
3108
- const jumpToGroupResult = reactExports.useCallback(
3109
- (result) => {
3110
- if (!result) return;
3111
- if (!scrollRef.current) return;
3112
- setFollow(false);
3113
- if (shouldVirtualize) {
3114
- rowVirtualizer.scrollToIndex(result.index, { align: "center" });
3115
- } else {
3116
- const node = scrollRef.current?.querySelector(
3117
- `[data-group-message-id="${result.id}"]`
3118
- );
3119
- if (node) {
3120
- node.scrollIntoView({ behavior: "smooth", block: "center" });
3121
- }
3122
- }
3123
- flashMessageHighlight(result.id);
3124
- },
3125
- [flashMessageHighlight, listOffset, rowVirtualizer, shouldVirtualize]
3126
- );
3127
- reactExports.useEffect(() => {
3128
- if (!searchQuery.trim()) return;
3129
- if (groupSearchResults.length === 0) return;
3130
- jumpToGroupResult(groupSearchResults[0]);
3131
- }, [groupSearchResults, jumpToGroupResult, searchQuery]);
3132
- const menuPortal = headerPortalTarget ? reactDomExports.createPortal(
3133
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3134
- LabCopilotOverflowMenu,
3135
- {
3136
- label: "Group",
3137
- entities: menuEntities,
3138
- emptyEntitiesLabel: emptyRosterLabel,
3139
- searchValue: searchQuery,
3140
- onSearchChange: setSearchQuery,
3141
- searchResults: groupSearchResults,
3142
- onSearchSelect: jumpToGroupResult,
3143
- searchPlaceholder: "Search group chat",
3144
- questId,
3145
- quests,
3146
- onQuestChange,
3147
- canManageRoster: !readOnly && Boolean(questId),
3148
- rosterBusy,
3149
- availableAgents,
3150
- onAddAgents: handleAddAgents,
3151
- onRemoveAgent: handleRemoveAgent
3152
- }
3153
- ),
3154
- headerPortalTarget
3155
- ) : null;
3156
- const messageContextMenuPortal = messageContextMenu && typeof document !== "undefined" ? reactDomExports.createPortal(
3157
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3158
- "div",
3159
- {
3160
- className: "lab-copilot-context-menu",
3161
- style: { left: messageContextMenu.x, top: messageContextMenu.y },
3162
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: handleOpenDirect, disabled: !canOpenDirect, children: "Open direct session" })
3163
- }
3164
- ),
3165
- document.body
3166
- ) : null;
3167
- const renderGroupMessage = (message) => {
3168
- const displayMessage = resolveGroupDisplayMessage(message);
3169
- const content = displayMessage.content;
3170
- const displayStreaming = message.type === "text_delta" && content.status === "in_progress";
3171
- const isHighlighted = highlightedMessageId === message.id;
3172
- const role = getMessageRole(displayMessage);
3173
- const metadata = getMessageMetadata(displayMessage);
3174
- const timestampLabel = role === "assistant" ? formatAbsoluteTimestamp(content?.timestamp) : "";
3175
- const quote = resolveGroupQuote(displayMessage);
3176
- const senderLabel = typeof metadata?.sender_label === "string" ? metadata.sender_label : typeof metadata?.agent_label === "string" ? metadata.agent_label : null;
3177
- const targetLabel = typeof metadata?.target_label === "string" ? metadata.target_label : null;
3178
- const replyState = typeof metadata?.reply_state === "string" ? metadata.reply_state : null;
3179
- const replyStateLabel = resolveReplyStateLabel(t, replyState);
3180
- const proposalId = typeof metadata?.proposal_id === "string" ? metadata.proposal_id : null;
3181
- const selectionLabel = resolveSelectionLabel(
3182
- metadata?.selection_context && typeof metadata.selection_context === "object" ? {
3183
- ...metadata.selection_context,
3184
- label: typeof metadata.selection_context.branch_name === "string" ? metadata.selection_context.branch_name : void 0
3185
- } : null
3186
- );
3187
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
3188
- "div",
3189
- {
3190
- className: cn("rounded-[12px] px-1", isHighlighted && "lab-message-highlight"),
3191
- "data-group-message-id": message.id,
3192
- children: [
3193
- senderLabel || targetLabel || replyStateLabel || proposalId || selectionLabel ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-2 flex flex-wrap items-center gap-2 px-2", children: [
3194
- senderLabel || targetLabel ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center rounded-full border border-[var(--lab-border)] bg-[var(--lab-surface)] px-2 py-0.5 text-[10px] font-medium text-[var(--lab-text-secondary)]", children: [
3195
- senderLabel || t("copilot_group_sender_fallback", void 0, "Agent"),
3196
- targetLabel ? ` → ${targetLabel}` : ""
3197
- ] }) : null,
3198
- replyStateLabel ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex items-center rounded-full border border-[rgba(83,176,174,0.2)] bg-[rgba(83,176,174,0.1)] px-2 py-0.5 text-[10px] font-medium text-[var(--lab-text-secondary)]", children: replyStateLabel }) : null,
3199
- proposalId ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center rounded-full border border-[var(--lab-border)] bg-[var(--lab-background)] px-2 py-0.5 text-[10px] text-[var(--lab-text-secondary)]", children: [
3200
- t("copilot_group_proposal_badge", void 0, "Proposal"),
3201
- " · ",
3202
- proposalId.slice(0, 8)
3203
- ] }) : null,
3204
- selectionLabel ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex items-center rounded-full border border-[var(--lab-border)] bg-[var(--lab-background)] px-2 py-0.5 text-[10px] text-[var(--lab-text-secondary)]", children: selectionLabel }) : null
3205
- ] }) : null,
3206
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3207
- ChatMessage,
3208
- {
3209
- message: displayMessage,
3210
- compact: true,
3211
- onAvatarContextMenu: handleAvatarContextMenu,
3212
- displayStreaming,
3213
- streamActive: connection.status === "open" || connection.status === "connecting" || connection.status === "reconnecting"
3214
- }
3215
- ),
3216
- quote ? /* @__PURE__ */ jsxRuntimeExports.jsx(
3217
- "div",
3218
- {
3219
- className: cn("mt-2 flex w-full", role === "user" ? "justify-end" : "justify-start"),
3220
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-w-[85%] rounded-[8px] bg-[rgba(0,0,0,0.06)] px-3 py-2 text-[10px] leading-relaxed text-[var(--text-secondary)]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "line-clamp-2", children: [
3221
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-medium text-[var(--text-tertiary)]", children: [
3222
- quote.sender,
3223
- ":"
3224
- ] }),
3225
- " ",
3226
- quote.content
3227
- ] }) })
3228
- }
3229
- ) : null,
3230
- timestampLabel ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-message-timestamp", children: timestampLabel }) : null
3231
- ]
3232
- },
3233
- message.id
3234
- );
3235
- };
3236
- const renderHistoryBanner = () => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-3 flex justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
3237
- "div",
3238
- {
3239
- className: cn(
3240
- "inline-flex max-w-full items-center gap-2 rounded-full border px-3 py-1 font-medium",
3241
- "border-[var(--lab-border)] bg-[var(--lab-surface)] text-[var(--lab-text-secondary)]",
3242
- "text-[11px]"
3243
- ),
3244
- children: [
3245
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: historyLabel }),
3246
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3247
- "button",
3248
- {
3249
- type: "button",
3250
- onClick: () => {
3251
- setFollow(false);
3252
- loadFullHistory();
3253
- },
3254
- disabled: historyLoadingFull,
3255
- className: "text-[var(--lab-text-primary)] underline decoration-dotted underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50",
3256
- children: historyLoadingFull ? "Loading full history..." : "Load full history"
3257
- }
3258
- )
3259
- ]
3260
- }
3261
- ) });
3262
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
3263
- menuPortal,
3264
- messageContextMenuPortal,
3265
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ai-manus-root ai-manus-copilot ai-manus-embedded flex flex-1 min-h-0 flex-col", children: [
3266
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex flex-1 min-h-0 flex-col", children: [
3267
- /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, { ref: scrollRef, className: "flex-1 min-h-0", onScroll: handleScroll, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex min-h-full flex-col px-4 py-4", children: !questId ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-1 items-center justify-center text-[12px] text-[var(--text-tertiary)]", children: emptyStateLabel }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
3268
- connectionStatus ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-3 text-center text-[11px] text-[var(--text-tertiary)]", children: connectionStatus }) : null,
3269
- shouldVirtualize ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { height: rowVirtualizer.getTotalSize(), position: "relative" }, children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
3270
- if (showLoadFullHistory && virtualRow.index === 0) {
3271
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3272
- "div",
3273
- {
3274
- "data-index": virtualRow.index,
3275
- ref: rowVirtualizer.measureElement,
3276
- style: {
3277
- position: "absolute",
3278
- top: 0,
3279
- left: 0,
3280
- width: "100%",
3281
- transform: `translateY(${virtualRow.start}px)`
3282
- },
3283
- children: renderHistoryBanner()
3284
- },
3285
- virtualRow.key
3286
- );
3287
- }
3288
- const message = messages[virtualRow.index - listOffset];
3289
- if (!message) return null;
3290
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3291
- "div",
3292
- {
3293
- "data-index": virtualRow.index,
3294
- ref: rowVirtualizer.measureElement,
3295
- style: {
3296
- position: "absolute",
3297
- top: 0,
3298
- left: 0,
3299
- width: "100%",
3300
- transform: `translateY(${virtualRow.start}px)`
3301
- },
3302
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pb-3", children: renderGroupMessage(message) })
3303
- },
3304
- virtualRow.key
3305
- );
3306
- }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [
3307
- showLoadFullHistory ? renderHistoryBanner() : null,
3308
- messages.map((message) => renderGroupMessage(message))
3309
- ] })
3310
- ] }) }) }),
3311
- showHistoryLoadingOverlay ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
3312
- "div",
3313
- {
3314
- className: cn(
3315
- "flex items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] font-medium shadow-sm",
3316
- "border-[var(--lab-border)] bg-[var(--lab-surface)] text-[var(--lab-text-secondary)]"
3317
- ),
3318
- children: [
3319
- /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" }),
3320
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("common_loading", void 0, "Loading...") })
3321
- ]
3322
- }
3323
- ) }) : null
3324
- ] }),
3325
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t border-[var(--border-main)] px-4 py-3", children: [
3326
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3327
- LabControlReferenceChips,
3328
- {
3329
- selection,
3330
- proposal: activeProposal ? {
3331
- proposal_id: activeProposal.proposal_id,
3332
- action_type: activeProposal.action_type,
3333
- status: activeProposal.status
3334
- } : null,
3335
- onClearSelection: () => setSelection(null),
3336
- onClearProposal: () => setActiveProposal(null),
3337
- selectionPrefix: t("copilot_selection_chip", void 0, "Ref"),
3338
- proposalPrefix: t("copilot_proposal_chip", void 0, "Proposal")
3339
- }
3340
- ),
3341
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3342
- ChatBox,
3343
- {
3344
- value: input,
3345
- onChange: setInput,
3346
- onSubmit: handleSend,
3347
- isRunning: false,
3348
- mentionables,
3349
- mentionEnabled: !readOnly,
3350
- includeDefaultAgent: false,
3351
- lockLeadingMentionSpace: true,
3352
- attachments,
3353
- onAttachmentsChange: setAttachments,
3354
- attachmentsEnabled: false,
3355
- readOnly,
3356
- inputDisabled: !questId,
3357
- rows: 2,
3358
- placeholder: !questId ? emptyStateLabel : readOnly ? t(
3359
- "copilot_group_placeholder_offline",
3360
- void 0,
3361
- "Your execution server is offline. Messages will be sent once it reconnects."
3362
- ) : t(
3363
- "copilot_group_placeholder",
3364
- void 0,
3365
- "Message @ALL or a quest agent"
3366
- ),
3367
- compact: true,
3368
- containerClassName: "pb-0"
3369
- }
3370
- )
3371
- ] })
3372
- ] })
3373
- ] });
3374
- }
3375
- function FriendsMomentCard({
3376
- message,
3377
- readOnly,
3378
- liked,
3379
- busy,
3380
- likeUsers,
3381
- comments,
3382
- onToggleLike,
3383
- onCommentSubmit,
3384
- onAvatarContextMenu
3385
- }) {
3386
- const metadata = getMessageMetadata(message);
3387
- const metadataRecord = metadata;
3388
- const momentId = typeof metadata?.moment_id === "string" ? metadata.moment_id : "";
3389
- const contentText = getMessageText(message);
3390
- const mediaItems = resolveMomentMedia(metadata?.moment_media);
3391
- const senderName = metadata?.sender_name || resolveMessageTitle(message);
3392
- const senderLabel = metadata?.sender_label || metadata?.agent_label;
3393
- const avatarUrl = metadata?.sender_avatar_url || metadata?.agent_logo || "";
3394
- const avatarColor = metadata?.sender_avatar_color || metadata?.agent_avatar_color || "";
3395
- const timestampLabel = formatAbsoluteTimestamp(resolveMomentTimestamp(message));
3396
- const contentHtml = contentText ? renderMarkdown(contentText) : "";
3397
- const [commentOpen, setCommentOpen] = reactExports.useState(false);
3398
- const [commentDraft, setCommentDraft] = reactExports.useState("");
3399
- reactExports.useEffect(() => {
3400
- setCommentOpen(false);
3401
- setCommentDraft("");
3402
- }, [momentId]);
3403
- const countValue = (value) => {
3404
- if (typeof value === "number" && Number.isFinite(value)) return value;
3405
- if (typeof value === "string") {
3406
- const parsed = Number(value);
3407
- if (Number.isFinite(parsed)) return parsed;
3408
- }
3409
- return 0;
3410
- };
3411
- const likeCount = countValue(metadata?.moment_like_count);
3412
- const commentCount = countValue(metadata?.moment_comment_count);
3413
- const avatarInitial = senderName?.trim().slice(0, 1).toUpperCase() || "?";
3414
- const displayName = (() => {
3415
- const raw = senderLabel || senderName || "Agent";
3416
- const trimmed = raw.trim();
3417
- if (!trimmed) return "@Agent";
3418
- return trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
3419
- })();
3420
- const resolvedLikeUsers = mergeMomentLikes(
3421
- resolveMomentLikes(
3422
- metadataRecord?.moment_like_users ?? metadataRecord?.moment_likes ?? metadataRecord?.like_users ?? metadataRecord?.likes ?? metadataRecord?.moment_reactions ?? null
3423
- ),
3424
- likeUsers.map((name) => ({ name }))
3425
- );
3426
- const resolvedComments = mergeMomentComments(
3427
- resolveMomentComments(
3428
- metadataRecord?.moment_comments ?? metadataRecord?.moment_comment_list ?? metadataRecord?.comments ?? metadataRecord?.moment_comment_items ?? metadataRecord?.comment_items ?? null
3429
- ),
3430
- comments
3431
- );
3432
- const showReactions = resolvedLikeUsers.length > 0 || resolvedComments.length > 0;
3433
- const handleLikeClick = async () => {
3434
- if (!momentId || readOnly || busy) return;
3435
- await onToggleLike(momentId, !liked);
3436
- };
3437
- const handleCommentToggle = () => {
3438
- if (readOnly) return;
3439
- setCommentOpen((prev) => !prev);
3440
- };
3441
- const handleCommentSubmit = async () => {
3442
- if (!momentId || readOnly || busy) return;
3443
- const trimmed = commentDraft.trim();
3444
- if (!trimmed) return;
3445
- const ok = await onCommentSubmit(momentId, trimmed);
3446
- if (ok) {
3447
- setCommentDraft("");
3448
- setCommentOpen(false);
3449
- }
3450
- };
3451
- const handleCommentKeyDown = (event) => {
3452
- if (event.key === "Enter" && !event.shiftKey) {
3453
- event.preventDefault();
3454
- void handleCommentSubmit();
3455
- }
3456
- };
3457
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-card", children: [
3458
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3459
- "div",
3460
- {
3461
- className: "lab-avatar lab-avatar-sm lab-moment-avatar",
3462
- onContextMenu: (event) => onAvatarContextMenu?.(event, message),
3463
- children: [
3464
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-avatar-ring", style: avatarColor ? { borderColor: avatarColor } : void 0 }),
3465
- avatarUrl ? /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: avatarUrl, alt: senderName }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: avatarInitial })
3466
- ]
3467
- }
3468
- ),
3469
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-main", children: [
3470
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-moment-name", children: displayName }),
3471
- contentHtml ? /* @__PURE__ */ jsxRuntimeExports.jsx(
3472
- "div",
3473
- {
3474
- className: "lab-moment-content",
3475
- dangerouslySetInnerHTML: { __html: contentHtml }
3476
- }
3477
- ) : null,
3478
- mediaItems.length ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-moment-media", children: mediaItems.map((item, index) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-moment-media-item", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: item.url, alt: item.label || `Moment media ${index + 1}` }) }, `${item.url}-${index}`)) }) : null,
3479
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-meta-row", children: [
3480
- timestampLabel ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-moment-timestamp", children: timestampLabel }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", {}),
3481
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-actions", children: [
3482
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3483
- "button",
3484
- {
3485
- type: "button",
3486
- className: cn("lab-moment-action", liked && "is-active"),
3487
- onClick: handleLikeClick,
3488
- disabled: readOnly || busy,
3489
- "aria-label": "Like moment",
3490
- children: [
3491
- /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 14 }),
3492
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: likeCount })
3493
- ]
3494
- }
3495
- ),
3496
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3497
- "button",
3498
- {
3499
- type: "button",
3500
- className: "lab-moment-action",
3501
- onClick: handleCommentToggle,
3502
- disabled: readOnly || busy,
3503
- "aria-label": "Comment on moment",
3504
- children: [
3505
- /* @__PURE__ */ jsxRuntimeExports.jsx(MessageCircle, { size: 14 }),
3506
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: commentCount })
3507
- ]
3508
- }
3509
- )
3510
- ] })
3511
- ] }),
3512
- showReactions ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-reactions", children: [
3513
- resolvedLikeUsers.length ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-like-row", children: [
3514
- /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 12 }),
3515
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-moment-like-names", children: resolvedLikeUsers.join(", ") })
3516
- ] }) : null,
3517
- resolvedComments.map((comment, index) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-comment-row", children: [
3518
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-moment-comment-name", children: comment.name }),
3519
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-moment-comment-sep", children: ":" }),
3520
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-moment-comment-text", children: comment.content })
3521
- ] }, `${comment.name}-${index}`))
3522
- ] }) : null,
3523
- commentOpen ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-comment", children: [
3524
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3525
- "textarea",
3526
- {
3527
- value: commentDraft,
3528
- onChange: (event) => setCommentDraft(event.target.value),
3529
- onKeyDown: handleCommentKeyDown,
3530
- placeholder: "Write a comment",
3531
- className: "lab-moment-comment-input",
3532
- rows: 2,
3533
- disabled: readOnly || busy
3534
- }
3535
- ),
3536
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-moment-comment-actions", children: [
3537
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3538
- "button",
3539
- {
3540
- type: "button",
3541
- className: "lab-moment-action lab-moment-action-secondary",
3542
- onClick: () => setCommentOpen(false),
3543
- disabled: readOnly || busy,
3544
- children: "Cancel"
3545
- }
3546
- ),
3547
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3548
- "button",
3549
- {
3550
- type: "button",
3551
- className: "lab-moment-action lab-moment-action-primary",
3552
- onClick: handleCommentSubmit,
3553
- disabled: readOnly || busy || !commentDraft.trim(),
3554
- children: "Send"
3555
- }
3556
- )
3557
- ] })
3558
- ] }) : null
3559
- ] })
3560
- ] });
3561
- }
3562
- function LabFriendsFeed({
3563
- projectId,
3564
- agents,
3565
- templatesById,
3566
- readOnly,
3567
- quest,
3568
- quests,
3569
- onQuestChange
3570
- }) {
3571
- const { addToast } = useToast();
3572
- const queryClient = useQueryClient();
3573
- const [rosterBusy, setRosterBusy] = reactExports.useState(false);
3574
- const [momentBusy, setMomentBusy] = reactExports.useState(/* @__PURE__ */ new Set());
3575
- const [likedMoments, setLikedMoments] = reactExports.useState({});
3576
- const [momentLikeUsers, setMomentLikeUsers] = reactExports.useState({});
3577
- const [momentComments, setMomentComments] = reactExports.useState({});
3578
- const [messageContextMenu, setMessageContextMenu] = reactExports.useState(null);
3579
- const scrollRef = reactExports.useRef(null);
3580
- const lastSeenMessageIdRef = reactExports.useRef(null);
3581
- const [highlightedMessageId, setHighlightedMessageId] = reactExports.useState(null);
3582
- const [follow, setFollow] = reactExports.useState(true);
3583
- const setMode = useLabCopilotStore((state) => state.setMode);
3584
- const setActiveAgent = useLabCopilotStore((state) => state.setActiveAgent);
3585
- const setSessionIdForSurface = useChatSessionStore((state) => state.setSessionIdForSurface);
3586
- const user = useAuthStore((state) => state.user);
3587
- const currentUserLabel = reactExports.useMemo(() => {
3588
- const raw = user?.username || user?.email || user?.id || "You";
3589
- const trimmed = raw?.trim?.() ?? "";
3590
- return trimmed || "You";
3591
- }, [user?.email, user?.id, user?.username]);
3592
- const headerPortalTarget = useCopilotDockHeaderPortal();
3593
- const [searchQuery, setSearchQuery] = reactExports.useState("");
3594
- const agentsById = reactExports.useMemo(() => new Map(agents.map((agent) => [agent.instance_id, agent])), [agents]);
3595
- const questId = quest?.quest_id ?? null;
3596
- const {
3597
- messages,
3598
- connection,
3599
- historyTruncated,
3600
- historyLimit,
3601
- historyLoadingFull,
3602
- historyLoading,
3603
- hasLoadedOnce,
3604
- loadFullHistory
3605
- } = useLabSurfaceSession({
3606
- projectId,
3607
- questId,
3608
- surface: "friends",
3609
- enabled: Boolean(questId)
3610
- });
3611
- const friendEntities = reactExports.useMemo(() => {
3612
- if (!questId) return [];
3613
- return agents.filter((agent) => agent.active_quest_id === questId).map((agent) => {
3614
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
3615
- return {
3616
- id: agent.instance_id,
3617
- name: resolveAgentDisplayName(agent),
3618
- avatar: resolveAgentLogo(agent, template)
3619
- };
3620
- });
3621
- }, [agents, questId, templatesById]);
3622
- const availableAgents = reactExports.useMemo(() => {
3623
- if (!questId) return [];
3624
- return agents.filter((agent) => agent.active_quest_id !== questId).map((agent) => {
3625
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
3626
- return {
3627
- id: agent.instance_id,
3628
- name: resolveAgentDisplayName(agent),
3629
- avatar: resolveAgentLogo(agent, template)
3630
- };
3631
- });
3632
- }, [agents, questId, templatesById]);
3633
- const emptyRosterLabel = questId ? "No agents assigned to this quest yet." : "Select a quest to see its agents.";
3634
- const emptyStateLabel = reactExports.useMemo(() => {
3635
- if (!questId) return "Select a quest to view friend updates.";
3636
- return "No friend updates yet for this quest.";
3637
- }, [questId]);
3638
- const connectionStatus = reactExports.useMemo(() => {
3639
- if (connection.status === "rate_limited") return "Rate limited. Retrying...";
3640
- if (connection.status === "reconnecting") return "Reconnecting...";
3641
- if (connection.status === "error") return connection.error || "Connection error";
3642
- return null;
3643
- }, [connection.error, connection.status]);
3644
- const showLoadFullHistory = historyTruncated && Boolean(questId);
3645
- const showHistoryLoadingOverlay = Boolean(questId) && historyLoading && messages.length === 0 && !hasLoadedOnce;
3646
- const historyLabel = typeof historyLimit === "number" && historyLimit > 0 ? `Showing latest ${historyLimit} messages.` : "Showing recent messages.";
3647
- reactExports.useEffect(() => {
3648
- if (!messageContextMenu) return;
3649
- const handleDismiss = () => setMessageContextMenu(null);
3650
- const handleKey = (event) => {
3651
- if (event.key === "Escape") {
3652
- setMessageContextMenu(null);
3653
- }
3654
- };
3655
- window.addEventListener("click", handleDismiss);
3656
- window.addEventListener("contextmenu", handleDismiss);
3657
- window.addEventListener("keydown", handleKey);
3658
- window.addEventListener("scroll", handleDismiss, true);
3659
- return () => {
3660
- window.removeEventListener("click", handleDismiss);
3661
- window.removeEventListener("contextmenu", handleDismiss);
3662
- window.removeEventListener("keydown", handleKey);
3663
- window.removeEventListener("scroll", handleDismiss, true);
3664
- };
3665
- }, [messageContextMenu]);
3666
- reactExports.useEffect(() => {
3667
- setSearchQuery("");
3668
- setHighlightedMessageId(null);
3669
- lastSeenMessageIdRef.current = null;
3670
- setFollow(true);
3671
- setMomentBusy(/* @__PURE__ */ new Set());
3672
- setLikedMoments({});
3673
- setMomentLikeUsers({});
3674
- setMomentComments({});
3675
- }, [questId]);
3676
- reactExports.useEffect(() => {
3677
- const latest = messages[messages.length - 1];
3678
- if (!latest?.id) return;
3679
- if (!lastSeenMessageIdRef.current) {
3680
- lastSeenMessageIdRef.current = latest.id;
3681
- return;
3682
- }
3683
- if (latest.id !== lastSeenMessageIdRef.current) {
3684
- lastSeenMessageIdRef.current = latest.id;
3685
- setHighlightedMessageId(latest.id);
3686
- const timer = window.setTimeout(() => setHighlightedMessageId(null), 1200);
3687
- return () => window.clearTimeout(timer);
3688
- }
3689
- }, [messages]);
3690
- const messageContentById = reactExports.useMemo(() => {
3691
- const map = /* @__PURE__ */ new Map();
3692
- messages.forEach((message) => {
3693
- const content = getMessageText(message);
3694
- if (content) {
3695
- map.set(message.id, content);
3696
- }
3697
- });
3698
- return map;
3699
- }, [messages]);
3700
- const messagesById = reactExports.useMemo(() => {
3701
- return new Map(messages.map((message) => [message.id, message]));
3702
- }, [messages]);
3703
- const friendMessages = reactExports.useMemo(() => {
3704
- return messages.length ? [...messages].reverse() : [];
3705
- }, [messages]);
3706
- const handleAvatarContextMenu = reactExports.useCallback(
3707
- (event, message) => {
3708
- const metadata = getMessageMetadata(message);
3709
- const agentId = metadata?.agent_instance_id;
3710
- const sessionId = metadata?.session_id;
3711
- if (!agentId && !sessionId) return;
3712
- setMessageContextMenu({ messageId: message.id, x: event.clientX, y: event.clientY });
3713
- },
3714
- []
3715
- );
3716
- const contextTarget = messageContextMenu ? messagesById.get(messageContextMenu.messageId) ?? null : null;
3717
- const contextMetadata = contextTarget ? getMessageMetadata(contextTarget) : null;
3718
- const contextAgentId = contextMetadata?.agent_instance_id ?? null;
3719
- const contextSessionId = typeof contextMetadata?.session_id === "string" ? contextMetadata.session_id : null;
3720
- const canOpenDirect = Boolean(contextAgentId);
3721
- const handleOpenDirect = reactExports.useCallback(() => {
3722
- if (!contextAgentId) return;
3723
- setActiveAgent(contextAgentId);
3724
- if (contextSessionId) {
3725
- setSessionIdForSurface(projectId, "lab-direct", contextSessionId);
3726
- }
3727
- setMode("direct");
3728
- setMessageContextMenu(null);
3729
- }, [contextAgentId, contextSessionId, projectId, setActiveAgent, setMode, setSessionIdForSurface]);
3730
- const listCount = friendMessages.length + (showLoadFullHistory ? 1 : 0);
3731
- const historyBannerIndex = showLoadFullHistory ? friendMessages.length : -1;
3732
- const shouldVirtualize = listCount > 20;
3733
- const rowVirtualizer = useVirtualizer({
3734
- count: listCount,
3735
- getScrollElement: () => scrollRef.current,
3736
- estimateSize: (index) => showLoadFullHistory && index === historyBannerIndex ? 56 : 180,
3737
- getItemKey: (index) => {
3738
- if (showLoadFullHistory && index === historyBannerIndex) return "lab-friends-history-banner";
3739
- const message = friendMessages[index];
3740
- return message?.id ?? `lab-friends-${index}`;
3741
- },
3742
- overscan: 6
3743
- });
3744
- const friendSearchResults = reactExports.useMemo(() => {
3745
- const query = searchQuery.trim().toLowerCase();
3746
- if (!query) return [];
3747
- const results = [];
3748
- friendMessages.forEach((item, index) => {
3749
- const content = messageContentById.get(item.id) ?? "";
3750
- if (!content) return;
3751
- if (!content.toLowerCase().includes(query)) return;
3752
- const title = resolveMessageTitle(item);
3753
- results.push({
3754
- id: item.id,
3755
- index,
3756
- title,
3757
- excerpt: buildSearchSnippet(content, query)
3758
- });
3759
- });
3760
- return results;
3761
- }, [friendMessages, messageContentById, searchQuery]);
3762
- const handleAddAgents = reactExports.useCallback(
3763
- async (agentIds) => {
3764
- if (!questId || readOnly || agentIds.length === 0) return;
3765
- setRosterBusy(true);
3766
- try {
3767
- const results = await Promise.allSettled(
3768
- agentIds.map(
3769
- (agentId) => assignLabAgent(projectId, agentId, { quest_id: questId, quest_node_id: null })
3770
- )
3771
- );
3772
- const failed = [];
3773
- results.forEach((result, index) => {
3774
- if (result.status !== "fulfilled") {
3775
- failed.push(agentIds[index]);
3776
- }
3777
- });
3778
- if (failed.length) {
3779
- const names = failed.map((agentId) => {
3780
- const agent = agentsById.get(agentId);
3781
- return agent ? resolveAgentDisplayName(agent) : agentId;
3782
- });
3783
- addToast({
3784
- type: "error",
3785
- title: "Unable to add agents",
3786
- description: `Failed to add: ${names.join(", ")}`
3787
- });
3788
- }
3789
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
3790
- } finally {
3791
- setRosterBusy(false);
3792
- }
3793
- },
3794
- [addToast, agentsById, projectId, queryClient, questId, readOnly]
3795
- );
3796
- const handleRemoveAgent = reactExports.useCallback(
3797
- async (agentId) => {
3798
- if (!questId || readOnly) return;
3799
- setRosterBusy(true);
3800
- try {
3801
- await assignLabAgent(projectId, agentId, { quest_id: null, quest_node_id: null });
3802
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
3803
- } catch (error) {
3804
- const agent = agentsById.get(agentId);
3805
- const label = agent ? resolveAgentDisplayName(agent) : agentId;
3806
- addToast({
3807
- type: "error",
3808
- title: "Unable to remove agent",
3809
- description: `Failed to remove ${label} from this quest.`
3810
- });
3811
- } finally {
3812
- setRosterBusy(false);
3813
- }
3814
- },
3815
- [addToast, agentsById, projectId, queryClient, questId, readOnly]
3816
- );
3817
- const setMomentBusyState = reactExports.useCallback((momentId, busy) => {
3818
- setMomentBusy((prev) => {
3819
- const next = new Set(prev);
3820
- if (busy) {
3821
- next.add(momentId);
3822
- } else {
3823
- next.delete(momentId);
3824
- }
3825
- return next;
3826
- });
3827
- }, []);
3828
- const handleMomentToggleLike = reactExports.useCallback(
3829
- async (momentId, nextLiked) => {
3830
- if (!momentId || readOnly) return;
3831
- setMomentBusyState(momentId, true);
3832
- try {
3833
- if (nextLiked) {
3834
- await likeLabMoment(projectId, momentId);
3835
- } else {
3836
- await unlikeLabMoment(projectId, momentId);
3837
- }
3838
- setLikedMoments((prev) => ({ ...prev, [momentId]: nextLiked }));
3839
- setMomentLikeUsers((prev) => {
3840
- const existing = prev[momentId] ?? [];
3841
- const next = new Set(existing);
3842
- if (nextLiked) {
3843
- next.add(currentUserLabel);
3844
- } else {
3845
- next.delete(currentUserLabel);
3846
- }
3847
- return { ...prev, [momentId]: Array.from(next) };
3848
- });
3849
- } catch {
3850
- addToast({
3851
- type: "error",
3852
- title: "Unable to update like",
3853
- description: "Please try again once your connection stabilizes."
3854
- });
3855
- } finally {
3856
- setMomentBusyState(momentId, false);
3857
- }
3858
- },
3859
- [addToast, currentUserLabel, projectId, readOnly, setMomentBusyState]
3860
- );
3861
- const handleMomentComment = reactExports.useCallback(
3862
- async (momentId, content) => {
3863
- if (!momentId || readOnly) return false;
3864
- setMomentBusyState(momentId, true);
3865
- try {
3866
- await commentLabMoment(projectId, momentId, { content });
3867
- setMomentComments((prev) => {
3868
- const existing = prev[momentId] ?? [];
3869
- const nextItem = { name: currentUserLabel, content };
3870
- return { ...prev, [momentId]: [...existing, nextItem] };
3871
- });
3872
- return true;
3873
- } catch {
3874
- addToast({
3875
- type: "error",
3876
- title: "Unable to send comment",
3877
- description: "Please try again once your connection stabilizes."
3878
- });
3879
- return false;
3880
- } finally {
3881
- setMomentBusyState(momentId, false);
3882
- }
3883
- },
3884
- [addToast, currentUserLabel, projectId, readOnly, setMomentBusyState]
3885
- );
3886
- const flashPostHighlight = reactExports.useCallback((messageId) => {
3887
- setHighlightedMessageId(messageId);
3888
- if (typeof window !== "undefined") {
3889
- window.setTimeout(() => setHighlightedMessageId(null), 1200);
3890
- }
3891
- }, []);
3892
- const jumpToFriendResult = reactExports.useCallback(
3893
- (result) => {
3894
- if (!result) return;
3895
- if (!scrollRef.current) return;
3896
- if (shouldVirtualize) {
3897
- rowVirtualizer.scrollToIndex(result.index, { align: "center" });
3898
- } else {
3899
- const node = scrollRef.current?.querySelector(
3900
- `[data-friend-post-id="${result.id}"]`
3901
- );
3902
- if (node) {
3903
- node.scrollIntoView({ behavior: "smooth", block: "center" });
3904
- }
3905
- }
3906
- flashPostHighlight(result.id);
3907
- },
3908
- [flashPostHighlight, rowVirtualizer, shouldVirtualize]
3909
- );
3910
- reactExports.useEffect(() => {
3911
- if (!searchQuery.trim()) return;
3912
- if (friendSearchResults.length === 0) return;
3913
- jumpToFriendResult(friendSearchResults[0]);
3914
- }, [friendSearchResults, jumpToFriendResult, searchQuery]);
3915
- const menuPortal = headerPortalTarget ? reactDomExports.createPortal(
3916
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3917
- LabCopilotOverflowMenu,
3918
- {
3919
- label: "Friends",
3920
- entities: friendEntities,
3921
- emptyEntitiesLabel: emptyRosterLabel,
3922
- searchValue: searchQuery,
3923
- onSearchChange: setSearchQuery,
3924
- searchResults: friendSearchResults,
3925
- onSearchSelect: jumpToFriendResult,
3926
- searchPlaceholder: "Search friend updates",
3927
- questId,
3928
- quests,
3929
- onQuestChange,
3930
- canManageRoster: !readOnly && Boolean(questId),
3931
- rosterBusy,
3932
- availableAgents,
3933
- onAddAgents: handleAddAgents,
3934
- onRemoveAgent: handleRemoveAgent
3935
- }
3936
- ),
3937
- headerPortalTarget
3938
- ) : null;
3939
- const messageContextMenuPortal = messageContextMenu && typeof document !== "undefined" ? reactDomExports.createPortal(
3940
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3941
- "div",
3942
- {
3943
- className: "lab-copilot-context-menu",
3944
- style: { left: messageContextMenu.x, top: messageContextMenu.y },
3945
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: handleOpenDirect, disabled: !canOpenDirect, children: "Open direct session" })
3946
- }
3947
- ),
3948
- document.body
3949
- ) : null;
3950
- const handleScroll = reactExports.useCallback(
3951
- (event) => {
3952
- const target = event.currentTarget;
3953
- const nextFollow = isScrolledToTop(target);
3954
- setFollow((prev) => prev === nextFollow ? prev : nextFollow);
3955
- },
3956
- [isScrolledToTop]
3957
- );
3958
- const renderFriendMessage = (message) => {
3959
- const content = message.content;
3960
- const metadata = getMessageMetadata(message);
3961
- const displayStreaming = message.type === "text_delta" && content.status === "in_progress";
3962
- const isHighlighted = highlightedMessageId === message.id;
3963
- const isMoment = metadata?.message_kind === "moment";
3964
- const momentId = typeof metadata?.moment_id === "string" ? metadata.moment_id : "";
3965
- const localLikeUsers = momentId ? momentLikeUsers[momentId] ?? [] : [];
3966
- const localComments = momentId ? momentComments[momentId] ?? [] : [];
3967
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3968
- "div",
3969
- {
3970
- className: cn("rounded-[12px] px-1", isHighlighted && "lab-message-highlight"),
3971
- "data-friend-post-id": message.id,
3972
- children: isMoment ? /* @__PURE__ */ jsxRuntimeExports.jsx(
3973
- FriendsMomentCard,
3974
- {
3975
- message,
3976
- readOnly,
3977
- liked: momentId ? Boolean(likedMoments[momentId]) : false,
3978
- busy: momentId ? momentBusy.has(momentId) : false,
3979
- likeUsers: localLikeUsers,
3980
- comments: localComments,
3981
- onToggleLike: handleMomentToggleLike,
3982
- onCommentSubmit: handleMomentComment,
3983
- onAvatarContextMenu: handleAvatarContextMenu
3984
- }
3985
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
3986
- ChatMessage,
3987
- {
3988
- message,
3989
- compact: true,
3990
- onAvatarContextMenu: handleAvatarContextMenu,
3991
- displayStreaming,
3992
- streamActive: connection.status === "open" || connection.status === "connecting" || connection.status === "reconnecting"
3993
- }
3994
- )
3995
- },
3996
- message.id
3997
- );
3998
- };
3999
- const renderHistoryBanner = () => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-4 flex justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
4000
- "div",
4001
- {
4002
- className: cn(
4003
- "inline-flex max-w-full items-center gap-2 rounded-full border px-3 py-1 font-medium",
4004
- "border-[var(--lab-border)] bg-[var(--lab-surface)] text-[var(--lab-text-secondary)]",
4005
- "text-[11px]"
4006
- ),
4007
- children: [
4008
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: historyLabel }),
4009
- /* @__PURE__ */ jsxRuntimeExports.jsx(
4010
- "button",
4011
- {
4012
- type: "button",
4013
- onClick: () => {
4014
- setFollow(false);
4015
- loadFullHistory();
4016
- },
4017
- disabled: historyLoadingFull,
4018
- className: "text-[var(--lab-text-primary)] underline decoration-dotted underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50",
4019
- children: historyLoadingFull ? "Loading full history..." : "Load full history"
4020
- }
4021
- )
4022
- ]
4023
- }
4024
- ) });
4025
- const scrollToTop = reactExports.useCallback(() => {
4026
- const node = scrollRef.current;
4027
- if (!node) return;
4028
- if (typeof node.scrollTo === "function") {
4029
- node.scrollTo({ top: 0, behavior: "auto" });
4030
- } else {
4031
- node.scrollTop = 0;
4032
- }
4033
- }, []);
4034
- reactExports.useEffect(() => {
4035
- if (!questId || messages.length === 0) return;
4036
- if (!follow) return;
4037
- window.requestAnimationFrame(() => scrollToTop());
4038
- }, [follow, messages, questId, scrollToTop]);
4039
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
4040
- menuPortal,
4041
- messageContextMenuPortal,
4042
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ai-manus-root ai-manus-copilot ai-manus-embedded flex flex-1 min-h-0 flex-col", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex flex-1 min-h-0 flex-col", children: [
4043
- /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, { ref: scrollRef, className: "flex-1 min-h-0", onScroll: handleScroll, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-5 py-6", children: [
4044
- connectionStatus ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-3 text-center text-[11px] text-[var(--text-tertiary)]", children: connectionStatus }) : null,
4045
- !questId ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex min-h-[200px] items-center justify-center text-[12px] text-[var(--text-tertiary)]", children: emptyStateLabel }) : messages.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex min-h-[200px] items-center justify-center text-[12px] text-[var(--text-tertiary)]", children: emptyStateLabel }) : shouldVirtualize ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { height: rowVirtualizer.getTotalSize(), position: "relative" }, children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
4046
- if (showLoadFullHistory && virtualRow.index === historyBannerIndex) {
4047
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
4048
- "div",
4049
- {
4050
- "data-index": virtualRow.index,
4051
- ref: rowVirtualizer.measureElement,
4052
- style: {
4053
- position: "absolute",
4054
- top: 0,
4055
- left: 0,
4056
- width: "100%",
4057
- transform: `translateY(${virtualRow.start}px)`
4058
- },
4059
- children: renderHistoryBanner()
4060
- },
4061
- virtualRow.key
4062
- );
4063
- }
4064
- const message = friendMessages[virtualRow.index];
4065
- if (!message) return null;
4066
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
4067
- "div",
4068
- {
4069
- "data-index": virtualRow.index,
4070
- ref: rowVirtualizer.measureElement,
4071
- style: {
4072
- position: "absolute",
4073
- top: 0,
4074
- left: 0,
4075
- width: "100%",
4076
- transform: `translateY(${virtualRow.start}px)`
4077
- },
4078
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pb-5", children: renderFriendMessage(message) })
4079
- },
4080
- virtualRow.key
4081
- );
4082
- }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col lab-friends-list", children: [
4083
- friendMessages.map((message) => renderFriendMessage(message)),
4084
- showLoadFullHistory ? renderHistoryBanner() : null
4085
- ] })
4086
- ] }) }),
4087
- showHistoryLoadingOverlay ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
4088
- "div",
4089
- {
4090
- className: cn(
4091
- "flex items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] font-medium shadow-sm",
4092
- "border-[var(--lab-border)] bg-[var(--lab-surface)] text-[var(--lab-text-secondary)]"
4093
- ),
4094
- children: [
4095
- /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" }),
4096
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Loading..." })
4097
- ]
4098
- }
4099
- ) }) : null
4100
- ] }) })
4101
- ] });
4102
- }
4103
-
4104
- export { LabCopilotHeader, LabCopilotPanel as default };