@researai/deepscientist 1.5.15 → 1.5.16

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 (193) hide show
  1. package/README.md +336 -98
  2. package/bin/ds.js +691 -91
  3. package/docs/en/00_QUICK_START.md +36 -15
  4. package/docs/en/01_SETTINGS_REFERENCE.md +33 -0
  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 +11 -5
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  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/README.md +18 -0
  15. package/docs/zh/00_QUICK_START.md +36 -15
  16. package/docs/zh/01_SETTINGS_REFERENCE.md +33 -0
  17. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  18. package/docs/zh/05_TUI_GUIDE.md +6 -0
  19. package/docs/zh/09_DOCTOR.md +11 -5
  20. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  21. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  22. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  23. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  24. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  25. package/docs/zh/README.md +18 -0
  26. package/package.json +1 -1
  27. package/pyproject.toml +1 -1
  28. package/src/deepscientist/__init__.py +1 -1
  29. package/src/deepscientist/acp/envelope.py +6 -0
  30. package/src/deepscientist/artifact/service.py +647 -22
  31. package/src/deepscientist/bash_exec/service.py +234 -9
  32. package/src/deepscientist/cli.py +115 -19
  33. package/src/deepscientist/codex_cli_compat.py +232 -0
  34. package/src/deepscientist/config/models.py +2 -1
  35. package/src/deepscientist/config/service.py +31 -9
  36. package/src/deepscientist/daemon/api/handlers.py +125 -6
  37. package/src/deepscientist/daemon/api/router.py +4 -0
  38. package/src/deepscientist/daemon/app.py +715 -98
  39. package/src/deepscientist/gitops/__init__.py +10 -1
  40. package/src/deepscientist/gitops/diff.py +129 -0
  41. package/src/deepscientist/gitops/service.py +4 -1
  42. package/src/deepscientist/mcp/server.py +39 -0
  43. package/src/deepscientist/prompts/builder.py +255 -32
  44. package/src/deepscientist/quest/layout.py +15 -2
  45. package/src/deepscientist/quest/service.py +295 -43
  46. package/src/deepscientist/quest/stage_views.py +6 -1
  47. package/src/deepscientist/runners/codex.py +86 -31
  48. package/src/deepscientist/skills/__init__.py +2 -2
  49. package/src/deepscientist/skills/installer.py +196 -5
  50. package/src/deepscientist/skills/registry.py +66 -0
  51. package/src/prompts/connectors/qq.md +18 -8
  52. package/src/prompts/connectors/weixin.md +16 -6
  53. package/src/prompts/contracts/shared_interaction.md +12 -1
  54. package/src/prompts/system.md +10 -5
  55. package/src/prompts/system_copilot.md +43 -0
  56. package/src/skills/analysis-campaign/SKILL.md +1 -0
  57. package/src/skills/baseline/SKILL.md +8 -0
  58. package/src/skills/decision/SKILL.md +8 -0
  59. package/src/skills/experiment/SKILL.md +8 -0
  60. package/src/skills/figure-polish/SKILL.md +1 -0
  61. package/src/skills/finalize/SKILL.md +1 -0
  62. package/src/skills/idea/SKILL.md +1 -0
  63. package/src/skills/intake-audit/SKILL.md +8 -0
  64. package/src/skills/mentor/SKILL.md +217 -0
  65. package/src/skills/mentor/references/correction-rules.md +210 -0
  66. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  67. package/src/skills/mentor/references/persona-profile.md +138 -0
  68. package/src/skills/mentor/references/taste-profile.md +128 -0
  69. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  70. package/src/skills/mentor/references/work-profile.md +289 -0
  71. package/src/skills/mentor/references/workflow-profile.md +240 -0
  72. package/src/skills/optimize/SKILL.md +1 -0
  73. package/src/skills/rebuttal/SKILL.md +1 -0
  74. package/src/skills/review/SKILL.md +1 -0
  75. package/src/skills/scout/SKILL.md +8 -0
  76. package/src/skills/write/SKILL.md +1 -0
  77. package/src/tui/dist/app/AppContainer.js +19 -11
  78. package/src/tui/dist/index.js +4 -1
  79. package/src/tui/dist/lib/api.js +33 -3
  80. package/src/tui/package.json +1 -1
  81. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  82. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  83. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  84. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  85. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  86. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  87. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  88. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  89. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  90. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  91. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  92. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  93. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  94. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  95. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  96. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  97. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  98. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  99. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  100. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  101. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  102. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  103. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  104. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  105. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  106. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  107. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  108. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  109. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  110. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  111. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  112. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  113. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  114. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  115. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  116. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  117. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  118. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  119. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  120. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  121. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  122. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  123. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  124. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  125. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  126. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  127. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  128. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  129. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  130. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  131. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  132. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  133. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  134. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  135. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  136. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  137. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  138. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  139. package/src/ui/dist/index.html +5 -2
  140. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  141. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  142. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  143. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  144. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  145. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  146. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  147. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  148. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  149. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  150. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  151. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  152. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  153. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  154. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  155. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  156. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  157. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  158. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  159. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  160. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  161. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  162. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  163. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  164. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  165. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  166. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  167. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  168. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  169. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  170. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  171. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  172. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  173. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  174. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  175. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  176. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  177. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  178. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  179. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  180. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  181. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  182. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  183. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  184. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  185. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  186. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  187. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  188. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  189. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  190. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  191. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  192. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  193. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -1,2677 +0,0 @@
1
- import { w as createLucideIcon, aN as useQuery, r as reactExports, j as jsxRuntimeExports, bh as Skeleton, bi as Badge, aQ as client, bj as ReactFlowProvider, u as useI18n, aF as resolveAgentDisplayName, a_ as resolveAgentLogo, b1 as pickAvatarFrameColor, aX as isLabWorkingStatus, bk as normalizeLabStatus, bl as useNodesState, bm as useReactFlow, bn as index, bo as Background, b as cn, bp as Handle, bq as Position, br as CircleHelp, bs as OrbitLogoStatus, bt as Clock, bu as Moon, aI as formatRelativeTime, m as ChevronDown, aT as useQueryClient, aA as useLabCopilotStore, aU as useLabGraphSelectionStore, bv as resolveLabCanvasSelectionSemantic, aO as Button, g as RefreshCw, bw as LabQuestGraphCanvas, bx as ArrowLeft, b4 as ScrollArea, N as Sparkles, by as GitBranch, bz as LAB_CANVAS_SEMANTIC_TONE_META, bA as getLabQuest, bB as getLabQuestSummary, bC as getLabQuestEventPayload, bD as getLabQuestNodeTrace, bE as isQuestRuntimeSurface, bF as getApiBaseUrl, bG as redirectToLanding, bH as useCliStore, bI as useActiveTab, v as BUILTIN_PLUGINS, bJ as listLabTemplates, aB as listLabAgents, aC as listLabQuests, bK as getLabOverview, bL as getProject, bM as useMaxEntitlement } from './index-D_E4281X.js';
2
- import { B as Bot } from './bot-0DYntytV.js';
3
-
4
- /**
5
- * @license lucide-react v0.511.0 - ISC
6
- *
7
- * This source code is licensed under the ISC license.
8
- * See the LICENSE file in the root directory of this source tree.
9
- */
10
-
11
-
12
- const __iconNode$1 = [
13
- ["polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12", key: "o97t9d" }],
14
- [
15
- "path",
16
- {
17
- d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",
18
- key: "oot6mr"
19
- }
20
- ]
21
- ];
22
- const Inbox = createLucideIcon("inbox", __iconNode$1);
23
-
24
- /**
25
- * @license lucide-react v0.511.0 - ISC
26
- *
27
- * This source code is licensed under the ISC license.
28
- * See the LICENSE file in the root directory of this source tree.
29
- */
30
-
31
-
32
- const __iconNode = [
33
- ["rect", { width: "7", height: "9", x: "3", y: "3", rx: "1", key: "10lvy0" }],
34
- ["rect", { width: "7", height: "5", x: "14", y: "3", rx: "1", key: "16une8" }],
35
- ["rect", { width: "7", height: "9", x: "14", y: "12", rx: "1", key: "1hutg5" }],
36
- ["rect", { width: "7", height: "5", x: "3", y: "16", rx: "1", key: "ldoo1y" }]
37
- ];
38
- const LayoutDashboard = createLucideIcon("layout-dashboard", __iconNode);
39
-
40
- const formatStateLabel$1 = (value) => {
41
- const normalized = String(value || "").trim().replace(/[_-]+/g, " ");
42
- if (!normalized) return "N/A";
43
- return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
44
- };
45
- const prettyJson = (value) => {
46
- try {
47
- return JSON.stringify(value, null, 2);
48
- } catch {
49
- return String(value);
50
- }
51
- };
52
- const asRecord$1 = (value) => {
53
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
54
- return value;
55
- };
56
- const asString = (value) => {
57
- if (typeof value === "string") return value.trim() || null;
58
- if (typeof value === "number" || typeof value === "boolean") return String(value);
59
- return null;
60
- };
61
- function MetaCard({
62
- label,
63
- value
64
- }) {
65
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[14px] border border-[var(--lab-border)] bg-[var(--lab-background)] px-3 py-2.5", children: [
66
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--lab-text-secondary)]", children: label }),
67
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-sm text-[var(--lab-text-primary)]", children: value })
68
- ] });
69
- }
70
- function LabNodeTraceDetail({
71
- projectId,
72
- questId,
73
- trace,
74
- isLoading,
75
- payloadJson,
76
- payloadTruncated
77
- }) {
78
- const primaryPayload = trace?.payload_json || asRecord$1(payloadJson)?.payload || payloadJson || null;
79
- const primaryAction = [...trace?.actions || []].reverse().find((action) => action.artifact_id || action.head_commit) || null;
80
- const headCommit = trace?.head_commit || primaryAction?.head_commit || null;
81
- const artifactKind = trace?.artifact_kind || primaryAction?.artifact_kind || null;
82
- const artifactId = trace?.artifact_id || primaryAction?.artifact_id || null;
83
- const commitQuery = useQuery({
84
- queryKey: ["lab-trace-commit", questId, headCommit],
85
- queryFn: () => client.gitCommit(questId, headCommit),
86
- enabled: Boolean(questId && headCommit),
87
- staleTime: 15e3
88
- });
89
- const fileCandidates = reactExports.useMemo(() => {
90
- const explicit = [...trace?.changed_files || []].filter(Boolean);
91
- const commitFiles = commitQuery.data?.files?.map((file) => file.path).filter(Boolean) || [];
92
- const payloadPaths = Object.values(trace?.paths_map || {}).map((entry) => asString(entry)).filter((entry) => Boolean(entry && !entry.startsWith("/")));
93
- return [.../* @__PURE__ */ new Set([...explicit, ...commitFiles, ...payloadPaths])];
94
- }, [commitQuery.data?.files, trace?.changed_files, trace?.paths_map]);
95
- const [selectedPath, setSelectedPath] = reactExports.useState(null);
96
- reactExports.useEffect(() => {
97
- if (!fileCandidates.length) {
98
- setSelectedPath(null);
99
- return;
100
- }
101
- if (!selectedPath || !fileCandidates.includes(selectedPath)) {
102
- setSelectedPath(fileCandidates[0] || null);
103
- }
104
- }, [fileCandidates, selectedPath]);
105
- const commitFileQuery = useQuery({
106
- queryKey: ["lab-trace-commit-file", questId, headCommit, selectedPath],
107
- queryFn: () => client.gitCommitFile(questId, headCommit, selectedPath),
108
- enabled: Boolean(questId && headCommit && selectedPath),
109
- staleTime: 15e3
110
- });
111
- if (isLoading && !trace) {
112
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
113
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-24 w-full rounded-[18px]" }),
114
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-20 w-full rounded-[18px]" }),
115
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-32 w-full rounded-[18px]" })
116
- ] });
117
- }
118
- if (!trace) {
119
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4 text-sm text-[var(--lab-text-secondary)]", children: "No trace is attached to this node yet." });
120
- }
121
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
122
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4", children: [
123
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
124
- /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(trace.selection_type) }),
125
- trace.stage_title ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: trace.stage_title }) : null,
126
- trace.status ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(trace.status) }) : null,
127
- artifactKind ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(artifactKind) }) : null
128
- ] }),
129
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 text-sm font-semibold text-[var(--lab-text-primary)]", children: trace.title }),
130
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-xs leading-5 text-[var(--lab-text-secondary)]", children: trace.summary || "No normalized summary is available for this node." })
131
- ] }),
132
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
133
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Branch", value: trace.branch_name || "N/A" }),
134
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Stage", value: trace.stage_title || "N/A" }),
135
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Artifact", value: artifactId || "N/A" }),
136
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Commit", value: headCommit || "N/A" }),
137
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Actions", value: trace.counts?.actions ?? trace.actions.length }),
138
- /* @__PURE__ */ jsxRuntimeExports.jsx(MetaCard, { label: "Updated", value: trace.updated_at || "N/A" })
139
- ] }),
140
- primaryPayload ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4", children: [
141
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
142
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold uppercase tracking-[0.12em] text-[var(--lab-text-secondary)]", children: "Artifact Payload" }),
143
- payloadTruncated ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: "Truncated" }) : null
144
- ] }),
145
- /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "mt-3 max-h-[280px] overflow-auto whitespace-pre-wrap rounded-[14px] bg-[var(--lab-background)] p-3 text-[11px] leading-5 text-[var(--lab-text-primary)]", children: prettyJson(primaryPayload) })
146
- ] }) : null,
147
- headCommit ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4", children: [
148
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold uppercase tracking-[0.12em] text-[var(--lab-text-secondary)]", children: "Commit" }),
149
- commitQuery.isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "mt-3 h-20 w-full rounded-[14px]" }) : commitQuery.data ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 space-y-2 text-xs text-[var(--lab-text-secondary)]", children: [
150
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-semibold text-[var(--lab-text-primary)]", children: commitQuery.data.subject }),
151
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
152
- commitQuery.data.short_sha,
153
- " · ",
154
- commitQuery.data.author_name || "Unknown author"
155
- ] }),
156
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: commitQuery.data.authored_at || "Unknown time" }),
157
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
158
- commitQuery.data.file_count || 0,
159
- " files · +",
160
- commitQuery.data.stats?.added || 0,
161
- " / -",
162
- commitQuery.data.stats?.removed || 0
163
- ] })
164
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 text-sm text-[var(--lab-text-secondary)]", children: "Commit metadata is unavailable." })
165
- ] }) : null,
166
- fileCandidates.length ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4", children: [
167
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold uppercase tracking-[0.12em] text-[var(--lab-text-secondary)]", children: "Changed Files" }),
168
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 flex flex-wrap gap-2", children: fileCandidates.slice(0, 12).map((path) => /* @__PURE__ */ jsxRuntimeExports.jsx(
169
- "button",
170
- {
171
- type: "button",
172
- className: `rounded-full border px-2.5 py-1 text-[11px] ${selectedPath === path ? "border-[var(--lab-accent-strong)] bg-[rgba(64,113,175,0.1)] text-[var(--lab-text-primary)]" : "border-[var(--lab-border)] bg-[var(--lab-background)] text-[var(--lab-text-secondary)]"}`,
173
- onClick: () => setSelectedPath(path),
174
- children: path
175
- },
176
- path
177
- )) }),
178
- selectedPath ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3", children: [
179
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold text-[var(--lab-text-primary)]", children: selectedPath }),
180
- commitFileQuery.isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "mt-2 h-36 w-full rounded-[14px]" }) : commitFileQuery.data?.lines?.length ? /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "mt-2 max-h-[320px] overflow-auto whitespace-pre-wrap rounded-[14px] bg-[var(--lab-background)] p-3 text-[11px] leading-5 text-[var(--lab-text-primary)]", children: commitFileQuery.data.lines.join("\n") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-sm text-[var(--lab-text-secondary)]", children: projectId && questId ? "Preview unavailable for this file." : "Preview unavailable." })
181
- ] }) : null
182
- ] }) : null,
183
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-[18px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-4", children: [
184
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold uppercase tracking-[0.12em] text-[var(--lab-text-secondary)]", children: "Trace Actions" }),
185
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 space-y-3", children: trace.actions.length ? trace.actions.map((action) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
186
- "div",
187
- {
188
- className: "rounded-[14px] border border-[var(--lab-border)] bg-[var(--lab-background)] px-3 py-3",
189
- children: [
190
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
191
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-semibold text-[var(--lab-text-primary)]", children: action.title || action.tool_name || action.raw_event_type || action.kind || action.action_id }),
192
- action.kind ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(action.kind) }) : null,
193
- action.status ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(action.status) }) : null,
194
- action.artifact_kind ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel$1(action.artifact_kind) }) : null
195
- ] }),
196
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-[11px] text-[var(--lab-text-secondary)]", children: action.created_at || "N/A" }),
197
- action.summary ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-xs leading-5 text-[var(--lab-text-secondary)]", children: action.summary }) : null,
198
- action.head_commit ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 text-xs text-[var(--lab-text-secondary)]", children: [
199
- "Commit: ",
200
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-[var(--lab-text-primary)]", children: action.head_commit })
201
- ] }) : null,
202
- action.tool_name ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 text-xs text-[var(--lab-text-secondary)]", children: [
203
- "Tool: ",
204
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-[var(--lab-text-primary)]", children: action.tool_name })
205
- ] }) : null,
206
- action.mcp_server || action.mcp_tool ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 text-xs text-[var(--lab-text-secondary)]", children: [
207
- "MCP:",
208
- " ",
209
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-[var(--lab-text-primary)]", children: [
210
- action.mcp_server || "unknown",
211
- action.mcp_tool ? `.${action.mcp_tool}` : ""
212
- ] })
213
- ] }) : null,
214
- action.args ? /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap rounded-[12px] bg-[rgba(15,23,42,0.04)] p-2 text-[11px] leading-5 text-[var(--lab-text-primary)]", children: action.args }) : null,
215
- action.output ? /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "mt-2 max-h-40 overflow-auto whitespace-pre-wrap rounded-[12px] bg-[rgba(15,23,42,0.04)] p-2 text-[11px] leading-5 text-[var(--lab-text-primary)]", children: action.output }) : null
216
- ]
217
- },
218
- action.action_id
219
- )) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-[var(--lab-text-secondary)]", children: "No action records yet." }) })
220
- ] })
221
- ] });
222
- }
223
-
224
- const LAB_FOCUS_EVENT$1 = "ds:lab:focus";
225
- const dispatchLabFocus = (payload) => {
226
- if (typeof window === "undefined") return;
227
- window.dispatchEvent(new CustomEvent(LAB_FOCUS_EVENT$1, { detail: payload }));
228
- };
229
-
230
- const getOverviewNodeKind = (data) => {
231
- if ("kind" in data) return data.kind;
232
- return "quest";
233
- };
234
- const isOverviewNodeDataEqual = (left, right) => {
235
- const leftKind = getOverviewNodeKind(left);
236
- const rightKind = getOverviewNodeKind(right);
237
- if (leftKind !== rightKind) return false;
238
- if (leftKind === "quest" && rightKind === "quest") {
239
- const a = left;
240
- const b = right;
241
- return a.questId === b.questId && a.title === b.title && a.status === b.status && a.piState === b.piState && a.baselineBound === b.baselineBound && a.hasCliBinding === b.hasCliBinding && a.headBranch === b.headBranch && a.lastEventAt === b.lastEventAt && a.pendingQuestionCount === b.pendingQuestionCount && a.piAgentName === b.piAgentName && a.piAgentLogo === b.piAgentLogo && a.piAgentFrameColor === b.piAgentFrameColor && a.createdAt === b.createdAt && a.isActive === b.isActive;
242
- }
243
- if (leftKind === "agent" && rightKind === "agent") {
244
- const a = left;
245
- const b = right;
246
- return a.questId === b.questId && a.agentId === b.agentId && a.name === b.name && a.avatar === b.avatar && a.avatarColor === b.avatarColor && a.statusTone === b.statusTone && a.statusState === b.statusState && a.statusLabel === b.statusLabel;
247
- }
248
- if (leftKind === "pi" && rightKind === "pi") {
249
- const a = left;
250
- const b = right;
251
- return a.agentId === b.agentId && a.name === b.name && a.avatar === b.avatar && a.avatarColor === b.avatarColor && a.statusTone === b.statusTone && a.statusState === b.statusState && a.statusLabel === b.statusLabel;
252
- }
253
- if (leftKind === "pending" && rightKind === "pending") {
254
- const a = left;
255
- const b = right;
256
- return a.count === b.count;
257
- }
258
- return false;
259
- };
260
- const STORAGE_VERSION = 2;
261
- const FLOATING_PANEL_MARGIN = 14;
262
- const ORB_SIZE = 44;
263
- const ORB_STEP = ORB_SIZE + 12;
264
- const OVERVIEW_PANEL_WIDTH = 420;
265
- const OVERVIEW_PANEL_HEIGHT = 520;
266
- const ACTION_PANEL_WIDTH = 420;
267
- const ACTION_PANEL_HEIGHT = 600;
268
- const readStoredLayout = (projectId) => {
269
- if (typeof window === "undefined") return null;
270
- try {
271
- const raw = window.localStorage.getItem(`ds:lab:overview_layout:v${STORAGE_VERSION}:${projectId}`);
272
- if (!raw) return null;
273
- const parsed = JSON.parse(raw);
274
- if (parsed?.version !== STORAGE_VERSION) return null;
275
- return parsed;
276
- } catch {
277
- return null;
278
- }
279
- };
280
- const writeStoredLayout = (projectId, layout) => {
281
- if (typeof window === "undefined") return;
282
- try {
283
- window.localStorage.setItem(
284
- `ds:lab:overview_layout:v${STORAGE_VERSION}:${projectId}`,
285
- JSON.stringify(layout)
286
- );
287
- } catch {
288
- }
289
- };
290
- const computeDefaultQuestPosition = (index, origin) => {
291
- const cols = 3;
292
- const gapX = 260;
293
- const gapY = 170;
294
- const col = index % cols;
295
- const row = Math.floor(index / cols);
296
- return { x: origin.x + col * gapX, y: origin.y + row * gapY };
297
- };
298
- const GUIDE_COLUMNS = 2;
299
- const GUIDE_GAP_X = 494;
300
- const GUIDE_GAP_Y = 221;
301
- const GUIDE_NODE_WIDTH = 340;
302
- const GUIDE_NODE_HEIGHT = 150;
303
- const GUIDE_GROUP_PADDING = 28;
304
- const GUIDE_GROUP_HEADER_HEIGHT = 28;
305
- const GUIDE_CENTER_X = 0;
306
- const GUIDE_START_Y = 40;
307
- const PI_NODE_WIDTH = 220;
308
- const computeGuideGroupRect = (stepsCount, cardHeight) => {
309
- const rows = Math.max(1, Math.ceil(stepsCount / GUIDE_COLUMNS));
310
- const totalWidth = (GUIDE_COLUMNS - 1) * GUIDE_GAP_X + GUIDE_NODE_WIDTH;
311
- const totalHeight = (rows - 1) * GUIDE_GAP_Y + cardHeight;
312
- const width = totalWidth + GUIDE_GROUP_PADDING * 2;
313
- const height = totalHeight + GUIDE_GROUP_PADDING * 2 + GUIDE_GROUP_HEADER_HEIGHT;
314
- const x = GUIDE_CENTER_X - totalWidth / 2 - GUIDE_GROUP_PADDING;
315
- const y = GUIDE_START_Y - GUIDE_GROUP_PADDING - GUIDE_GROUP_HEADER_HEIGHT;
316
- return { x, y, width, height };
317
- };
318
- const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
319
- const rectsOverlap = (a, b) => {
320
- return !(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y);
321
- };
322
- const POSITION_EPS = 0.5;
323
- const sanitizePosition = (pos, fallback) => {
324
- const x = Number.isFinite(pos.x) ? pos.x : fallback.x;
325
- const y = Number.isFinite(pos.y) ? pos.y : fallback.y;
326
- return { x, y };
327
- };
328
- const isPositionDifferent = (a, b) => {
329
- return Math.abs(a.x - b.x) > POSITION_EPS || Math.abs(a.y - b.y) > POSITION_EPS;
330
- };
331
- const resolveOrbPosition = (current, bounds, blockers) => {
332
- const leftX = FLOATING_PANEL_MARGIN;
333
- const rightX = Math.max(FLOATING_PANEL_MARGIN, bounds.width - ORB_SIZE - FLOATING_PANEL_MARGIN);
334
- const topY = FLOATING_PANEL_MARGIN;
335
- const bottomY = Math.max(FLOATING_PANEL_MARGIN, bounds.height - ORB_SIZE - FLOATING_PANEL_MARGIN);
336
- const clampedX = clamp(current.x, leftX, rightX);
337
- const clampedY = clamp(current.y, topY, bottomY);
338
- const distances = [
339
- { edge: "left", dist: Math.abs(clampedX - leftX) },
340
- { edge: "right", dist: Math.abs(clampedX - rightX) },
341
- { edge: "top", dist: Math.abs(clampedY - topY) },
342
- { edge: "bottom", dist: Math.abs(clampedY - bottomY) }
343
- ].sort((a, b) => a.dist - b.dist);
344
- const primary = distances[0]?.edge ?? "left";
345
- const candidates = [];
346
- const pushCandidate = (x, y) => {
347
- candidates.push({
348
- x: clamp(x, leftX, rightX),
349
- y: clamp(y, topY, bottomY)
350
- });
351
- };
352
- if (primary === "left" || primary === "right") {
353
- const edgeX = primary === "left" ? leftX : rightX;
354
- pushCandidate(edgeX, clampedY);
355
- pushCandidate(edgeX, clampedY + ORB_STEP);
356
- pushCandidate(edgeX, clampedY - ORB_STEP);
357
- pushCandidate(primary === "left" ? rightX : leftX, clampedY);
358
- } else {
359
- const edgeY = primary === "top" ? topY : bottomY;
360
- pushCandidate(clampedX, edgeY);
361
- pushCandidate(clampedX + ORB_STEP, edgeY);
362
- pushCandidate(clampedX - ORB_STEP, edgeY);
363
- pushCandidate(clampedX, primary === "top" ? bottomY : topY);
364
- }
365
- pushCandidate(leftX, topY);
366
- pushCandidate(rightX, topY);
367
- pushCandidate(leftX, bottomY);
368
- pushCandidate(rightX, bottomY);
369
- const seen = /* @__PURE__ */ new Set();
370
- const uniqueCandidates = candidates.filter((candidate) => {
371
- const key = `${candidate.x}:${candidate.y}`;
372
- if (seen.has(key)) return false;
373
- seen.add(key);
374
- return true;
375
- });
376
- for (const candidate of uniqueCandidates) {
377
- const orbRect = { x: candidate.x, y: candidate.y, width: ORB_SIZE, height: ORB_SIZE };
378
- if (!blockers.some((block) => rectsOverlap(orbRect, block))) {
379
- return candidate;
380
- }
381
- }
382
- return uniqueCandidates[0] ?? { x: leftX, y: topY };
383
- };
384
- const resolvePanelSize = (kind, collapsed, bounds) => {
385
- if (collapsed) {
386
- return { width: ORB_SIZE, height: ORB_SIZE };
387
- }
388
- const baseWidth = kind === "action" ? ACTION_PANEL_WIDTH : OVERVIEW_PANEL_WIDTH;
389
- const baseHeight = kind === "action" ? ACTION_PANEL_HEIGHT : OVERVIEW_PANEL_HEIGHT;
390
- return {
391
- width: Math.min(baseWidth, Math.max(ORB_SIZE, bounds.width - 24)),
392
- height: Math.min(baseHeight, Math.max(ORB_SIZE, bounds.height - 24))
393
- };
394
- };
395
- const resolvePanelState = (stored, fallback) => {
396
- return {
397
- x: Number.isFinite(stored?.x) ? Number(stored?.x) : fallback.x,
398
- y: Number.isFinite(stored?.y) ? Number(stored?.y) : fallback.y,
399
- collapsed: typeof stored?.collapsed === "boolean" ? stored.collapsed : fallback.collapsed
400
- };
401
- };
402
- const OverviewPendingActionPanelContext = reactExports.createContext(null);
403
- const OverviewQuestNode = ({ data }) => {
404
- const { t } = useI18n("lab");
405
- const nodeData = data;
406
- const status = nodeData.status ? String(nodeData.status).toLowerCase() : null;
407
- const pi = nodeData.piState ? String(nodeData.piState).toLowerCase() : null;
408
- const baselineBound = Boolean(nodeData.baselineBound);
409
- const headBranch = nodeData.headBranch?.trim() || "main";
410
- const hasCliBinding = Boolean(nodeData.hasCliBinding);
411
- const pendingQuestionCount = Math.max(0, Number(nodeData.pendingQuestionCount ?? 0));
412
- const hasPendingQuestions = pendingQuestionCount > 0;
413
- const lastEventLabel = nodeData.lastEventAt ? formatRelativeTime(nodeData.lastEventAt) : t("overview_last_event_none", void 0, "No event yet");
414
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
415
- "div",
416
- {
417
- className: cn(
418
- "lab-overview-quest-node",
419
- nodeData.isActive && "is-active",
420
- status ? `is-${status}` : null
421
- ),
422
- children: [
423
- /* @__PURE__ */ jsxRuntimeExports.jsx(Handle, { type: "target", position: Position.Left, className: "lab-flow-handle" }),
424
- /* @__PURE__ */ jsxRuntimeExports.jsx(Handle, { type: "source", position: Position.Right, className: "lab-flow-handle" }),
425
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-quest-node__title-row", children: [
426
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-quest-node__title", children: nodeData.title || "Quest" }),
427
- nodeData.piAgentName ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
428
- "button",
429
- {
430
- type: "button",
431
- className: "lab-overview-pi-chip nodrag nopan nowheel",
432
- onClick: (event) => {
433
- event.stopPropagation();
434
- nodeData.onPiClick?.();
435
- },
436
- "aria-label": t("overview_open_pi_chat", void 0, "Open PI chat"),
437
- title: t("overview_open_pi_chat", void 0, "Open PI chat"),
438
- children: [
439
- /* @__PURE__ */ jsxRuntimeExports.jsx(
440
- "span",
441
- {
442
- className: "lab-overview-pi-avatar",
443
- style: nodeData.piAgentFrameColor ? { borderColor: nodeData.piAgentFrameColor } : void 0,
444
- children: nodeData.piAgentLogo ? (
445
- // eslint-disable-next-line @next/next/no-img-element
446
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: nodeData.piAgentLogo, alt: "" })
447
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-pi-avatar__fallback", children: t("overview_pi_label") })
448
- }
449
- ),
450
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-pi-label", children: nodeData.piAgentName })
451
- ]
452
- }
453
- ) : null
454
- ] }),
455
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-quest-node__meta", children: [
456
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: status || t("overview_unknown", void 0, "unknown") }),
457
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
458
- t("overview_pi_label", void 0, "PI"),
459
- " ",
460
- pi || t("overview_unknown", void 0, "unknown")
461
- ] })
462
- ] }),
463
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-quest-node__chips", children: [
464
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: baselineBound ? t("overview_baseline_bound", void 0, "Baseline bound") : t("overview_baseline_pending", void 0, "Baseline pending") }),
465
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: t("overview_head_branch", { branch: headBranch }, "Head {branch}") }),
466
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: hasCliBinding ? t("overview_cli_bound", void 0, "CLI bound") : t("overview_cli_pending", void 0, "CLI pending") }),
467
- hasPendingQuestions ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip lab-overview-quest-chip--attention", children: t("overview_decision_count", { count: pendingQuestionCount }, "{count} decisions") }) : null,
468
- typeof nodeData.progressingCount === "number" ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: t("overview_progressing_count", { count: nodeData.progressingCount }, "{count} progressing") }) : null,
469
- typeof nodeData.experimentCount === "number" ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: t("overview_experiment_count", { count: nodeData.experimentCount }, "{count} experiments") }) : null,
470
- typeof nodeData.pushFailedCount === "number" && nodeData.pushFailedCount > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip lab-overview-quest-chip--attention", children: t("overview_push_failed_count", { count: nodeData.pushFailedCount }, "{count} push failed") }) : null,
471
- typeof nodeData.writerConflictCount === "number" && nodeData.writerConflictCount > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip lab-overview-quest-chip--attention", children: t(
472
- "overview_writer_conflict_count",
473
- { count: nodeData.writerConflictCount },
474
- "{count} writer conflicts"
475
- ) }) : null,
476
- nodeData.runtimeLabel ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", children: nodeData.runtimeLabel }) : null,
477
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-quest-chip", title: nodeData.lastEventAt ?? void 0, children: t("overview_last_event", { value: lastEventLabel }, "Last event {value}") })
478
- ] })
479
- ]
480
- }
481
- );
482
- };
483
- const OverviewAgentNode = ({ data }) => {
484
- const nodeData = data;
485
- const statusClass = `lab-status-${nodeData.statusTone}`;
486
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node", children: [
487
- /* @__PURE__ */ jsxRuntimeExports.jsx(Handle, { type: "target", position: Position.Left, className: "lab-flow-handle" }),
488
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("lab-avatar lab-avatar-sm", statusClass), children: [
489
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-avatar-ring", style: { borderColor: nodeData.avatarColor } }),
490
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: nodeData.avatar, alt: nodeData.name })
491
- ] }),
492
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node__meta", children: [
493
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-agent-node__name", children: nodeData.name }),
494
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node__status", children: [
495
- /* @__PURE__ */ jsxRuntimeExports.jsx(
496
- "span",
497
- {
498
- className: cn(
499
- "lab-status-indicator",
500
- nodeData.statusState === "working" && "lab-status-indicator-working",
501
- nodeData.statusState === "waiting" && "lab-status-indicator-waiting"
502
- ),
503
- "aria-label": nodeData.statusLabel,
504
- title: nodeData.statusLabel,
505
- children: nodeData.statusState === "waiting" ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleHelp, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : nodeData.statusState === "working" ? /* @__PURE__ */ jsxRuntimeExports.jsx(OrbitLogoStatus, { compact: true, sizePx: 16, className: "lab-status-orbit" }) : nodeData.statusState === "paused" ? /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Moon, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
506
- }
507
- ),
508
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-agent-node__status-label", children: nodeData.statusLabel })
509
- ] })
510
- ] })
511
- ] });
512
- };
513
- const OverviewPiNode = ({ data }) => {
514
- const nodeData = data;
515
- const statusClass = `lab-status-${nodeData.statusTone}`;
516
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node", children: [
517
- /* @__PURE__ */ jsxRuntimeExports.jsx(Handle, { type: "source", position: Position.Right, className: "lab-flow-handle" }),
518
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("lab-avatar lab-avatar-sm", statusClass), children: [
519
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-avatar-ring", style: { borderColor: nodeData.avatarColor } }),
520
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: nodeData.avatar, alt: nodeData.name })
521
- ] }),
522
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node__meta", children: [
523
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-agent-node__name", children: nodeData.name }),
524
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-agent-node__status", children: [
525
- /* @__PURE__ */ jsxRuntimeExports.jsx(
526
- "span",
527
- {
528
- className: cn(
529
- "lab-status-indicator",
530
- nodeData.statusState === "working" && "lab-status-indicator-working",
531
- nodeData.statusState === "waiting" && "lab-status-indicator-waiting"
532
- ),
533
- "aria-label": nodeData.statusLabel,
534
- title: nodeData.statusLabel,
535
- children: nodeData.statusState === "waiting" ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleHelp, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : nodeData.statusState === "working" ? /* @__PURE__ */ jsxRuntimeExports.jsx(OrbitLogoStatus, { compact: true, sizePx: 16, className: "lab-status-orbit" }) : nodeData.statusState === "paused" ? /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Moon, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
536
- }
537
- ),
538
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-agent-node__status-label", children: nodeData.statusLabel })
539
- ] })
540
- ] })
541
- ] });
542
- };
543
- const OverviewPendingNode = ({ data }) => {
544
- const { t } = useI18n("lab");
545
- const nodeData = data;
546
- const actionPanel = reactExports.useContext(OverviewPendingActionPanelContext);
547
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-pending-node", "aria-label": t("overview_pending_decisions"), children: [
548
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-overview-pending-node__header", children: [
549
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
550
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-pending-node__title", children: t("overview_pending_decisions") }),
551
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-pending-node__subtitle", children: nodeData.count === 1 ? t("overview_pending_single") : t("overview_pending_plural", { count: nodeData.count }) })
552
- ] }),
553
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-overview-pending-node__count", "aria-hidden": "true", children: nodeData.count })
554
- ] }),
555
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-overview-pending-node__content nodrag nowheel", children: actionPanel })
556
- ] });
557
- };
558
- const OVERVIEW_NODE_TYPES = {
559
- overviewQuest: OverviewQuestNode,
560
- overviewAgent: OverviewAgentNode,
561
- overviewPi: OverviewPiNode,
562
- overviewPending: OverviewPendingNode
563
- };
564
- function FloatingPanel({
565
- boundsRef,
566
- title,
567
- icon,
568
- state,
569
- zIndex,
570
- onChange,
571
- onActivate,
572
- className,
573
- children
574
- }) {
575
- const dragRef = reactExports.useRef(null);
576
- const rootRef = reactExports.useRef(null);
577
- const cleanupRef = reactExports.useRef(null);
578
- const lastDragMovedRef = reactExports.useRef(false);
579
- reactExports.useEffect(() => {
580
- return () => {
581
- cleanupRef.current?.();
582
- cleanupRef.current = null;
583
- };
584
- }, []);
585
- const startDrag = reactExports.useCallback(
586
- (event) => {
587
- event.stopPropagation();
588
- if (typeof event.button === "number" && event.button !== 0) return;
589
- onActivate?.();
590
- if (!rootRef.current) return;
591
- const bounds = boundsRef.current;
592
- if (!bounds) return;
593
- cleanupRef.current?.();
594
- lastDragMovedRef.current = false;
595
- const boundsRect = bounds.getBoundingClientRect();
596
- const rootRect = rootRef.current.getBoundingClientRect();
597
- const originX = rootRect.left - boundsRect.left;
598
- const originY = rootRect.top - boundsRect.top;
599
- dragRef.current = {
600
- originX,
601
- originY,
602
- pointerId: event.pointerId,
603
- startX: event.clientX,
604
- startY: event.clientY,
605
- moved: false
606
- };
607
- rootRef.current.setPointerCapture(event.pointerId);
608
- const handleMove = (nextEvent) => {
609
- if (!dragRef.current) return;
610
- if (nextEvent.pointerId !== dragRef.current.pointerId) return;
611
- const dx = nextEvent.clientX - dragRef.current.startX;
612
- const dy = nextEvent.clientY - dragRef.current.startY;
613
- if (Math.abs(dx) + Math.abs(dy) > 2) {
614
- dragRef.current.moved = true;
615
- }
616
- const maxX = Math.max(
617
- FLOATING_PANEL_MARGIN,
618
- boundsRect.width - rootRect.width - FLOATING_PANEL_MARGIN
619
- );
620
- const maxY = Math.max(
621
- FLOATING_PANEL_MARGIN,
622
- boundsRect.height - rootRect.height - FLOATING_PANEL_MARGIN
623
- );
624
- const nextX = clamp(
625
- dragRef.current.originX + dx,
626
- FLOATING_PANEL_MARGIN,
627
- maxX
628
- );
629
- const nextY = clamp(
630
- dragRef.current.originY + dy,
631
- FLOATING_PANEL_MARGIN,
632
- maxY
633
- );
634
- onChange({ ...state, x: nextX, y: nextY });
635
- };
636
- const handleUp = (nextEvent) => {
637
- if (!dragRef.current) return;
638
- if (nextEvent.pointerId !== dragRef.current.pointerId) return;
639
- lastDragMovedRef.current = dragRef.current.moved;
640
- dragRef.current = null;
641
- cleanupRef.current?.();
642
- cleanupRef.current = null;
643
- };
644
- window.addEventListener("pointermove", handleMove);
645
- window.addEventListener("pointerup", handleUp);
646
- window.addEventListener("pointercancel", handleUp);
647
- cleanupRef.current = () => {
648
- window.removeEventListener("pointermove", handleMove);
649
- window.removeEventListener("pointerup", handleUp);
650
- window.removeEventListener("pointercancel", handleUp);
651
- };
652
- },
653
- [boundsRef, onActivate, onChange, state]
654
- );
655
- const collapse = reactExports.useCallback(
656
- (event) => {
657
- event.stopPropagation();
658
- onChange({ ...state, collapsed: true });
659
- },
660
- [onChange, state]
661
- );
662
- const expand = reactExports.useCallback(() => {
663
- if (lastDragMovedRef.current) {
664
- lastDragMovedRef.current = false;
665
- return;
666
- }
667
- onActivate?.();
668
- onChange({ ...state, collapsed: false });
669
- }, [onActivate, onChange, state]);
670
- if (state.collapsed) {
671
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
672
- "button",
673
- {
674
- ref: rootRef,
675
- type: "button",
676
- className: cn("lab-floating-orb nowheel nopan", className),
677
- style: { transform: `translate3d(${state.x}px, ${state.y}px, 0)`, zIndex },
678
- onPointerDown: startDrag,
679
- onClick: (event) => {
680
- event.stopPropagation();
681
- expand();
682
- },
683
- "aria-label": `Show ${title}`,
684
- children: icon
685
- }
686
- );
687
- }
688
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
689
- "div",
690
- {
691
- ref: rootRef,
692
- className: cn("lab-floating-panel nowheel nopan", className),
693
- style: { transform: `translate3d(${state.x}px, ${state.y}px, 0)`, zIndex },
694
- onPointerDown: (event) => {
695
- event.stopPropagation();
696
- onActivate?.();
697
- },
698
- children: [
699
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-floating-panel__header", onPointerDown: startDrag, children: [
700
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lab-floating-panel__title", children: [
701
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "lab-floating-panel__icon", children: icon }),
702
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: title })
703
- ] }),
704
- /* @__PURE__ */ jsxRuntimeExports.jsx(
705
- "button",
706
- {
707
- type: "button",
708
- className: "lab-floating-panel__collapse",
709
- onPointerDown: (event) => event.stopPropagation(),
710
- onClick: collapse,
711
- "aria-label": `Collapse ${title}`,
712
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 16 })
713
- }
714
- )
715
- ] }),
716
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-floating-panel__content", children })
717
- ]
718
- }
719
- );
720
- }
721
- function LabOverviewCanvasInner({
722
- projectId,
723
- quests,
724
- agents = [],
725
- templates = [],
726
- graphVm,
727
- waitingAnswerByAgentId = /* @__PURE__ */ new Map(),
728
- pendingDecisionCount,
729
- activeQuestId,
730
- hasPiAgent,
731
- piAgent,
732
- readOnly,
733
- canvasLevel = "map",
734
- actionPanel,
735
- overviewPanel,
736
- showFloatingPanels = true,
737
- onOpenCanvas,
738
- onOpenQuestPiChat,
739
- onOpenPiChat,
740
- onSelectAgent
741
- }) {
742
- const { t } = useI18n("lab");
743
- const boundsRef = reactExports.useRef(null);
744
- const [canvasBoundsReady, setCanvasBoundsReady] = reactExports.useState(false);
745
- const storedLayout = reactExports.useMemo(() => readStoredLayout(projectId), [projectId]);
746
- const storedViewport = storedLayout?.viewport ?? null;
747
- const initialViewport = storedViewport && Number.isFinite(storedViewport.x) && Number.isFinite(storedViewport.y) && Number.isFinite(storedViewport.zoom) && storedViewport.zoom >= 0.15 && storedViewport.zoom <= 3 ? storedViewport : void 0;
748
- const fittedViewportRef = reactExports.useRef(false);
749
- const viewportCheckedRef = reactExports.useRef(false);
750
- const nodesRef = reactExports.useRef([]);
751
- const viewportRef = reactExports.useRef(storedLayout?.viewport ?? null);
752
- const saveTimeoutRef = reactExports.useRef(null);
753
- const zCounterRef = reactExports.useRef(2);
754
- const [panelZ, setPanelZ] = reactExports.useState({
755
- overview: 1,
756
- action: 2
757
- });
758
- const [overviewPanelState, setOverviewPanelState] = reactExports.useState(
759
- () => resolvePanelState(storedLayout?.panels?.overview, { x: 16, y: 16, collapsed: false })
760
- );
761
- const [actionPanelState, setActionPanelState] = reactExports.useState(
762
- () => resolvePanelState(storedLayout?.panels?.action, { x: 16, y: 88, collapsed: false })
763
- );
764
- const governanceByQuestId = reactExports.useMemo(() => {
765
- const map = /* @__PURE__ */ new Map();
766
- graphVm?.quests?.forEach((questVm) => {
767
- map.set(questVm.questId, questVm);
768
- });
769
- return map;
770
- }, [graphVm]);
771
- const bringToFront = reactExports.useCallback((key) => {
772
- zCounterRef.current += 1;
773
- const nextZ = zCounterRef.current;
774
- setPanelZ((prev) => ({ ...prev, [key]: nextZ }));
775
- }, []);
776
- const scheduleSave = reactExports.useCallback(() => {
777
- if (typeof window === "undefined") return;
778
- if (saveTimeoutRef.current) {
779
- window.clearTimeout(saveTimeoutRef.current);
780
- }
781
- saveTimeoutRef.current = window.setTimeout(() => {
782
- const nodePositions = {};
783
- nodesRef.current.forEach((node) => {
784
- nodePositions[node.id] = { x: node.position.x, y: node.position.y };
785
- });
786
- writeStoredLayout(projectId, {
787
- version: STORAGE_VERSION,
788
- viewport: viewportRef.current,
789
- nodes: nodePositions,
790
- panels: {
791
- overview: {
792
- x: overviewPanelState.x,
793
- y: overviewPanelState.y,
794
- collapsed: overviewPanelState.collapsed
795
- },
796
- action: {
797
- x: actionPanelState.x,
798
- y: actionPanelState.y,
799
- collapsed: actionPanelState.collapsed
800
- }
801
- }
802
- });
803
- }, 450);
804
- }, [actionPanelState, overviewPanelState, projectId]);
805
- const guideGroupRect = reactExports.useMemo(() => {
806
- return computeGuideGroupRect(4, GUIDE_NODE_HEIGHT);
807
- }, []);
808
- const questOrigin = reactExports.useMemo(() => {
809
- const x = Math.round(guideGroupRect.x + guideGroupRect.width + 140);
810
- const y = Math.round(guideGroupRect.y);
811
- return { x, y };
812
- }, [guideGroupRect]);
813
- const resolvedPendingCount = reactExports.useMemo(() => {
814
- if (typeof pendingDecisionCount === "number") {
815
- return Math.max(0, pendingDecisionCount);
816
- }
817
- let count = 0;
818
- for (const value of waitingAnswerByAgentId.values()) {
819
- if (value) count += 1;
820
- }
821
- return count;
822
- }, [pendingDecisionCount, waitingAnswerByAgentId]);
823
- const isOpsMode = canvasLevel === "ops";
824
- const hasPendingDecisions = resolvedPendingCount > 0;
825
- const hasPendingNode = hasPendingDecisions && !isOpsMode;
826
- const showActionPanelFloating = isOpsMode || !hasPendingNode;
827
- reactExports.useLayoutEffect(() => {
828
- const bounds = boundsRef.current;
829
- if (!bounds) return;
830
- const rect = bounds.getBoundingClientRect();
831
- setActionPanelState((prev) => {
832
- const hasStoredX = Number.isFinite(storedLayout?.panels?.action?.x);
833
- const hasStoredY = Number.isFinite(storedLayout?.panels?.action?.y);
834
- if (hasStoredX || hasStoredY) return prev;
835
- if (prev.x !== 16 || prev.y !== 88) return prev;
836
- const nextX = Math.max(FLOATING_PANEL_MARGIN, rect.width - ACTION_PANEL_WIDTH - FLOATING_PANEL_MARGIN);
837
- return { ...prev, x: nextX };
838
- });
839
- }, [storedLayout?.panels?.action?.x, storedLayout?.panels?.action?.y]);
840
- reactExports.useLayoutEffect(() => {
841
- const bounds = boundsRef.current;
842
- if (!bounds) return;
843
- const rect = bounds.getBoundingClientRect();
844
- const clampPanel = (panel, size) => {
845
- const maxX = Math.max(FLOATING_PANEL_MARGIN, rect.width - size.width - FLOATING_PANEL_MARGIN);
846
- const maxY = Math.max(FLOATING_PANEL_MARGIN, rect.height - size.height - FLOATING_PANEL_MARGIN);
847
- return {
848
- x: clamp(panel.x, FLOATING_PANEL_MARGIN, maxX),
849
- y: clamp(panel.y, FLOATING_PANEL_MARGIN, maxY)
850
- };
851
- };
852
- const overviewSize = resolvePanelSize("overview", overviewPanelState.collapsed, rect);
853
- const actionSize = resolvePanelSize("action", actionPanelState.collapsed, rect);
854
- let nextOverview = overviewPanelState;
855
- if (!overviewPanelState.collapsed) {
856
- const clamped = clampPanel(overviewPanelState, overviewSize);
857
- if (clamped.x !== overviewPanelState.x || clamped.y !== overviewPanelState.y) {
858
- nextOverview = { ...overviewPanelState, ...clamped };
859
- }
860
- }
861
- let nextAction = actionPanelState;
862
- if (!actionPanelState.collapsed) {
863
- const clamped = clampPanel(actionPanelState, actionSize);
864
- if (clamped.x !== actionPanelState.x || clamped.y !== actionPanelState.y) {
865
- nextAction = { ...actionPanelState, ...clamped };
866
- }
867
- }
868
- const blockers = [];
869
- if (!nextOverview.collapsed) {
870
- blockers.push({ x: nextOverview.x, y: nextOverview.y, ...overviewSize });
871
- }
872
- if (!nextAction.collapsed) {
873
- blockers.push({ x: nextAction.x, y: nextAction.y, ...actionSize });
874
- }
875
- if (nextOverview.collapsed) {
876
- const pos = sanitizePosition(resolveOrbPosition(nextOverview, rect, blockers), nextOverview);
877
- if (isPositionDifferent(pos, nextOverview)) {
878
- nextOverview = { ...nextOverview, ...pos };
879
- }
880
- blockers.push({ x: nextOverview.x, y: nextOverview.y, width: ORB_SIZE, height: ORB_SIZE });
881
- }
882
- if (nextAction.collapsed) {
883
- const pos = sanitizePosition(resolveOrbPosition(nextAction, rect, blockers), nextAction);
884
- if (isPositionDifferent(pos, nextAction)) {
885
- nextAction = { ...nextAction, ...pos };
886
- }
887
- }
888
- if (isPositionDifferent(nextOverview, overviewPanelState) || nextOverview.collapsed !== overviewPanelState.collapsed) {
889
- setOverviewPanelState(nextOverview);
890
- }
891
- if (isPositionDifferent(nextAction, actionPanelState) || nextAction.collapsed !== actionPanelState.collapsed) {
892
- setActionPanelState(nextAction);
893
- }
894
- }, [actionPanelState, overviewPanelState, storedLayout?.panels?.action?.x, storedLayout?.panels?.action?.y]);
895
- reactExports.useEffect(() => {
896
- if (!isOpsMode) return;
897
- bringToFront("action");
898
- setActionPanelState((prev) => prev.collapsed ? { ...prev, collapsed: false } : prev);
899
- }, [bringToFront, isOpsMode]);
900
- const pendingNode = reactExports.useMemo(() => {
901
- if (!hasPendingNode) return null;
902
- const width = ACTION_PANEL_WIDTH;
903
- const height = ACTION_PANEL_HEIGHT;
904
- const stored = storedLayout?.nodes?.["overview:pending"] ?? null;
905
- const fallbackX = Math.round(guideGroupRect.x + guideGroupRect.width / 2 - width / 2);
906
- const fallbackY = Math.round(guideGroupRect.y + guideGroupRect.height + 96);
907
- const position = stored ? stored : { x: fallbackX, y: fallbackY };
908
- return {
909
- id: "overview:pending",
910
- type: "overviewPending",
911
- position,
912
- draggable: !readOnly,
913
- selectable: !readOnly,
914
- data: {
915
- kind: "pending",
916
- count: resolvedPendingCount
917
- },
918
- style: {
919
- width,
920
- height,
921
- zIndex: 2
922
- }
923
- };
924
- }, [guideGroupRect, hasPendingNode, readOnly, resolvedPendingCount, storedLayout?.nodes]);
925
- const questNodes = reactExports.useMemo(() => {
926
- const storedNodes = storedLayout?.nodes ?? {};
927
- return quests.map((quest, index) => {
928
- const governanceQuest = governanceByQuestId.get(quest.quest_id);
929
- const id = `quest:${quest.quest_id}`;
930
- const stored = storedNodes?.[id] ?? null;
931
- const position = stored ? stored : computeDefaultQuestPosition(index, questOrigin);
932
- const summary = governanceQuest?.summary;
933
- const runtime = governanceQuest?.runtime;
934
- const runtimeLabel = runtime && runtime.runningAgents > 0 ? t("overview_runtime_running", { count: runtime.runningAgents }, "{count} running") : null;
935
- return {
936
- id,
937
- type: "overviewQuest",
938
- position,
939
- draggable: !readOnly,
940
- data: {
941
- questId: quest.quest_id,
942
- title: quest.title || "Quest",
943
- status: quest.status ?? null,
944
- piState: quest.pi_state ?? runtime?.piState ?? null,
945
- baselineBound: governanceQuest?.governance.formalBaselineState === "confirmed" || quest.governance?.formalBaselineState === "confirmed",
946
- hasCliBinding: Boolean(quest.cli_server_id),
947
- headBranch: governanceQuest?.topology.headBranch ?? quest.git_head_branch ?? null,
948
- lastEventAt: quest.last_event_at ?? null,
949
- pendingQuestionCount: Math.max(0, Number(quest.pending_question_count ?? 0)),
950
- piAgentName: piAgent?.name ?? null,
951
- piAgentLogo: piAgent?.logo ?? null,
952
- piAgentFrameColor: piAgent?.frameColor ?? null,
953
- onPiClick: piAgent?.name ? () => {
954
- if (onOpenQuestPiChat) {
955
- onOpenQuestPiChat(quest.quest_id);
956
- return;
957
- }
958
- onOpenPiChat?.();
959
- } : null,
960
- createdAt: quest.created_at ?? null,
961
- isActive: quest.quest_id === activeQuestId,
962
- progressingCount: summary?.progressingCount ?? void 0,
963
- experimentCount: summary?.experimentCount ?? void 0,
964
- runtimeLabel,
965
- pushFailedCount: summary?.pushFailedCount ?? void 0,
966
- writerConflictCount: summary?.writerConflictCount ?? void 0
967
- }
968
- };
969
- });
970
- }, [
971
- activeQuestId,
972
- governanceByQuestId,
973
- onOpenPiChat,
974
- onOpenQuestPiChat,
975
- piAgent,
976
- t,
977
- quests,
978
- readOnly,
979
- storedLayout?.nodes,
980
- questOrigin
981
- ]);
982
- const templatesById = reactExports.useMemo(() => {
983
- return new Map(templates.map((template) => [template.template_id, template]));
984
- }, [templates]);
985
- const piNode = reactExports.useMemo(() => {
986
- if (!hasPiAgent) return null;
987
- if (!agents.length) return null;
988
- const piTemplate = templates.find((template2) => template2.template_key === "pi") ?? null;
989
- const piAgentInstance = (piTemplate ? agents.find((agent) => agent.template_id === piTemplate.template_id) : agents.find((agent) => agent.agent_id === "pi")) ?? null;
990
- if (!piAgentInstance) return null;
991
- const storedNodes = storedLayout?.nodes ?? {};
992
- const defaultPosition = {
993
- x: GUIDE_CENTER_X - PI_NODE_WIDTH / 2,
994
- y: guideGroupRect.y + guideGroupRect.height + 48
995
- };
996
- const nodeId = `pi:${piAgentInstance.instance_id}`;
997
- const position = storedNodes?.[nodeId] ?? defaultPosition;
998
- const template = piAgentInstance.template_id ? templatesById.get(piAgentInstance.template_id) ?? null : null;
999
- const displayName = resolveAgentDisplayName(piAgentInstance) || "PI";
1000
- const avatar = resolveAgentLogo(piAgentInstance, template);
1001
- const avatarColor = piAgentInstance.avatar_frame_color?.trim() || pickAvatarFrameColor(piAgentInstance.instance_id, 0);
1002
- const statusLabelRaw = typeof piAgentInstance.status === "string" ? piAgentInstance.status.toLowerCase() : "idle";
1003
- const waitingForAnswer = waitingAnswerByAgentId.get(piAgentInstance.instance_id) ?? false;
1004
- const isPaused = statusLabelRaw === "waiting" && !waitingForAnswer;
1005
- const effectiveStatus = statusLabelRaw === "waiting" && !waitingForAnswer ? "idle" : statusLabelRaw;
1006
- const isWaiting = waitingForAnswer;
1007
- const isWorking = !isWaiting && isLabWorkingStatus(effectiveStatus);
1008
- const statusTone = isWaiting ? "waiting" : isWorking ? "working" : normalizeLabStatus(effectiveStatus);
1009
- const statusState = isWaiting ? "waiting" : isWorking ? "working" : isPaused ? "paused" : "resting";
1010
- const statusLabel = isWaiting ? "Waiting for answer" : isWorking ? "Working" : isPaused ? "Paused" : "Resting";
1011
- return {
1012
- id: nodeId,
1013
- type: "overviewPi",
1014
- position,
1015
- draggable: !readOnly,
1016
- data: {
1017
- kind: "pi",
1018
- agentId: piAgentInstance.instance_id,
1019
- name: displayName,
1020
- avatar,
1021
- avatarColor,
1022
- statusTone,
1023
- statusState,
1024
- statusLabel
1025
- }
1026
- };
1027
- }, [
1028
- agents,
1029
- guideGroupRect,
1030
- hasPiAgent,
1031
- readOnly,
1032
- storedLayout?.nodes,
1033
- templates,
1034
- templatesById,
1035
- waitingAnswerByAgentId
1036
- ]);
1037
- const agentNodes = reactExports.useMemo(() => {
1038
- if (!agents.length || !questNodes.length) return [];
1039
- const storedNodes = storedLayout?.nodes ?? {};
1040
- const questNodeById = new Map(questNodes.map((node) => [node.id, node]));
1041
- const questIds = new Set(quests.map((quest) => quest.quest_id));
1042
- const agentsByQuest = /* @__PURE__ */ new Map();
1043
- agents.forEach((agent) => {
1044
- const questId = agent.active_quest_id;
1045
- if (!questId || !questIds.has(questId)) return;
1046
- const list = agentsByQuest.get(questId) ?? [];
1047
- list.push(agent);
1048
- agentsByQuest.set(questId, list);
1049
- });
1050
- const nodes2 = [];
1051
- agentsByQuest.forEach((questAgents, questId) => {
1052
- const questNode = questNodeById.get(`quest:${questId}`);
1053
- if (!questNode) return;
1054
- const columns = questAgents.length > 3 ? 2 : 1;
1055
- const rows = Math.ceil(questAgents.length / columns);
1056
- questAgents.forEach((agent, index) => {
1057
- const col = index % columns;
1058
- const row = Math.floor(index / columns);
1059
- const offsetX = 240 + col * 170;
1060
- const offsetY = (row - (rows - 1) / 2) * 90;
1061
- const nodeId = `agent:${questId}:${agent.instance_id}`;
1062
- const stored = storedNodes?.[nodeId] ?? null;
1063
- const position = stored ?? { x: questNode.position.x + offsetX, y: questNode.position.y + offsetY };
1064
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
1065
- const displayName = resolveAgentDisplayName(agent);
1066
- const avatar = resolveAgentLogo(agent, template);
1067
- const avatarColor = agent.avatar_frame_color?.trim() || pickAvatarFrameColor(agent.instance_id, index);
1068
- const statusLabelRaw = typeof agent.status === "string" ? agent.status.toLowerCase() : "idle";
1069
- const waitingForAnswer = waitingAnswerByAgentId.get(agent.instance_id) ?? false;
1070
- const isPaused = statusLabelRaw === "waiting" && !waitingForAnswer;
1071
- const effectiveStatus = statusLabelRaw === "waiting" && !waitingForAnswer ? "idle" : statusLabelRaw;
1072
- const isWaiting = waitingForAnswer;
1073
- const isWorking = !isWaiting && isLabWorkingStatus(effectiveStatus);
1074
- const statusTone = isWaiting ? "waiting" : isWorking ? "working" : normalizeLabStatus(effectiveStatus);
1075
- const statusState = isWaiting ? "waiting" : isWorking ? "working" : isPaused ? "paused" : "resting";
1076
- const statusLabel = isWaiting ? "Waiting for answer" : isWorking ? "Working" : isPaused ? "Paused" : "Resting";
1077
- nodes2.push({
1078
- id: nodeId,
1079
- type: "overviewAgent",
1080
- position,
1081
- draggable: !readOnly,
1082
- data: {
1083
- kind: "agent",
1084
- questId,
1085
- agentId: agent.instance_id,
1086
- name: displayName,
1087
- avatar,
1088
- avatarColor,
1089
- statusTone,
1090
- statusState,
1091
- statusLabel
1092
- }
1093
- });
1094
- });
1095
- });
1096
- return nodes2;
1097
- }, [
1098
- agents,
1099
- questNodes,
1100
- quests,
1101
- readOnly,
1102
- storedLayout?.nodes,
1103
- templatesById,
1104
- waitingAnswerByAgentId
1105
- ]);
1106
- const initialNodes = reactExports.useMemo(() => {
1107
- const piNodes = piNode ? [piNode] : [];
1108
- const pendingNodes = pendingNode ? [pendingNode] : [];
1109
- return [...pendingNodes, ...piNodes, ...questNodes, ...agentNodes];
1110
- }, [agentNodes, pendingNode, piNode, questNodes]);
1111
- const [nodes, setNodes, onNodesChange] = useNodesState(
1112
- initialNodes
1113
- );
1114
- const agentEdges = reactExports.useMemo(() => {
1115
- const baseStyle = { stroke: "var(--lab-border-strong)", strokeWidth: 1.2, strokeDasharray: "4 6" };
1116
- return agentNodes.map((node) => ({
1117
- id: `agent-edge:${node.data.questId}:${node.data.agentId}`,
1118
- source: `quest:${node.data.questId}`,
1119
- target: node.id,
1120
- type: "smoothstep",
1121
- style: baseStyle,
1122
- animated: false
1123
- }));
1124
- }, [agentNodes]);
1125
- const piQuestEdges = reactExports.useMemo(() => {
1126
- if (!piNode) return [];
1127
- return questNodes.map((questNode) => ({
1128
- id: `pi-edge:${piNode.id}:${questNode.id}`,
1129
- source: piNode.id,
1130
- target: questNode.id,
1131
- type: "smoothstep",
1132
- style: { stroke: "var(--lab-accent-strong)", strokeWidth: 1.4, strokeDasharray: "2 6" },
1133
- animated: false
1134
- }));
1135
- }, [piNode, questNodes]);
1136
- const edges = reactExports.useMemo(() => {
1137
- return [...piQuestEdges, ...agentEdges];
1138
- }, [agentEdges, piQuestEdges]);
1139
- const flow = useReactFlow();
1140
- reactExports.useEffect(() => {
1141
- nodesRef.current = nodes;
1142
- }, [nodes]);
1143
- reactExports.useEffect(() => {
1144
- setNodes((prev) => {
1145
- const prevById = new Map(prev.map((node) => [node.id, node]));
1146
- const piNodes = piNode ? [piNode] : [];
1147
- const pendingNodes = pendingNode ? [pendingNode] : [];
1148
- const next = [...pendingNodes, ...piNodes, ...questNodes, ...agentNodes].map((node) => {
1149
- const existing = prevById.get(node.id);
1150
- if (!existing) return node;
1151
- const preservePosition = node.type === "overviewQuest" || node.type === "overviewAgent" || node.type === "overviewPi" || node.type === "overviewPending";
1152
- return preservePosition ? { ...node, position: existing.position } : node;
1153
- });
1154
- if (prev.length === next.length) {
1155
- let unchanged = true;
1156
- for (let i = 0; i < next.length; i += 1) {
1157
- const a = prev[i];
1158
- const b = next[i];
1159
- if (!a || !b || a.id !== b.id) {
1160
- unchanged = false;
1161
- break;
1162
- }
1163
- if (a.position.x !== b.position.x || a.position.y !== b.position.y) {
1164
- unchanged = false;
1165
- break;
1166
- }
1167
- if (!isOverviewNodeDataEqual(a.data, b.data)) {
1168
- unchanged = false;
1169
- break;
1170
- }
1171
- }
1172
- if (unchanged) return prev;
1173
- }
1174
- return next;
1175
- });
1176
- }, [activeQuestId, agentNodes, pendingNode, piNode, questNodes, setNodes]);
1177
- reactExports.useEffect(() => {
1178
- scheduleSave();
1179
- }, [actionPanelState, overviewPanelState, scheduleSave]);
1180
- reactExports.useEffect(() => {
1181
- if (!nodes.length) return;
1182
- if (initialViewport) return;
1183
- if (fittedViewportRef.current) return;
1184
- let raf = 0;
1185
- let attempts = 0;
1186
- const tryFit = () => {
1187
- attempts += 1;
1188
- try {
1189
- flow.fitView({ padding: 0.2, duration: 450 });
1190
- fittedViewportRef.current = true;
1191
- } catch {
1192
- if (attempts < 8 && typeof window !== "undefined") {
1193
- raf = window.requestAnimationFrame(tryFit);
1194
- }
1195
- }
1196
- };
1197
- tryFit();
1198
- return () => {
1199
- if (raf && typeof window !== "undefined") {
1200
- window.cancelAnimationFrame(raf);
1201
- }
1202
- };
1203
- }, [flow, initialViewport, nodes.length]);
1204
- reactExports.useEffect(() => {
1205
- if (!nodes.length) return;
1206
- if (!initialViewport) return;
1207
- if (fittedViewportRef.current) return;
1208
- if (viewportCheckedRef.current) return;
1209
- const bounds = boundsRef.current;
1210
- if (!bounds) return;
1211
- const rect = bounds.getBoundingClientRect();
1212
- if (!rect.width || !rect.height) return;
1213
- const tx = initialViewport.x;
1214
- const ty = initialViewport.y;
1215
- const zoom = initialViewport.zoom;
1216
- const padding = 160;
1217
- const anyVisible = nodes.some((node) => {
1218
- const sx = node.position.x * zoom + tx;
1219
- const sy = node.position.y * zoom + ty;
1220
- return sx > -padding && sx < rect.width + padding && sy > -padding && sy < rect.height + padding;
1221
- });
1222
- if (!anyVisible) {
1223
- let raf = 0;
1224
- let attempts = 0;
1225
- const tryFit = () => {
1226
- attempts += 1;
1227
- try {
1228
- flow.fitView({ padding: 0.2, duration: 450 });
1229
- fittedViewportRef.current = true;
1230
- } catch {
1231
- if (attempts < 8 && typeof window !== "undefined") {
1232
- raf = window.requestAnimationFrame(tryFit);
1233
- }
1234
- }
1235
- };
1236
- tryFit();
1237
- return () => {
1238
- if (raf && typeof window !== "undefined") {
1239
- window.cancelAnimationFrame(raf);
1240
- }
1241
- };
1242
- }
1243
- viewportCheckedRef.current = true;
1244
- }, [flow, initialViewport, nodes]);
1245
- reactExports.useEffect(() => {
1246
- return () => {
1247
- if (saveTimeoutRef.current) {
1248
- window.clearTimeout(saveTimeoutRef.current);
1249
- }
1250
- };
1251
- }, []);
1252
- reactExports.useLayoutEffect(() => {
1253
- setCanvasBoundsReady(false);
1254
- const bounds = boundsRef.current;
1255
- if (!bounds || typeof window === "undefined") return;
1256
- let raf = 0;
1257
- let attempts = 0;
1258
- const check = () => {
1259
- attempts += 1;
1260
- const rect = bounds.getBoundingClientRect();
1261
- if (rect.width > 0 && rect.height > 0) {
1262
- setCanvasBoundsReady(true);
1263
- return;
1264
- }
1265
- if (attempts < 6) {
1266
- raf = window.requestAnimationFrame(check);
1267
- }
1268
- };
1269
- check();
1270
- return () => {
1271
- if (raf) {
1272
- window.cancelAnimationFrame(raf);
1273
- }
1274
- };
1275
- }, [projectId]);
1276
- return /* @__PURE__ */ jsxRuntimeExports.jsx(OverviewPendingActionPanelContext.Provider, { value: hasPendingNode ? actionPanel : null, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: boundsRef, className: cn("lab-overview-canvas", isOpsMode && "lab-overview-canvas--ops"), children: [
1277
- !canvasBoundsReady ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3 p-4", children: showFloatingPanels ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1278
- !isOpsMode ? overviewPanel : null,
1279
- actionPanel
1280
- ] }) : null }) : null,
1281
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lab-quest-graph-shell lab-quest-graph-shell--full", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1282
- index,
1283
- {
1284
- nodes,
1285
- edges,
1286
- nodeTypes: OVERVIEW_NODE_TYPES,
1287
- onNodesChange,
1288
- nodesDraggable: !readOnly,
1289
- panOnDrag: true,
1290
- zoomOnScroll: true,
1291
- zoomOnPinch: true,
1292
- minZoom: 0.15,
1293
- maxZoom: 3,
1294
- defaultViewport: initialViewport,
1295
- onNodeDragStop: () => scheduleSave(),
1296
- onMoveEnd: (_, viewport) => {
1297
- viewportRef.current = viewport;
1298
- scheduleSave();
1299
- },
1300
- onNodeClick: (_, node) => {
1301
- const data = node.data;
1302
- if (data && typeof data === "object" && data.kind === "pending") {
1303
- dispatchLabFocus({
1304
- projectId,
1305
- focusType: "overview",
1306
- focusId: projectId
1307
- });
1308
- if (showFloatingPanels && showActionPanelFloating) {
1309
- bringToFront("action");
1310
- setActionPanelState((prev) => ({ ...prev, collapsed: false }));
1311
- }
1312
- return;
1313
- }
1314
- if (data && typeof data === "object" && data.kind === "agent") {
1315
- const payload = data;
1316
- onSelectAgent?.(payload.agentId, payload.questId);
1317
- return;
1318
- }
1319
- if (data && typeof data === "object" && data.kind === "pi") {
1320
- const payload = data;
1321
- onSelectAgent?.(payload.agentId, null);
1322
- onOpenPiChat?.();
1323
- return;
1324
- }
1325
- const questId = node.data?.questId;
1326
- if (!questId) return;
1327
- dispatchLabFocus({
1328
- projectId,
1329
- focusType: "quest",
1330
- focusId: questId
1331
- });
1332
- onOpenCanvas?.(questId);
1333
- },
1334
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Background, { color: "var(--lab-border)", gap: 28, size: 1, variant: "dots" })
1335
- }
1336
- ) }),
1337
- canvasBoundsReady ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1338
- showFloatingPanels && !isOpsMode ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1339
- FloatingPanel,
1340
- {
1341
- boundsRef,
1342
- title: t("overview_overall"),
1343
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(LayoutDashboard, { size: 16 }),
1344
- state: overviewPanelState,
1345
- zIndex: panelZ.overview,
1346
- onChange: setOverviewPanelState,
1347
- onActivate: () => bringToFront("overview"),
1348
- className: "lab-overview-panel--overview",
1349
- children: overviewPanel
1350
- }
1351
- ) : null,
1352
- showFloatingPanels && showActionPanelFloating ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1353
- FloatingPanel,
1354
- {
1355
- boundsRef,
1356
- title: t("overview_action_center"),
1357
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Inbox, { size: 16 }),
1358
- state: actionPanelState,
1359
- zIndex: panelZ.action,
1360
- onChange: setActionPanelState,
1361
- onActivate: () => bringToFront("action"),
1362
- className: "lab-overview-panel--action",
1363
- children: actionPanel
1364
- }
1365
- ) : null
1366
- ] }) : null
1367
- ] }) });
1368
- }
1369
- function LabOverviewCanvas(props) {
1370
- return /* @__PURE__ */ jsxRuntimeExports.jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewCanvasInner, { ...props }) });
1371
- }
1372
-
1373
- const formatStateLabel = (value) => {
1374
- const normalized = String(value || "").trim().replace(/[_-]+/g, " ");
1375
- if (!normalized) return "N/A";
1376
- return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
1377
- };
1378
- function LabStatusPill({
1379
- children,
1380
- mono = false
1381
- }) {
1382
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
1383
- "span",
1384
- {
1385
- className: cn(
1386
- "inline-flex max-w-full items-center rounded-full border border-[var(--lab-border)] px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.14em] text-[var(--lab-text-secondary)]",
1387
- mono && "font-mono normal-case tracking-[0.02em]"
1388
- ),
1389
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children })
1390
- }
1391
- );
1392
- }
1393
- function LabDetailSection({
1394
- title,
1395
- hint,
1396
- actions,
1397
- children,
1398
- first = false
1399
- }) {
1400
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1401
- "section",
1402
- {
1403
- className: cn(
1404
- "py-5",
1405
- first ? "pt-0" : "border-t border-dashed border-[var(--lab-border)]"
1406
- ),
1407
- children: [
1408
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-5 flex flex-wrap items-start justify-between gap-3", children: [
1409
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1410
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--lab-text-secondary)]", children: title }),
1411
- hint ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-sm leading-7 text-[var(--lab-text-secondary)]", children: hint }) : null
1412
- ] }),
1413
- actions ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0", children: actions }) : null
1414
- ] }),
1415
- children
1416
- ]
1417
- }
1418
- );
1419
- }
1420
- function LabOverviewMetric({
1421
- icon,
1422
- label,
1423
- value,
1424
- hint
1425
- }) {
1426
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1427
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-[var(--lab-text-secondary)]", children: [
1428
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0", children: icon }),
1429
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: label })
1430
- ] }),
1431
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 break-words text-[15px] font-semibold leading-6 text-[var(--lab-text-primary)]", children: value }),
1432
- hint ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 break-words text-sm leading-6 text-[var(--lab-text-secondary)]", children: hint }) : null
1433
- ] });
1434
- }
1435
- function LabFactRows({
1436
- items
1437
- }) {
1438
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-dashed divide-[var(--lab-border)]", children: items.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-2 py-3 sm:grid-cols-[130px_minmax(0,1fr)]", children: [
1439
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] uppercase tracking-[0.16em] text-[var(--lab-text-secondary)]", children: item.label }),
1440
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "break-words text-sm leading-7 text-[var(--lab-text-primary)]", children: item.value })
1441
- ] }, item.label)) });
1442
- }
1443
- function semanticToneBadgeClass(tone) {
1444
- if (tone === "truth") {
1445
- return "border-[rgba(64,113,175,0.24)] bg-[rgba(64,113,175,0.1)] text-[#315c97] dark:text-[#9ec5ff]";
1446
- }
1447
- if (tone === "runtime") {
1448
- return "border-[rgba(99,102,241,0.24)] bg-[rgba(99,102,241,0.1)] text-[#4f46e5] dark:text-[#c7d2fe]";
1449
- }
1450
- if (tone === "overlay") {
1451
- return "border-[rgba(83,176,174,0.26)] bg-[rgba(83,176,174,0.12)] text-[#0f766e] dark:text-[#8be4db]";
1452
- }
1453
- return "border-[rgba(148,163,184,0.26)] bg-[rgba(148,163,184,0.12)] text-[var(--lab-text-secondary)]";
1454
- }
1455
- function LabCanvasStudio({
1456
- projectId,
1457
- readOnly,
1458
- onRefresh,
1459
- onOpenStageSelection,
1460
- cliStatus,
1461
- labStream,
1462
- projectName,
1463
- templates,
1464
- agents,
1465
- quests,
1466
- overview,
1467
- isLoading,
1468
- lockedQuestId = null,
1469
- immersiveLockedQuest = false
1470
- }) {
1471
- const { t } = useI18n("lab");
1472
- const queryClient = useQueryClient();
1473
- const setActiveQuest = useLabCopilotStore((state) => state.setActiveQuest);
1474
- const selection = useLabGraphSelectionStore((state) => state.selection);
1475
- const clearGraphSelection = useLabGraphSelectionStore((state) => state.clear);
1476
- const [selectedQuestIdState, setSelectedQuestIdState] = reactExports.useState(null);
1477
- const [selectedQuestBranch, setSelectedQuestBranch] = reactExports.useState(null);
1478
- const [selectedQuestEventId, setSelectedQuestEventId] = reactExports.useState(null);
1479
- const [selectedAgentId, setSelectedAgentId] = reactExports.useState(null);
1480
- const selectedQuestId = lockedQuestId ?? selectedQuestIdState;
1481
- const templatesById = reactExports.useMemo(
1482
- () => new Map(templates.map((template) => [template.template_id, template])),
1483
- [templates]
1484
- );
1485
- const selectedQuest = reactExports.useMemo(
1486
- () => quests.find((quest) => quest.quest_id === selectedQuestId) ?? null,
1487
- [quests, selectedQuestId]
1488
- );
1489
- const selectedAgent = reactExports.useMemo(
1490
- () => agents.find((agent) => agent.instance_id === selectedAgentId) ?? null,
1491
- [agents, selectedAgentId]
1492
- );
1493
- const piAgentInstance = reactExports.useMemo(() => {
1494
- const piTemplates = templates.filter(
1495
- (template) => ["pi", "principal-investigator"].includes(String(template.template_key || "").trim().toLowerCase())
1496
- );
1497
- if (piTemplates.length) {
1498
- const fromTemplate = agents.find((agent) => piTemplates.some((template) => agent.template_id === template.template_id)) ?? null;
1499
- if (fromTemplate) return fromTemplate;
1500
- }
1501
- return agents.find((agent) => {
1502
- const agentId = String(agent.agent_id || "").trim().toLowerCase();
1503
- const mention = String(agent.mention_label || "").trim().replace(/^@/, "").toLowerCase();
1504
- return agentId === "pi" || mention === "pi" || agentId.endsWith(":pi") || String(agent.template_id || "").trim().toLowerCase().includes("principal");
1505
- }) ?? null;
1506
- }, [agents, templates]);
1507
- const piTemplate = piAgentInstance?.template_id ? templatesById.get(piAgentInstance.template_id) ?? null : null;
1508
- const piAgent = piAgentInstance ? {
1509
- name: resolveAgentDisplayName(piAgentInstance),
1510
- logo: resolveAgentLogo(piAgentInstance, piTemplate),
1511
- frameColor: piAgentInstance.avatar_frame_color || pickAvatarFrameColor(piAgentInstance.instance_id, 0)
1512
- } : null;
1513
- const pendingDecisionCount = reactExports.useMemo(() => {
1514
- const fromGraph = overview.graph_vm?.project?.pendingDecisionCount;
1515
- if (typeof fromGraph === "number") return Math.max(0, fromGraph);
1516
- return quests.reduce((sum, quest) => sum + Math.max(0, Number(quest.pending_question_count ?? 0)), 0);
1517
- }, [overview.graph_vm?.project?.pendingDecisionCount, quests]);
1518
- reactExports.useEffect(() => {
1519
- if (lockedQuestId) {
1520
- return;
1521
- }
1522
- if (selectedQuestId && !selectedQuest && !isLoading.quests) {
1523
- setSelectedQuestIdState(null);
1524
- setSelectedQuestBranch(null);
1525
- setSelectedQuestEventId(null);
1526
- clearGraphSelection();
1527
- }
1528
- }, [clearGraphSelection, isLoading.quests, lockedQuestId, selectedQuest, selectedQuestId]);
1529
- reactExports.useEffect(() => {
1530
- if (!lockedQuestId) return;
1531
- setSelectedQuestBranch((current) => current ?? null);
1532
- setSelectedQuestEventId((current) => current ?? null);
1533
- setSelectedAgentId(null);
1534
- }, [lockedQuestId]);
1535
- reactExports.useEffect(() => {
1536
- setActiveQuest(selectedQuestId);
1537
- }, [selectedQuestId, setActiveQuest]);
1538
- reactExports.useEffect(() => {
1539
- const handler = (event) => {
1540
- const detail = event.detail;
1541
- if (!detail) return;
1542
- if (detail.projectId && detail.projectId !== projectId) return;
1543
- if (detail.focusType === "overview" || detail.focusType === "canvas") {
1544
- if (!lockedQuestId) {
1545
- setSelectedQuestIdState(null);
1546
- }
1547
- setSelectedQuestBranch(null);
1548
- setSelectedQuestEventId(null);
1549
- setSelectedAgentId(null);
1550
- clearGraphSelection();
1551
- return;
1552
- }
1553
- if (detail.focusType === "agent") {
1554
- setSelectedAgentId(detail.focusId ?? null);
1555
- return;
1556
- }
1557
- if (detail.focusType === "quest" || detail.focusType === "quest-branch" || detail.focusType === "quest-event") {
1558
- if (lockedQuestId && detail.focusId && detail.focusId !== lockedQuestId) {
1559
- return;
1560
- }
1561
- if (!lockedQuestId) {
1562
- setSelectedQuestIdState(detail.focusId ?? null);
1563
- }
1564
- setSelectedQuestBranch(detail.branch ?? null);
1565
- setSelectedQuestEventId(detail.eventId ?? null);
1566
- setSelectedAgentId(null);
1567
- clearGraphSelection();
1568
- }
1569
- };
1570
- window.addEventListener(LAB_FOCUS_EVENT$1, handler);
1571
- return () => window.removeEventListener(LAB_FOCUS_EVENT$1, handler);
1572
- }, [clearGraphSelection, lockedQuestId, projectId]);
1573
- const selectedQuestDetailQuery = useQuery({
1574
- queryKey: ["lab-quest-detail", projectId, selectedQuestId, "canvas-studio"],
1575
- queryFn: () => getLabQuest(projectId, selectedQuestId),
1576
- enabled: Boolean(projectId && selectedQuestId),
1577
- staleTime: 1e4
1578
- });
1579
- const selectedQuestSummaryQuery = useQuery({
1580
- queryKey: ["lab-quest-summary", projectId, selectedQuestId, selectedQuestEventId ?? null, "canvas-studio"],
1581
- queryFn: () => getLabQuestSummary(projectId, selectedQuestId, {
1582
- atEventId: selectedQuestEventId
1583
- }),
1584
- enabled: Boolean(projectId && selectedQuestId),
1585
- staleTime: 1e4
1586
- });
1587
- const selectedEventPayloadQuery = useQuery({
1588
- queryKey: [
1589
- "lab-quest-event-payload",
1590
- projectId,
1591
- selectedQuestId,
1592
- selection?.selection_type ?? null,
1593
- selection?.selection_ref ?? null,
1594
- "canvas-studio"
1595
- ],
1596
- queryFn: () => getLabQuestEventPayload(projectId, selectedQuestId, selection?.selection_ref, {
1597
- maxBytes: 1e5
1598
- }),
1599
- enabled: Boolean(
1600
- projectId && selectedQuestId && selection?.selection_type === "event_node" && selection?.selection_ref
1601
- ),
1602
- staleTime: 1e4
1603
- });
1604
- const selectedNodeTraceQuery = useQuery({
1605
- queryKey: [
1606
- "lab-quest-node-trace",
1607
- projectId,
1608
- selectedQuestId,
1609
- selection?.selection_type ?? null,
1610
- selection?.selection_ref ?? null,
1611
- "canvas-studio"
1612
- ],
1613
- queryFn: () => getLabQuestNodeTrace(projectId, selectedQuestId, selection?.selection_ref, {
1614
- selectionType: selection?.selection_type ?? null
1615
- }),
1616
- enabled: Boolean(
1617
- projectId && selectedQuestId && selection?.selection_ref && ["branch_node", "event_node", "stage_node"].includes(String(selection?.selection_type || ""))
1618
- ),
1619
- staleTime: 1e4
1620
- });
1621
- const selectionSemantic = reactExports.useMemo(
1622
- () => resolveLabCanvasSelectionSemantic({
1623
- selectionType: selection?.selection_type,
1624
- edgeType: selection?.summary ?? selection?.selection_ref ?? null,
1625
- hasActiveProposal: false
1626
- }),
1627
- [selection?.selection_ref, selection?.selection_type, selection?.summary]
1628
- );
1629
- const handleRefresh = reactExports.useCallback(() => {
1630
- void Promise.resolve(onRefresh?.());
1631
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
1632
- queryClient.invalidateQueries({ queryKey: ["lab-quests", projectId] });
1633
- queryClient.invalidateQueries({ queryKey: ["lab-overview", projectId] });
1634
- if (selectedQuestId) {
1635
- queryClient.invalidateQueries({ queryKey: ["lab-quest-detail", projectId, selectedQuestId] });
1636
- queryClient.invalidateQueries({ queryKey: ["lab-quest-summary", projectId, selectedQuestId] });
1637
- queryClient.invalidateQueries({ queryKey: ["lab-quest-graph", projectId, selectedQuestId] });
1638
- queryClient.invalidateQueries({ queryKey: ["lab-quest-node-trace", projectId, selectedQuestId] });
1639
- }
1640
- }, [onRefresh, projectId, queryClient, selectedQuestId]);
1641
- const openQuestCanvas = reactExports.useCallback(
1642
- (questId, branch, eventId) => {
1643
- if (lockedQuestId && questId !== lockedQuestId) {
1644
- return;
1645
- }
1646
- if (!lockedQuestId) {
1647
- setSelectedQuestIdState(questId);
1648
- }
1649
- setSelectedQuestBranch(branch ?? null);
1650
- setSelectedQuestEventId(eventId ?? null);
1651
- setSelectedAgentId(null);
1652
- clearGraphSelection();
1653
- },
1654
- [clearGraphSelection, lockedQuestId]
1655
- );
1656
- const closeQuestCanvas = reactExports.useCallback(() => {
1657
- if (!lockedQuestId) {
1658
- setSelectedQuestIdState(null);
1659
- }
1660
- setSelectedQuestBranch(null);
1661
- setSelectedQuestEventId(null);
1662
- clearGraphSelection();
1663
- }, [clearGraphSelection, lockedQuestId]);
1664
- const renderOverviewDetail = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-0", children: [
1665
- isLoading.quests || isLoading.agents || isLoading.overview ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
1666
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-24 w-full rounded-[18px]" }),
1667
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-20 w-full rounded-[18px]" })
1668
- ] }) : null,
1669
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1670
- LabDetailSection,
1671
- {
1672
- first: true,
1673
- title: "Overall",
1674
- hint: "Select a quest node to enter its canvas. After that, click a branch, event, or stage node to inspect its durable detail state here.",
1675
- children: [
1676
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1677
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { children: "Home" }),
1678
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { children: formatStateLabel(cliStatus) }),
1679
- /* @__PURE__ */ jsxRuntimeExports.jsxs(LabStatusPill, { children: [
1680
- quests.length,
1681
- " quests"
1682
- ] }),
1683
- /* @__PURE__ */ jsxRuntimeExports.jsxs(LabStatusPill, { children: [
1684
- agents.length,
1685
- " agents"
1686
- ] })
1687
- ] }),
1688
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex items-center gap-2", children: [
1689
- /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-4 w-4 text-[#4071af]" }),
1690
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[22px] font-semibold tracking-[-0.03em] text-[var(--lab-text-primary)]", children: projectName || t("plugin_home_title", void 0, "Home") })
1691
- ] })
1692
- ]
1693
- }
1694
- ),
1695
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1696
- LabDetailSection,
1697
- {
1698
- title: "Operational Status",
1699
- hint: "Keep this panel focused on high-signal canvas state rather than dashboard clutter.",
1700
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-x-8 gap-y-5 sm:grid-cols-2", children: [
1701
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(GitBranch, { className: "h-4 w-4" }), label: "Quests", value: quests.length, hint: "Durable research repositories tracked in this project." }),
1702
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Bot, { className: "h-4 w-4" }), label: "Agents", value: agents.length, hint: "Live agent instances visible to the Lab surface." }),
1703
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-4 w-4" }), label: "Pending", value: pendingDecisionCount, hint: "User decisions or unresolved blocking items." }),
1704
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "h-4 w-4" }), label: "CLI", value: formatStateLabel(cliStatus), hint: "Shared runtime binding for this project workspace." })
1705
- ] })
1706
- }
1707
- ),
1708
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1709
- LabDetailSection,
1710
- {
1711
- title: "Next Step",
1712
- hint: "The Lab plugin is now canvas-first. Explorer handles navigation, center tabs handle content, and this rail only summarizes context.",
1713
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1714
- LabFactRows,
1715
- {
1716
- items: [
1717
- { label: "Canvas", value: "Open a quest node to switch from map view into its research graph." },
1718
- { label: "Selection", value: "Branch, event, and stage nodes expose stable detail summaries here." },
1719
- { label: "Explorer", value: "Changed files now open in center tabs instead of rendering inline in the navigator." }
1720
- ]
1721
- }
1722
- )
1723
- }
1724
- ),
1725
- selectedAgent ? renderAgentDetail(selectedAgent) : null
1726
- ] });
1727
- const renderAgentDetail = (agent) => {
1728
- const template = agent.template_id ? templatesById.get(agent.template_id) ?? null : null;
1729
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1730
- LabDetailSection,
1731
- {
1732
- title: "Agent",
1733
- hint: "Current agent focus selected from the overview canvas.",
1734
- children: [
1735
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
1736
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1737
- "div",
1738
- {
1739
- className: "flex h-10 w-10 items-center justify-center rounded-full border border-[var(--lab-border)] bg-[var(--lab-background)]",
1740
- style: {
1741
- boxShadow: `0 0 0 2px ${agent.avatar_frame_color || pickAvatarFrameColor(agent.instance_id)}`
1742
- },
1743
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Bot, { className: "h-4 w-4 text-[var(--lab-text-primary)]" })
1744
- }
1745
- ),
1746
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1747
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "truncate text-sm font-semibold text-[var(--lab-text-primary)]", children: resolveAgentDisplayName(agent) }),
1748
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "truncate text-xs text-[var(--lab-text-secondary)]", children: template?.template_key || agent.agent_id })
1749
- ] })
1750
- ] }),
1751
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-5", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1752
- LabFactRows,
1753
- {
1754
- items: [
1755
- { label: "Status", value: formatStateLabel(agent.status) },
1756
- { label: "Quest", value: agent.active_quest_id || "Unassigned" },
1757
- { label: "Branch", value: agent.active_quest_branch || "N/A" },
1758
- { label: "Stage", value: agent.active_quest_stage_key || "N/A" }
1759
- ]
1760
- }
1761
- ) })
1762
- ]
1763
- }
1764
- );
1765
- };
1766
- const renderQuestDetail = () => {
1767
- if (!selectedQuestId) return null;
1768
- const questDetail = selectedQuestDetailQuery.data?.quest ?? selectedQuest;
1769
- const questSummary = selectedQuestSummaryQuery.data?.quest ?? null;
1770
- const questRuntime = questSummary?.runtime ?? null;
1771
- const questGovernance = questSummary?.governance ?? null;
1772
- const pushStatus = questGovernance?.lastPushStatus || (typeof questDetail?.github_push?.status === "string" ? questDetail.github_push.status : null) || "N/A";
1773
- if (selectedQuestDetailQuery.isLoading && !questDetail) {
1774
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
1775
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-28 w-full rounded-[18px]" }),
1776
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-20 w-full rounded-[18px]" }),
1777
- /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-20 w-full rounded-[18px]" })
1778
- ] });
1779
- }
1780
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-0", children: [
1781
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1782
- LabDetailSection,
1783
- {
1784
- first: true,
1785
- title: "Overall",
1786
- hint: questDetail?.description?.trim() || questDetail?.summary?.trim() || "No quest description yet.",
1787
- children: [
1788
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1789
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { children: formatStateLabel(questDetail?.status) }),
1790
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { children: questSummary?.topology?.headBranch || questDetail?.git_head_branch || "main" }),
1791
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { mono: true, children: selectedQuestId }),
1792
- selectedQuestBranch ? /* @__PURE__ */ jsxRuntimeExports.jsx(LabStatusPill, { children: selectedQuestBranch }) : null
1793
- ] }),
1794
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 text-[22px] font-semibold tracking-[-0.03em] text-[var(--lab-text-primary)]", children: questDetail?.title || selectedQuestId }),
1795
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-5 grid gap-x-8 gap-y-5 sm:grid-cols-2", children: [
1796
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(GitBranch, { className: "h-4 w-4" }), label: "Branches", value: questSummary?.topology?.branchCount ?? 0, hint: `Head ${questSummary?.topology?.headBranch || questDetail?.git_head_branch || "main"}` }),
1797
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Bot, { className: "h-4 w-4" }), label: "Running Agents", value: questRuntime?.runningAgents ?? 0, hint: "Live agents attached to this quest." }),
1798
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-4 w-4" }), label: "Pending Questions", value: Math.max(0, Number(questDetail?.pending_question_count ?? 0)), hint: "Open decisions or direct user replies still waiting." }),
1799
- /* @__PURE__ */ jsxRuntimeExports.jsx(LabOverviewMetric, { icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "h-4 w-4" }), label: "Push Status", value: pushStatus, hint: questDetail?.last_event_at ? `Last event ${formatRelativeTime(questDetail.last_event_at)}` : "No event timestamp yet." })
1800
- ] })
1801
- ]
1802
- }
1803
- ),
1804
- selection ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1805
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1806
- LabDetailSection,
1807
- {
1808
- title: "Selection",
1809
- hint: selection.summary || "No structured summary is attached to this node yet.",
1810
- children: [
1811
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1812
- selectionSemantic ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1813
- "span",
1814
- {
1815
- className: cn(
1816
- "inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold",
1817
- semanticToneBadgeClass(selectionSemantic.tone)
1818
- ),
1819
- children: t(
1820
- LAB_CANVAS_SEMANTIC_TONE_META[selectionSemantic.tone].labelKey,
1821
- void 0,
1822
- LAB_CANVAS_SEMANTIC_TONE_META[selectionSemantic.tone].labelDefault
1823
- )
1824
- }
1825
- ) : null,
1826
- /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel(selection.selection_type) })
1827
- ] }),
1828
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 text-base font-semibold text-[var(--lab-text-primary)]", children: selection.label || selection.selection_ref }),
1829
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-5", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1830
- LabFactRows,
1831
- {
1832
- items: [
1833
- { label: "Ref", value: selection.selection_ref },
1834
- { label: "Branch No", value: selection.branch_no || "N/A" },
1835
- { label: "Branch", value: selection.branch_name || "N/A" },
1836
- { label: "Parent", value: selection.parent_branch || "N/A" },
1837
- { label: "Foundation", value: selection.foundation_label || "N/A" },
1838
- { label: "Stage", value: selection.stage_key || "N/A" },
1839
- { label: "Worktree", value: selection.worktree_rel_path || "N/A" },
1840
- { label: "Agent", value: selection.agent_instance_id || "N/A" }
1841
- ]
1842
- }
1843
- ) })
1844
- ]
1845
- }
1846
- ),
1847
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1848
- LabDetailSection,
1849
- {
1850
- title: "Trace",
1851
- hint: "Durable actions, artifact payloads, and commit evidence attached to the selected node.",
1852
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1853
- LabNodeTraceDetail,
1854
- {
1855
- projectId,
1856
- questId: selectedQuestId,
1857
- trace: selectedNodeTraceQuery.data?.trace ?? null,
1858
- isLoading: selectedNodeTraceQuery.isLoading,
1859
- payloadJson: selectedEventPayloadQuery.data?.payload_json ?? null,
1860
- payloadTruncated: selectedEventPayloadQuery.data?.truncated ?? null
1861
- }
1862
- )
1863
- }
1864
- )
1865
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
1866
- LabDetailSection,
1867
- {
1868
- title: "Operational Status",
1869
- hint: "Quest-level runtime, topology, and governance signals.",
1870
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1871
- LabFactRows,
1872
- {
1873
- items: [
1874
- { label: "Head Branch", value: questSummary?.topology?.headBranch || questDetail?.git_head_branch || "main" },
1875
- { label: "Pending Questions", value: Math.max(0, Number(questDetail?.pending_question_count ?? 0)) },
1876
- { label: "Branch Count", value: questSummary?.topology?.branchCount ?? 0 },
1877
- { label: "Running Agents", value: questRuntime?.runningAgents ?? 0 },
1878
- { label: "Push Status", value: pushStatus },
1879
- { label: "Last Event", value: questDetail?.last_event_at ? formatRelativeTime(questDetail.last_event_at) : "N/A" },
1880
- { label: "Created", value: questDetail?.created_at ? formatRelativeTime(questDetail.created_at) : "N/A" }
1881
- ]
1882
- }
1883
- )
1884
- }
1885
- )
1886
- ] });
1887
- };
1888
- const canvasTitle = selectedQuest ? selectedQuest.title || selectedQuest.quest_id : projectName || t("plugin_home_title", void 0, "Home");
1889
- const canvasSubtitle = selectedQuest ? lockedQuestId ? "Research canvas" : "Quest canvas" : "Quest map";
1890
- if (lockedQuestId && immersiveLockedQuest && selectedQuestId) {
1891
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex h-full min-h-0 w-full flex-1 overflow-hidden", children: [
1892
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-4 top-4 z-20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
1893
- Button,
1894
- {
1895
- type: "button",
1896
- size: "sm",
1897
- variant: "outline",
1898
- className: "h-9 rounded-full border-[var(--lab-border)] bg-[rgba(255,255,255,0.94)] px-3 text-[11px] text-[var(--lab-text-primary)] shadow-[0_10px_28px_rgba(15,23,42,0.08)] backdrop-blur dark:bg-[rgba(28,29,34,0.94)]",
1899
- onClick: handleRefresh,
1900
- children: [
1901
- /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "mr-1.5 h-3.5 w-3.5" }),
1902
- "Refresh"
1903
- ]
1904
- }
1905
- ) }),
1906
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full min-h-0 w-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1907
- LabQuestGraphCanvas,
1908
- {
1909
- projectId,
1910
- questId: selectedQuestId,
1911
- readOnly,
1912
- highlightBranch: selectedQuestBranch,
1913
- atEventId: selectedQuestEventId,
1914
- preferredViewMode: selectedQuestEventId ? "event" : "branch",
1915
- activeBranch: selectedQuestBranch,
1916
- onBranchSelect: (branch) => {
1917
- setSelectedQuestBranch(branch);
1918
- setSelectedQuestEventId(null);
1919
- },
1920
- onEventSelect: (eventId, branchName) => {
1921
- setSelectedQuestEventId(eventId);
1922
- if (branchName) {
1923
- setSelectedQuestBranch(branchName);
1924
- }
1925
- },
1926
- showFloatingPanels: false,
1927
- minimalChrome: true,
1928
- onStageOpen: onOpenStageSelection
1929
- }
1930
- ) })
1931
- ] });
1932
- }
1933
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex h-full min-h-0 flex-col gap-4", children: [
1934
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-[20px] border border-[var(--lab-border)] bg-[var(--lab-surface)] px-4 py-3", children: [
1935
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1936
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1937
- selectedQuestId && !lockedQuestId ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1938
- Button,
1939
- {
1940
- type: "button",
1941
- size: "sm",
1942
- variant: "ghost",
1943
- className: "h-8 px-2",
1944
- onClick: closeQuestCanvas,
1945
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { className: "h-4 w-4" })
1946
- }
1947
- ) : null,
1948
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "truncate text-sm font-semibold text-[var(--lab-text-primary)]", children: canvasTitle })
1949
- ] }),
1950
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--lab-text-secondary)]", children: [
1951
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: canvasSubtitle }),
1952
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "·" }),
1953
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
1954
- quests.length,
1955
- " quests"
1956
- ] }),
1957
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "·" }),
1958
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
1959
- agents.length,
1960
- " agents"
1961
- ] }),
1962
- labStream?.status && labStream.status !== "idle" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1963
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "·" }),
1964
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
1965
- "stream ",
1966
- labStream.status
1967
- ] })
1968
- ] }) : null
1969
- ] })
1970
- ] }),
1971
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1972
- selectedQuestBranch ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: selectedQuestBranch }) : null,
1973
- /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: formatStateLabel(cliStatus) }),
1974
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1975
- Button,
1976
- {
1977
- type: "button",
1978
- size: "sm",
1979
- variant: "outline",
1980
- className: "h-8 px-3",
1981
- onClick: handleRefresh,
1982
- children: [
1983
- /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "mr-1.5 h-3.5 w-3.5" }),
1984
- "Refresh"
1985
- ]
1986
- }
1987
- )
1988
- ] })
1989
- ] }),
1990
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid min-h-0 flex-1 gap-4 xl:grid-cols-[minmax(0,1fr)_360px]", children: [
1991
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "min-h-0 overflow-hidden rounded-[24px] border border-[var(--lab-border)] bg-[var(--lab-surface)]", children: selectedQuestId ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1992
- LabQuestGraphCanvas,
1993
- {
1994
- projectId,
1995
- questId: selectedQuestId,
1996
- readOnly,
1997
- highlightBranch: selectedQuestBranch,
1998
- atEventId: selectedQuestEventId,
1999
- preferredViewMode: selectedQuestEventId ? "event" : "branch",
2000
- activeBranch: selectedQuestBranch,
2001
- onBranchSelect: (branch) => {
2002
- setSelectedQuestBranch(branch);
2003
- setSelectedQuestEventId(null);
2004
- },
2005
- onEventSelect: (eventId, branchName) => {
2006
- setSelectedQuestEventId(eventId);
2007
- if (branchName) {
2008
- setSelectedQuestBranch(branchName);
2009
- }
2010
- },
2011
- showFloatingPanels: false,
2012
- onStageOpen: onOpenStageSelection
2013
- }
2014
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
2015
- LabOverviewCanvas,
2016
- {
2017
- projectId,
2018
- quests,
2019
- agents,
2020
- templates,
2021
- graphVm: overview.graph_vm ?? null,
2022
- pendingDecisionCount,
2023
- activeQuestId: selectedQuestId,
2024
- hasPiAgent: Boolean(piAgent),
2025
- piAgent,
2026
- readOnly,
2027
- actionPanel: null,
2028
- overviewPanel: null,
2029
- showFloatingPanels: false,
2030
- onOpenCanvas: openQuestCanvas,
2031
- onSelectAgent: (agentId) => {
2032
- setSelectedAgentId(agentId);
2033
- }
2034
- }
2035
- ) }),
2036
- /* @__PURE__ */ jsxRuntimeExports.jsx("aside", { className: "min-h-0 overflow-hidden rounded-[24px] border border-[var(--lab-border)] bg-[var(--lab-surface)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, { className: "h-full", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3 p-4", children: [
2037
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
2038
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2039
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-semibold text-[var(--lab-text-primary)]", children: "Details" }),
2040
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-xs text-[var(--lab-text-secondary)]", children: selectedQuestId ? "Selected node and quest context." : "Overview context and node guidance." })
2041
- ] }),
2042
- selectedQuestId ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2043
- Button,
2044
- {
2045
- type: "button",
2046
- size: "sm",
2047
- variant: "ghost",
2048
- className: "h-8 px-3",
2049
- onClick: () => {
2050
- clearGraphSelection();
2051
- setSelectedQuestEventId(null);
2052
- },
2053
- children: "Clear node"
2054
- }
2055
- ) : null
2056
- ] }),
2057
- selectedQuestId ? renderQuestDetail() : renderOverviewDetail()
2058
- ] }) }) })
2059
- ] })
2060
- ] });
2061
- }
2062
-
2063
- const INVALIDATION_BATCH_MS = 80;
2064
- const AGENT_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.agent.changed", "lab.agent.status"]);
2065
- const ASSIGNMENT_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.assignment.changed"]);
2066
- const QUEST_EVENT_TYPES = /* @__PURE__ */ new Set([
2067
- "lab.quest.changed",
2068
- "lab.quest.event",
2069
- "lab.quest.sync"
2070
- ]);
2071
- const RUNTIME_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.quest.runtime"]);
2072
- const MEMORY_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.memory.changed"]);
2073
- const PAPER_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.paper.changed"]);
2074
- const BASELINE_EVENT_TYPES = /* @__PURE__ */ new Set(["lab.baseline.archive", "lab.baseline.restore"]);
2075
- const asRecord = (value) => {
2076
- if (!value || typeof value !== "object") return null;
2077
- return value;
2078
- };
2079
- const toNonEmptyString = (value) => {
2080
- if (typeof value !== "string") return null;
2081
- const trimmed = value.trim();
2082
- return trimmed ? trimmed : null;
2083
- };
2084
- const toBoolean = (value) => {
2085
- if (typeof value === "boolean") return value;
2086
- if (typeof value === "number") return value !== 0;
2087
- if (typeof value === "string") {
2088
- const normalized = value.trim().toLowerCase();
2089
- return normalized === "1" || normalized === "true" || normalized === "yes";
2090
- }
2091
- return false;
2092
- };
2093
- const toStringArray = (value) => {
2094
- if (!Array.isArray(value)) return [];
2095
- return value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
2096
- };
2097
- const resolveQuestId = (envelope) => {
2098
- if (!envelope) return null;
2099
- const payloadData = asRecord(envelope.data);
2100
- const directQuest = toNonEmptyString(payloadData?.quest_id) ?? toNonEmptyString(payloadData?.questId) ?? toNonEmptyString(payloadData?.active_quest_id) ?? toNonEmptyString(envelope.quest_id) ?? toNonEmptyString(envelope.questId) ?? toNonEmptyString(envelope.active_quest_id);
2101
- if (directQuest) return directQuest;
2102
- const nestedQuest = asRecord(payloadData?.quest);
2103
- return toNonEmptyString(nestedQuest?.quest_id) ?? toNonEmptyString(nestedQuest?.id);
2104
- };
2105
- const baseProjectTargets = (projectId) => {
2106
- return [
2107
- { queryKey: ["lab-agents", projectId] },
2108
- { queryKey: ["lab-quests", projectId] },
2109
- { queryKey: ["lab-overview", projectId] }
2110
- ];
2111
- };
2112
- const questTargets = (projectId, questId) => {
2113
- if (!questId) {
2114
- return [
2115
- { queryKey: ["lab-quest-detail", projectId] },
2116
- { queryKey: ["lab-quest", projectId] },
2117
- { queryKey: ["lab-quest-summary", projectId] },
2118
- { queryKey: ["lab-quest-graph", projectId] },
2119
- { queryKey: ["lab-quest-events", projectId] },
2120
- { queryKey: ["lab-quest-event-payload", projectId] },
2121
- { queryKey: ["lab-quest-node-trace", projectId] },
2122
- { queryKey: ["lab-quest-decision-events", projectId] },
2123
- { queryKey: ["lab-quest-pi-qa-events", projectId] },
2124
- { queryKey: ["lab-quest-branch-insights", projectId] },
2125
- { queryKey: ["lab-quest-runtime", projectId] },
2126
- { queryKey: ["lab-papers", projectId] },
2127
- { queryKey: ["lab-memory", projectId] }
2128
- ];
2129
- }
2130
- return [
2131
- { queryKey: ["lab-quest-detail", projectId, questId] },
2132
- { queryKey: ["lab-quest", projectId, questId] },
2133
- { queryKey: ["lab-quest-summary", projectId, questId] },
2134
- { queryKey: ["lab-quest-graph", projectId, questId] },
2135
- { queryKey: ["lab-quest-events", projectId, questId] },
2136
- { queryKey: ["lab-quest-event-payload", projectId, questId] },
2137
- { queryKey: ["lab-quest-node-trace", projectId, questId] },
2138
- { queryKey: ["lab-quest-decision-events", projectId, questId] },
2139
- { queryKey: ["lab-quest-pi-qa-events", projectId, questId] },
2140
- { queryKey: ["lab-quest-branch-insights", projectId, questId] },
2141
- { queryKey: ["lab-quest-runtime", projectId, questId] },
2142
- { queryKey: ["lab-papers", projectId, questId] },
2143
- { queryKey: ["lab-memory", projectId, questId] }
2144
- ];
2145
- };
2146
- const memoryTargets = (projectId) => {
2147
- return [
2148
- { queryKey: ["lab-memory", projectId] }
2149
- ];
2150
- };
2151
- const buildTargetsForEvent = (eventType, projectId, questId) => {
2152
- if (AGENT_EVENT_TYPES.has(eventType)) {
2153
- return [
2154
- { queryKey: ["lab-agents", projectId] },
2155
- { queryKey: ["lab-overview", projectId] }
2156
- ];
2157
- }
2158
- if (ASSIGNMENT_EVENT_TYPES.has(eventType)) {
2159
- return [
2160
- { queryKey: ["lab-agents", projectId] },
2161
- { queryKey: ["lab-quests", projectId] },
2162
- { queryKey: ["lab-overview", projectId] },
2163
- ...questTargets(projectId, questId)
2164
- ];
2165
- }
2166
- if (QUEST_EVENT_TYPES.has(eventType)) {
2167
- return [
2168
- { queryKey: ["lab-agents", projectId] },
2169
- { queryKey: ["lab-quests", projectId] },
2170
- { queryKey: ["lab-overview", projectId] },
2171
- ...questTargets(projectId, questId)
2172
- ];
2173
- }
2174
- if (RUNTIME_EVENT_TYPES.has(eventType)) {
2175
- return [
2176
- { queryKey: ["lab-quests", projectId] },
2177
- { queryKey: ["lab-overview", projectId] },
2178
- ...questTargets(projectId, questId)
2179
- ];
2180
- }
2181
- if (MEMORY_EVENT_TYPES.has(eventType)) {
2182
- return [...memoryTargets(projectId), ...questTargets(projectId, questId)];
2183
- }
2184
- if (PAPER_EVENT_TYPES.has(eventType)) {
2185
- return [
2186
- { queryKey: ["lab-papers", projectId] },
2187
- { queryKey: ["lab-overview", projectId] },
2188
- ...questTargets(projectId, questId)
2189
- ];
2190
- }
2191
- if (BASELINE_EVENT_TYPES.has(eventType)) {
2192
- return [
2193
- { queryKey: ["lab-baselines", projectId] },
2194
- { queryKey: ["lab-quests", projectId] },
2195
- { queryKey: ["lab-overview", projectId] },
2196
- ...questTargets(projectId, questId)
2197
- ];
2198
- }
2199
- if (eventType.startsWith("lab.")) {
2200
- return [
2201
- { queryKey: ["lab-quests", projectId] },
2202
- { queryKey: ["lab-overview", projectId] },
2203
- ...questId ? questTargets(projectId, questId) : []
2204
- ];
2205
- }
2206
- return baseProjectTargets(projectId);
2207
- };
2208
- const buildFocusPayloadForEvent = (eventType, projectId, envelope) => {
2209
- if (eventType !== "lab.agent.changed") return null;
2210
- const payload = asRecord(envelope?.data);
2211
- if (!payload) return null;
2212
- const action = toNonEmptyString(payload.action)?.toLowerCase();
2213
- if (action !== "created" && action !== "promoted") return null;
2214
- if (!toBoolean(payload.auto_focus)) return null;
2215
- const agentId = toNonEmptyString(payload.agent_instance_id);
2216
- if (!agentId) return null;
2217
- return {
2218
- projectId,
2219
- focusType: "agent",
2220
- focusId: agentId
2221
- };
2222
- };
2223
- function useLabProjectStream({ projectId, enabled }) {
2224
- const queryClient = useQueryClient();
2225
- const [state, setState] = reactExports.useState({ status: "idle", lastEventAt: null });
2226
- const localQuestSurface = isQuestRuntimeSurface();
2227
- reactExports.useEffect(() => {
2228
- if (!enabled || !projectId || localQuestSurface) {
2229
- setState((current) => current.status === "idle" ? current : { ...current, status: "idle" });
2230
- return;
2231
- }
2232
- let closed = false;
2233
- let reconnectTimer = null;
2234
- let invalidateTimer = null;
2235
- let attempts = 0;
2236
- let lastEventId = null;
2237
- let abortController = null;
2238
- const seenEventIds = /* @__PURE__ */ new Set();
2239
- const seenEventOrder = [];
2240
- const maxSeenEventIds = 512;
2241
- const pendingTargets = /* @__PURE__ */ new Map();
2242
- const streamUrl = `${getApiBaseUrl()}/api/v1/projects/${projectId}/lab/stream`;
2243
- const parseEventBlock = (block) => {
2244
- const lines = block.split(/\n/);
2245
- let eventType = "";
2246
- let eventId = "";
2247
- const dataLines = [];
2248
- for (const line of lines) {
2249
- if (!line) continue;
2250
- if (line.startsWith("event:")) {
2251
- eventType = line.slice(6).trim();
2252
- continue;
2253
- }
2254
- if (line.startsWith("data:")) {
2255
- dataLines.push(line.slice(5).trimStart());
2256
- continue;
2257
- }
2258
- if (line.startsWith("id:")) {
2259
- eventId = line.slice(3).trim();
2260
- }
2261
- }
2262
- if (dataLines.length === 0) return null;
2263
- return {
2264
- event: eventType || "message",
2265
- data: dataLines.join("\n"),
2266
- id: eventId || void 0
2267
- };
2268
- };
2269
- const isDuplicateEventId = (eventType, eventId) => {
2270
- const normalizedId = (eventId || "").trim();
2271
- if (!normalizedId) return false;
2272
- const normalizedType = (eventType || "").trim().toLowerCase();
2273
- const dedupeKey = normalizedType ? `${normalizedType}|${normalizedId}` : normalizedId;
2274
- if (seenEventIds.has(dedupeKey)) return true;
2275
- seenEventIds.add(dedupeKey);
2276
- seenEventOrder.push(dedupeKey);
2277
- if (seenEventOrder.length > maxSeenEventIds) {
2278
- const evicted = seenEventOrder.shift();
2279
- if (evicted) seenEventIds.delete(evicted);
2280
- }
2281
- return false;
2282
- };
2283
- const flushInvalidations = () => {
2284
- if (pendingTargets.size === 0) return;
2285
- const entries = Array.from(pendingTargets.values());
2286
- pendingTargets.clear();
2287
- entries.forEach((target) => {
2288
- queryClient.invalidateQueries({
2289
- queryKey: target.queryKey,
2290
- exact: target.exact
2291
- });
2292
- });
2293
- };
2294
- const enqueueInvalidations = (targets) => {
2295
- targets.forEach((target) => {
2296
- const key = `${target.exact ? 1 : 0}:${JSON.stringify(target.queryKey)}`;
2297
- pendingTargets.set(key, target);
2298
- });
2299
- if (invalidateTimer) return;
2300
- invalidateTimer = window.setTimeout(() => {
2301
- invalidateTimer = null;
2302
- flushInvalidations();
2303
- }, INVALIDATION_BATCH_MS);
2304
- };
2305
- const handleStreamEvent = (eventType, payloadText) => {
2306
- if (eventType === "lab.ping") return;
2307
- setState((current) => ({ ...current, lastEventAt: Date.now() }));
2308
- let envelope = null;
2309
- try {
2310
- const parsed = JSON.parse(payloadText || "{}");
2311
- envelope = asRecord(parsed);
2312
- } catch {
2313
- envelope = null;
2314
- }
2315
- const questId = resolveQuestId(envelope);
2316
- const payload = asRecord(envelope?.data);
2317
- const questIds = toStringArray(payload?.quest_ids);
2318
- const targets = buildTargetsForEvent(eventType, projectId, questId);
2319
- enqueueInvalidations(targets);
2320
- questIds.forEach((id) => {
2321
- enqueueInvalidations(questTargets(projectId, id));
2322
- });
2323
- const focusPayload = buildFocusPayloadForEvent(eventType, projectId, envelope);
2324
- if (focusPayload) {
2325
- dispatchLabFocus(focusPayload);
2326
- }
2327
- };
2328
- const scheduleReconnect = () => {
2329
- if (closed) return;
2330
- if (reconnectTimer) {
2331
- window.clearTimeout(reconnectTimer);
2332
- }
2333
- const capped = Math.min(attempts, 4);
2334
- const baseDelay = Math.min(1e4, 1e3 * 2 ** capped);
2335
- const jitter = Math.min(250, attempts * 50);
2336
- setState(
2337
- (current) => current.status === "connecting" || current.status === "open" ? { ...current, status: "reconnecting" } : current
2338
- );
2339
- reconnectTimer = window.setTimeout(connect, baseDelay + jitter);
2340
- };
2341
- const connect = () => {
2342
- if (closed) return;
2343
- if (abortController) {
2344
- abortController.abort();
2345
- }
2346
- abortController = new AbortController();
2347
- const controller = abortController;
2348
- setState((current) => ({
2349
- ...current,
2350
- status: attempts > 0 ? "reconnecting" : "connecting"
2351
- }));
2352
- void (async () => {
2353
- try {
2354
- const token = typeof window !== "undefined" ? window.localStorage.getItem("ds_access_token") : null;
2355
- if (!token) return;
2356
- const headers = {
2357
- Accept: "text/event-stream",
2358
- Authorization: `Bearer ${token}`
2359
- };
2360
- if (lastEventId) {
2361
- headers["Last-Event-ID"] = lastEventId;
2362
- }
2363
- const response = await fetch(streamUrl, {
2364
- method: "GET",
2365
- headers,
2366
- signal: controller.signal
2367
- });
2368
- if (closed || controller.signal.aborted) return;
2369
- if (response.status === 401) {
2370
- if (typeof window !== "undefined") {
2371
- window.localStorage.removeItem("ds_access_token");
2372
- redirectToLanding("session_expired");
2373
- }
2374
- return;
2375
- }
2376
- if (!response.ok) {
2377
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2378
- }
2379
- attempts = 0;
2380
- setState((current) => ({ ...current, status: "open" }));
2381
- enqueueInvalidations(baseProjectTargets(projectId));
2382
- const reader = response.body?.getReader();
2383
- if (!reader) {
2384
- throw new Error("No response body");
2385
- }
2386
- const decoder = new TextDecoder();
2387
- let buffer = "";
2388
- while (true) {
2389
- if (closed || controller.signal.aborted) break;
2390
- const { done, value } = await reader.read();
2391
- if (done) break;
2392
- if (closed || controller.signal.aborted) break;
2393
- buffer += decoder.decode(value, { stream: true });
2394
- if (buffer.includes("\r")) {
2395
- buffer = buffer.replace(/\r\n/g, "\n");
2396
- }
2397
- let boundaryIndex = buffer.indexOf("\n\n");
2398
- while (boundaryIndex !== -1) {
2399
- const raw = buffer.slice(0, boundaryIndex);
2400
- buffer = buffer.slice(boundaryIndex + 2);
2401
- const normalized = raw.replace(/\r\n/g, "\n").trim();
2402
- const parsed = parseEventBlock(normalized);
2403
- if (parsed?.id) {
2404
- lastEventId = parsed.id;
2405
- if (isDuplicateEventId(parsed.event, parsed.id)) {
2406
- boundaryIndex = buffer.indexOf("\n\n");
2407
- continue;
2408
- }
2409
- }
2410
- if (parsed && !closed && !controller.signal.aborted) {
2411
- handleStreamEvent(parsed.event, parsed.data);
2412
- }
2413
- boundaryIndex = buffer.indexOf("\n\n");
2414
- }
2415
- }
2416
- const trailing = buffer.replace(/\r\n/g, "\n").trim();
2417
- if (trailing && !closed && !controller.signal.aborted) {
2418
- const parsed = parseEventBlock(trailing);
2419
- if (parsed?.id) {
2420
- lastEventId = parsed.id;
2421
- if (isDuplicateEventId(parsed.event, parsed.id)) {
2422
- return;
2423
- }
2424
- }
2425
- if (parsed) {
2426
- handleStreamEvent(parsed.event, parsed.data);
2427
- }
2428
- }
2429
- if (!closed && !controller.signal.aborted) {
2430
- attempts += 1;
2431
- scheduleReconnect();
2432
- }
2433
- } catch {
2434
- if (closed || controller.signal.aborted) return;
2435
- setState((current) => ({ ...current, status: "error" }));
2436
- attempts += 1;
2437
- scheduleReconnect();
2438
- }
2439
- })();
2440
- };
2441
- connect();
2442
- return () => {
2443
- closed = true;
2444
- flushInvalidations();
2445
- if (reconnectTimer) {
2446
- window.clearTimeout(reconnectTimer);
2447
- }
2448
- if (invalidateTimer) {
2449
- window.clearTimeout(invalidateTimer);
2450
- }
2451
- if (abortController) {
2452
- abortController.abort();
2453
- }
2454
- };
2455
- }, [enabled, localQuestSurface, projectId, queryClient]);
2456
- return state;
2457
- }
2458
-
2459
- function resolveLabListPollingInterval({
2460
- liveEnabled,
2461
- streamStatus,
2462
- fastMs,
2463
- slowMs
2464
- }) {
2465
- if (!liveEnabled) return false;
2466
- return streamStatus === "open" ? slowMs : fastMs;
2467
- }
2468
-
2469
- function LabSurface({
2470
- projectId,
2471
- readOnly,
2472
- lockedQuestId = null,
2473
- immersiveLockedQuest = false,
2474
- onRefresh,
2475
- onOpenStageSelection
2476
- }) {
2477
- const queryClient = useQueryClient();
2478
- const setActiveQuest = useLabCopilotStore((state) => state.setActiveQuest);
2479
- const activeQuestId = useLabCopilotStore((state) => state.activeQuestId);
2480
- const clearSelections = useLabCopilotStore((state) => state.clearSelections);
2481
- const cliServers = useCliStore((state) => state.servers);
2482
- const loadCliServers = useCliStore((state) => state.loadServers);
2483
- const cliProjectId = useCliStore((state) => state.projectId);
2484
- const activeTab = useActiveTab();
2485
- const isLabTabActive = reactExports.useMemo(() => {
2486
- const customData = activeTab?.context?.customData;
2487
- const tabProjectId = typeof customData?.projectId === "string" ? customData.projectId : null;
2488
- const matchesProject = !tabProjectId || tabProjectId === projectId;
2489
- return activeTab?.pluginId === BUILTIN_PLUGINS.LAB && matchesProject;
2490
- }, [activeTab, projectId]);
2491
- reactExports.useEffect(() => {
2492
- if (!projectId) return;
2493
- if (cliProjectId === projectId) return;
2494
- void loadCliServers(projectId);
2495
- }, [cliProjectId, loadCliServers, projectId]);
2496
- const onlineCliServers = reactExports.useMemo(
2497
- () => cliServers.filter((server) => server.status !== "offline" && server.status !== "error"),
2498
- [cliServers]
2499
- );
2500
- const cliStatus = cliServers.length === 0 ? "unbound" : onlineCliServers.length > 0 ? "online" : "offline";
2501
- const labReadOnly = true;
2502
- const [isPageActive, setIsPageActive] = reactExports.useState(true);
2503
- reactExports.useEffect(() => {
2504
- const handleVisibility = () => {
2505
- setIsPageActive(typeof document !== "undefined" && document.visibilityState === "visible");
2506
- };
2507
- handleVisibility();
2508
- document.addEventListener("visibilitychange", handleVisibility);
2509
- return () => document.removeEventListener("visibilitychange", handleVisibility);
2510
- }, []);
2511
- const labStaleTime = 3e4;
2512
- const liveRefetchEnabled = Boolean(projectId && isLabTabActive && isPageActive);
2513
- const labStreamEnabled = Boolean(projectId && isPageActive);
2514
- const LIVE_AGENTS_INTERVAL_MS = 5e3;
2515
- const LIVE_QUESTS_INTERVAL_MS = 1e4;
2516
- const LIVE_OVERVIEW_INTERVAL_MS = 1e4;
2517
- const labStream = useLabProjectStream({ projectId, enabled: labStreamEnabled });
2518
- const labStreamStatus = labStream?.status ?? "idle";
2519
- const agentsRefetchInterval = resolveLabListPollingInterval({
2520
- liveEnabled: liveRefetchEnabled,
2521
- streamStatus: labStreamStatus,
2522
- fastMs: LIVE_AGENTS_INTERVAL_MS,
2523
- // Agent statuses are derived from chat session state; until we publish per-agent status events,
2524
- // keep polling relatively frequent even when the project stream is healthy.
2525
- slowMs: LIVE_AGENTS_INTERVAL_MS
2526
- });
2527
- const questsRefetchInterval = resolveLabListPollingInterval({
2528
- liveEnabled: liveRefetchEnabled,
2529
- streamStatus: labStreamStatus,
2530
- fastMs: LIVE_QUESTS_INTERVAL_MS,
2531
- slowMs: 3e4
2532
- });
2533
- const overviewRefetchInterval = resolveLabListPollingInterval({
2534
- liveEnabled: liveRefetchEnabled,
2535
- streamStatus: labStreamStatus,
2536
- fastMs: LIVE_OVERVIEW_INTERVAL_MS,
2537
- slowMs: 3e4
2538
- });
2539
- const wasLiveRefetchEnabledRef = reactExports.useRef(liveRefetchEnabled);
2540
- reactExports.useEffect(() => {
2541
- const wasEnabled = wasLiveRefetchEnabledRef.current;
2542
- wasLiveRefetchEnabledRef.current = liveRefetchEnabled;
2543
- if (!projectId || !liveRefetchEnabled || wasEnabled) return;
2544
- queryClient.invalidateQueries({ queryKey: ["lab-agents", projectId] });
2545
- queryClient.invalidateQueries({ queryKey: ["lab-quests", projectId] });
2546
- queryClient.invalidateQueries({ queryKey: ["lab-overview", projectId] });
2547
- }, [liveRefetchEnabled, projectId, queryClient]);
2548
- const templatesQuery = useQuery({
2549
- queryKey: ["lab-templates", projectId],
2550
- queryFn: () => listLabTemplates(projectId),
2551
- enabled: Boolean(projectId),
2552
- staleTime: labStaleTime
2553
- });
2554
- const agentsQuery = useQuery({
2555
- queryKey: ["lab-agents", projectId],
2556
- queryFn: () => listLabAgents(projectId, { silent: true }),
2557
- enabled: Boolean(projectId),
2558
- staleTime: labStaleTime,
2559
- refetchInterval: agentsRefetchInterval
2560
- });
2561
- const questsQuery = useQuery({
2562
- queryKey: ["lab-quests", projectId],
2563
- queryFn: () => listLabQuests(projectId, { silent: true }),
2564
- enabled: Boolean(projectId),
2565
- staleTime: labStaleTime,
2566
- refetchInterval: questsRefetchInterval
2567
- });
2568
- const overviewQuery = useQuery({
2569
- queryKey: ["lab-overview", projectId],
2570
- queryFn: () => getLabOverview(projectId, { silent: true }),
2571
- staleTime: labStaleTime,
2572
- refetchInterval: overviewRefetchInterval
2573
- });
2574
- const projectQuery = useQuery({
2575
- queryKey: ["project", projectId],
2576
- queryFn: () => getProject(projectId),
2577
- enabled: Boolean(projectId),
2578
- staleTime: labStaleTime
2579
- });
2580
- const templates = templatesQuery.data?.items ?? [];
2581
- const agents = agentsQuery.data?.items ?? [];
2582
- const quests = questsQuery.data?.items ?? [];
2583
- const overview = overviewQuery.data ?? {};
2584
- const latestQuestId = reactExports.useMemo(() => {
2585
- if (quests.length === 0) return null;
2586
- const latest = quests.reduce((current, candidate) => {
2587
- const currentTime = current?.created_at ? new Date(current.created_at).getTime() : 0;
2588
- const candidateTime = candidate?.created_at ? new Date(candidate.created_at).getTime() : 0;
2589
- return candidateTime > currentTime ? candidate : current;
2590
- }, quests[0]);
2591
- return latest?.quest_id ?? null;
2592
- }, [quests]);
2593
- const projectName = projectQuery.data?.name ?? null;
2594
- reactExports.useEffect(() => {
2595
- if (lockedQuestId) {
2596
- setActiveQuest(lockedQuestId);
2597
- return;
2598
- }
2599
- if (activeQuestId || !latestQuestId) return;
2600
- setActiveQuest(latestQuestId);
2601
- }, [activeQuestId, latestQuestId, lockedQuestId, setActiveQuest]);
2602
- reactExports.useEffect(() => {
2603
- clearSelections();
2604
- }, [clearSelections, projectId]);
2605
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
2606
- "div",
2607
- {
2608
- className: "lab-root flex h-full min-h-0 flex-col",
2609
- "data-activity": isPageActive ? "active" : "inactive",
2610
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cn("lab-panel h-full min-h-0 flex-1 overflow-hidden"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2611
- LabCanvasStudio,
2612
- {
2613
- projectId,
2614
- readOnly: labReadOnly,
2615
- onRefresh,
2616
- onOpenStageSelection,
2617
- cliStatus,
2618
- labStream,
2619
- projectName,
2620
- templates,
2621
- agents,
2622
- quests,
2623
- overview,
2624
- isLoading: {
2625
- agents: agentsQuery.isLoading,
2626
- quests: questsQuery.isLoading,
2627
- overview: overviewQuery.isLoading
2628
- },
2629
- lockedQuestId,
2630
- immersiveLockedQuest
2631
- }
2632
- ) })
2633
- }
2634
- );
2635
- }
2636
-
2637
- const LAB_FOCUS_EVENT = "ds:lab:focus";
2638
- function LabPlugin({ context, setTitle }) {
2639
- const { t } = useI18n("lab");
2640
- const maxEntitlement = useMaxEntitlement("lab.use");
2641
- const projectId = typeof context.customData?.projectId === "string" ? context.customData.projectId : null;
2642
- const readOnly = Boolean(context.customData?.readOnly);
2643
- const focusType = context.customData?.focusType === "agent" || context.customData?.focusType === "quest" || context.customData?.focusType === "quest-branch" || context.customData?.focusType === "quest-event" || context.customData?.focusType === "overview" ? context.customData.focusType : null;
2644
- const focusId = typeof context.customData?.focusId === "string" ? context.customData.focusId : null;
2645
- const focusBranch = typeof context.customData?.branch === "string" ? context.customData.branch : null;
2646
- const focusEventId = typeof context.customData?.eventId === "string" ? context.customData.eventId : null;
2647
- reactExports.useEffect(() => {
2648
- setTitle(t("plugin_home_title", void 0, "Home"));
2649
- }, [setTitle, t]);
2650
- reactExports.useEffect(() => {
2651
- if (!projectId || !focusType) return;
2652
- if (!focusId && focusType !== "overview") return;
2653
- window.dispatchEvent(
2654
- new CustomEvent(LAB_FOCUS_EVENT, {
2655
- detail: {
2656
- projectId,
2657
- focusType,
2658
- focusId,
2659
- branch: focusBranch,
2660
- eventId: focusEventId
2661
- }
2662
- })
2663
- );
2664
- }, [focusBranch, focusEventId, focusId, focusType, projectId]);
2665
- if (!projectId) {
2666
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full items-center justify-center rounded-2xl border border-[var(--soft-border)] bg-[var(--soft-bg-surface)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-[var(--soft-text-secondary)]", children: t("plugin_project_not_found", void 0, "Project not found.") }) });
2667
- }
2668
- if (!maxEntitlement.isEntitlementLoading && !maxEntitlement.isMaxEntitled) {
2669
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full items-center justify-center rounded-2xl border border-[var(--soft-border)] bg-[var(--soft-bg-surface)]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 text-center", children: [
2670
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-[var(--soft-text-primary)]", children: t("plugin_plan_access_required", void 0, "Plan access required") }),
2671
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-xs text-[var(--soft-text-secondary)]", children: t("plugin_max_only_desc", void 0, "Lab is currently available for Max users only.") })
2672
- ] }) });
2673
- }
2674
- return /* @__PURE__ */ jsxRuntimeExports.jsx(LabSurface, { projectId, readOnly });
2675
- }
2676
-
2677
- export { LabPlugin as default };