@opengeni/react 1.4.4 → 1.4.5-canary.37485542607001

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 (249) hide show
  1. package/README.md +17 -6
  2. package/dist/artifacts.d.ts +1 -0
  3. package/dist/artifacts.js +22 -15
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{browser-viewer-5EEAS3IU.js → browser-viewer-CV23VIHB.js} +8 -6
  6. package/dist/{chunk-XS67YG5R.js → chunk-4O7OE7ON.js} +45 -4
  7. package/dist/chunk-4O7OE7ON.js.map +1 -0
  8. package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
  9. package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
  10. package/dist/{chunk-QMMCJGFI.js → chunk-6BAMPGBV.js} +6 -3
  11. package/dist/chunk-6BAMPGBV.js.map +1 -0
  12. package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
  13. package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
  14. package/dist/chunk-ATIRUHBZ.js.map +1 -0
  15. package/dist/chunk-BOFEQC6K.js +162 -0
  16. package/dist/chunk-BOFEQC6K.js.map +1 -0
  17. package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
  18. package/dist/chunk-CESBPQP5.js.map +1 -0
  19. package/dist/{chunk-YVFRBXST.js → chunk-GMNGUPMX.js} +2 -2
  20. package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
  21. package/dist/chunk-I5EGRXXG.js.map +1 -0
  22. package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
  23. package/dist/chunk-IZV5FTJC.js +55 -0
  24. package/dist/chunk-IZV5FTJC.js.map +1 -0
  25. package/dist/chunk-JDJJMBB3.js +1883 -0
  26. package/dist/chunk-JDJJMBB3.js.map +1 -0
  27. package/dist/chunk-LHF6BVM4.js +83 -0
  28. package/dist/chunk-LHF6BVM4.js.map +1 -0
  29. package/dist/{chunk-C5BF4L26.js → chunk-LSMWZSCJ.js} +7 -7
  30. package/dist/chunk-LTKGTXUQ.js +174 -0
  31. package/dist/chunk-LTKGTXUQ.js.map +1 -0
  32. package/dist/chunk-N7S72EXK.js +25 -0
  33. package/dist/chunk-N7S72EXK.js.map +1 -0
  34. package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
  35. package/dist/chunk-PEHTX3V7.js.map +1 -0
  36. package/dist/chunk-R5R5RQQD.js +51 -0
  37. package/dist/chunk-R5R5RQQD.js.map +1 -0
  38. package/dist/{chunk-IYQCWGU5.js → chunk-RGTZ2VDR.js} +35 -62
  39. package/dist/chunk-RGTZ2VDR.js.map +1 -0
  40. package/dist/chunk-RI42H5IU.js +326 -0
  41. package/dist/chunk-RI42H5IU.js.map +1 -0
  42. package/dist/{chunk-EAOSEIXB.js → chunk-RRRS5D5T.js} +727 -1983
  43. package/dist/chunk-RRRS5D5T.js.map +1 -0
  44. package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
  45. package/dist/chunk-SHLZMAJN.js.map +1 -0
  46. package/dist/{chunk-IUHHPMAX.js → chunk-SZOCY5VG.js} +18 -87
  47. package/dist/chunk-SZOCY5VG.js.map +1 -0
  48. package/dist/{chunk-L2J3PU7A.js → chunk-VYIPNDUZ.js} +29 -100
  49. package/dist/chunk-VYIPNDUZ.js.map +1 -0
  50. package/dist/{chunk-G6YRCIYJ.js → chunk-YK6QMGVL.js} +55 -60
  51. package/dist/chunk-YK6QMGVL.js.map +1 -0
  52. package/dist/chunk-ZIB4H5LN.js +101 -0
  53. package/dist/chunk-ZIB4H5LN.js.map +1 -0
  54. package/dist/client.d.ts +9 -3
  55. package/dist/clipboard.js +1 -1
  56. package/dist/components/browser-viewer.d.ts +6 -0
  57. package/dist/components/composer.d.ts +3 -69
  58. package/dist/components/human-input-form.d.ts +3 -32
  59. package/dist/components/markdown.d.ts +8 -0
  60. package/dist/components/model-policy-picker.d.ts +4 -37
  61. package/dist/components/opper-mark.d.ts +7 -0
  62. package/dist/components/provider-recovery-notice.d.ts +15 -0
  63. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  64. package/dist/components/sandbox-workspace.d.ts +13 -0
  65. package/dist/components/session-chrome.d.ts +5 -3
  66. package/dist/components/session-conversation.d.ts +15 -3
  67. package/dist/composer-messages.d.ts +74 -0
  68. package/dist/composer-messages.js +9 -0
  69. package/dist/composer.js +15 -10
  70. package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
  71. package/dist/connect-setup.d.ts +2 -0
  72. package/dist/connect.d.ts +1 -1
  73. package/dist/connect.js +21 -6
  74. package/dist/connect.js.map +1 -1
  75. package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-K7SOZVVZ.js} +6 -4
  76. package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-K7SOZVVZ.js.map} +1 -1
  77. package/dist/hooks/shared-machines-list.d.ts +4 -0
  78. package/dist/hooks/use-client-config-flags.d.ts +6 -0
  79. package/dist/hooks/use-goal.d.ts +2 -1
  80. package/dist/hooks/use-new-session-draft.d.ts +74 -0
  81. package/dist/human-input-model.d.ts +40 -0
  82. package/dist/index.d.ts +4 -1
  83. package/dist/index.js +116 -73
  84. package/dist/index.js.map +1 -1
  85. package/dist/interaction.js +5 -5
  86. package/dist/{latest-question-2POFPYNB.js → latest-question-27HGRPVF.js} +3 -3
  87. package/dist/lib/clipboard.d.ts +7 -0
  88. package/dist/lib/provider-recovery.d.ts +49 -0
  89. package/dist/lib/transient-retry.d.ts +25 -0
  90. package/dist/machines.js +2 -2
  91. package/dist/model-mark-paths.d.ts +34 -0
  92. package/dist/model-mark-paths.js +16 -0
  93. package/dist/model-picker-model.d.ts +111 -0
  94. package/dist/model-picker-model.js +25 -0
  95. package/dist/model-picker-model.js.map +1 -0
  96. package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-I5KPC3D6.js} +6 -4
  97. package/dist/model-policy-picker-menu-I5KPC3D6.js.map +1 -0
  98. package/dist/model-policy.d.ts +10 -0
  99. package/dist/model-policy.js +5 -1
  100. package/dist/new-session-draft.d.ts +7 -0
  101. package/dist/new-session-draft.js +639 -0
  102. package/dist/new-session-draft.js.map +1 -0
  103. package/dist/organization-model.d.ts +32 -0
  104. package/dist/organization-model.js +65 -0
  105. package/dist/organization-model.js.map +1 -0
  106. package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
  107. package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
  108. package/dist/queue-presentation-model.d.ts +72 -0
  109. package/dist/queue-presentation-model.js +38 -0
  110. package/dist/queue-presentation-model.js.map +1 -0
  111. package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
  112. package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
  113. package/dist/realtime.js +5 -3
  114. package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-2DGDO6NI.js} +5 -5
  115. package/dist/sandbox-files-2DGDO6NI.js.map +1 -0
  116. package/dist/sandbox-label-model.d.ts +23 -0
  117. package/dist/sandbox-label-model.js +28 -0
  118. package/dist/sandbox-label-model.js.map +1 -0
  119. package/dist/session-agents-model.d.ts +10 -0
  120. package/dist/session-agents-model.js +25 -0
  121. package/dist/session-agents-model.js.map +1 -0
  122. package/dist/session-attention-model.d.ts +8 -0
  123. package/dist/session-attention-model.js +46 -0
  124. package/dist/session-attention-model.js.map +1 -0
  125. package/dist/session-feedback-model.d.ts +20 -0
  126. package/dist/session-feedback-model.js +56 -0
  127. package/dist/session-feedback-model.js.map +1 -0
  128. package/dist/session-list-model.d.ts +119 -0
  129. package/dist/session-list-model.js +244 -0
  130. package/dist/session-list-model.js.map +1 -0
  131. package/dist/session-status-model.d.ts +19 -0
  132. package/dist/session-ui.js +26 -17
  133. package/dist/session-ui.js.map +1 -1
  134. package/dist/session.js +8 -6
  135. package/dist/testing/session-scenarios.d.ts +10 -0
  136. package/dist/testing/session-scenarios.js +303 -0
  137. package/dist/testing/session-scenarios.js.map +1 -0
  138. package/dist/timeline/genie-copy.d.ts +3 -0
  139. package/dist/timeline/genie-loading.d.ts +11 -0
  140. package/dist/timeline/index.d.ts +1 -1
  141. package/dist/timeline/notice-presentation.d.ts +7 -0
  142. package/dist/timeline/patch-apply-command.d.ts +18 -0
  143. package/dist/timeline/platform-activity-presentation.d.ts +7 -0
  144. package/dist/timeline/presented-image.d.ts +6 -1
  145. package/dist/timeline/question-nav-model.d.ts +23 -0
  146. package/dist/timeline/registry.d.ts +10 -1
  147. package/dist/timeline/retained-loaders.d.ts +1 -1
  148. package/dist/timeline/tool-presentation.d.ts +172 -0
  149. package/dist/timeline/turn-summary-model.d.ts +90 -0
  150. package/dist/timeline/turn-summary.d.ts +4 -74
  151. package/dist/timeline/work-presentation.d.ts +42 -0
  152. package/dist/timeline-model.d.ts +24 -0
  153. package/dist/timeline-model.js +152 -0
  154. package/dist/timeline-model.js.map +1 -0
  155. package/package.json +55 -3
  156. package/src/artifacts.ts +1 -0
  157. package/src/client.ts +9 -3
  158. package/src/components/browser-viewer.tsx +23 -2
  159. package/src/components/claude-mark.tsx +2 -1
  160. package/src/components/composer.tsx +8 -146
  161. package/src/components/grok-mark.tsx +2 -1
  162. package/src/components/human-input-form.tsx +13 -124
  163. package/src/components/markdown.tsx +19 -1
  164. package/src/components/message-timeline.tsx +45 -99
  165. package/src/components/model-mark.tsx +2 -1
  166. package/src/components/model-policy-picker-menu.tsx +30 -85
  167. package/src/components/model-policy-picker.tsx +20 -150
  168. package/src/components/opper-mark.tsx +18 -0
  169. package/src/components/provider-recovery-notice.tsx +41 -0
  170. package/src/components/queue-surface-implementation.tsx +20 -349
  171. package/src/components/remark-soft-line-breaks.ts +166 -0
  172. package/src/components/sandbox-workspace.tsx +17 -1
  173. package/src/components/session-chrome.tsx +114 -99
  174. package/src/components/session-conversation.tsx +55 -6
  175. package/src/components/session-status.tsx +17 -16
  176. package/src/composer-messages.ts +150 -0
  177. package/src/connect-setup.tsx +45 -11
  178. package/src/connect.ts +1 -1
  179. package/src/hooks/shared-machines-list.ts +34 -1
  180. package/src/hooks/use-client-config-flags.ts +8 -0
  181. package/src/hooks/use-goal.ts +66 -10
  182. package/src/hooks/use-new-session-draft.ts +813 -0
  183. package/src/human-input-model.ts +147 -0
  184. package/src/index.ts +16 -0
  185. package/src/lib/clipboard.ts +29 -0
  186. package/src/lib/format.ts +7 -0
  187. package/src/lib/provider-recovery.ts +202 -0
  188. package/src/lib/transient-retry.ts +99 -0
  189. package/src/model-mark-paths.ts +53 -0
  190. package/src/model-picker-model.ts +330 -0
  191. package/src/model-policy.ts +75 -5
  192. package/src/new-session-draft.ts +22 -0
  193. package/src/organization-model.ts +118 -0
  194. package/src/queue-presentation-model.ts +464 -0
  195. package/src/sandbox-label-model.ts +41 -0
  196. package/src/session-agents-model.ts +33 -0
  197. package/src/session-attention-model.ts +59 -0
  198. package/src/session-feedback-model.ts +95 -0
  199. package/src/session-list-model.ts +377 -0
  200. package/src/session-status-model.ts +62 -0
  201. package/src/testing/session-scenarios.ts +326 -0
  202. package/src/timeline/activity-rail.tsx +54 -24
  203. package/src/timeline/genie-copy.ts +19 -0
  204. package/src/timeline/genie-loading.tsx +15 -19
  205. package/src/timeline/index.ts +1 -0
  206. package/src/timeline/notice-presentation.ts +22 -0
  207. package/src/timeline/patch-apply-command.tsx +135 -0
  208. package/src/timeline/platform-activity-presentation.ts +122 -0
  209. package/src/timeline/platform-activity-row.tsx +6 -96
  210. package/src/timeline/presented-image.ts +16 -2
  211. package/src/timeline/projection.ts +12 -1
  212. package/src/timeline/question-nav-model.ts +28 -0
  213. package/src/timeline/registry.ts +11 -1
  214. package/src/timeline/retained-loaders.ts +9 -4
  215. package/src/timeline/rolling-activity.tsx +7 -19
  216. package/src/timeline/tool-presentation.ts +1316 -0
  217. package/src/timeline/tool-renderers.tsx +254 -1024
  218. package/src/timeline/turn-summary-model.ts +338 -0
  219. package/src/timeline/turn-summary.tsx +21 -326
  220. package/src/timeline/work-presentation.ts +139 -0
  221. package/src/timeline-model.ts +108 -0
  222. package/styles/compiled.css +17 -4
  223. package/styles/connect.css +4 -0
  224. package/styles/responsive.css +11 -2
  225. package/dist/chunk-2OQA3CPB.js.map +0 -1
  226. package/dist/chunk-CVYCLFMH.js.map +0 -1
  227. package/dist/chunk-EAOSEIXB.js.map +0 -1
  228. package/dist/chunk-G6YRCIYJ.js.map +0 -1
  229. package/dist/chunk-HXYOSSQO.js +0 -117
  230. package/dist/chunk-HXYOSSQO.js.map +0 -1
  231. package/dist/chunk-IUHHPMAX.js.map +0 -1
  232. package/dist/chunk-IYQCWGU5.js.map +0 -1
  233. package/dist/chunk-JH7V627O.js.map +0 -1
  234. package/dist/chunk-L2J3PU7A.js.map +0 -1
  235. package/dist/chunk-LVS3MH5T.js.map +0 -1
  236. package/dist/chunk-PHBFJCEU.js.map +0 -1
  237. package/dist/chunk-QMMCJGFI.js.map +0 -1
  238. package/dist/chunk-XS67YG5R.js.map +0 -1
  239. package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
  240. package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
  241. /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
  242. /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
  243. /package/dist/{chunk-YVFRBXST.js.map → chunk-GMNGUPMX.js.map} +0 -0
  244. /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
  245. /package/dist/{chunk-C5BF4L26.js.map → chunk-LSMWZSCJ.js.map} +0 -0
  246. /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
  247. /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
  248. /package/dist/{latest-question-2POFPYNB.js.map → latest-question-27HGRPVF.js.map} +0 -0
  249. /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
@@ -0,0 +1,326 @@
1
+ // Deterministic session scenarios for visual harnesses (web dev gallery and native lab).
2
+ // They use the same durable event shapes a live session produces, so every renderer
3
+ // replays identical input through the official projection with no model calls.
4
+ import type { SessionEvent } from "@opengeni/sdk";
5
+
6
+ export type LabScenarioId = "answer" | "working" | "needs-you" | "history" | "failure" | "long";
7
+
8
+ export interface LabScenario {
9
+ id: LabScenarioId;
10
+ title: string;
11
+ events: SessionEvent[];
12
+ /** Whether the session is currently running a turn. */
13
+ running: boolean;
14
+ }
15
+
16
+ function recorder(sessionId: string) {
17
+ const events: SessionEvent[] = [];
18
+ let clock = Date.parse("2026-10-03T09:00:00Z");
19
+ const push = (type: string, payload: unknown, turnId: string | null, advanceMs = 2_000) => {
20
+ clock += advanceMs;
21
+ events.push({
22
+ id: `${sessionId}-evt-${events.length + 1}`,
23
+ workspaceId: "lab-workspace",
24
+ sessionId,
25
+ sequence: events.length + 1,
26
+ type,
27
+ payload,
28
+ turnId,
29
+ occurredAt: new Date(clock).toISOString(),
30
+ } as SessionEvent);
31
+ };
32
+ const tool = (
33
+ turnId: string,
34
+ id: string,
35
+ name: string,
36
+ args: unknown,
37
+ output?: unknown,
38
+ durationMs = 3_000,
39
+ ) => {
40
+ push("agent.toolCall.created", { id, name, arguments: args }, turnId, 800);
41
+ if (output !== undefined) push("agent.toolCall.output", { id, output }, turnId, durationMs);
42
+ };
43
+ return { events, push, tool };
44
+ }
45
+
46
+ function answer(): LabScenario {
47
+ const r = recorder("lab-answer");
48
+ const t = "turn-answer";
49
+ r.push(
50
+ "user.message",
51
+ { text: "What's the difference between a retry and a resume for a failed run?" },
52
+ t,
53
+ );
54
+ r.push("turn.started", {}, t, 400);
55
+ r.push(
56
+ "agent.reasoning.delta",
57
+ { text: "The user wants a crisp conceptual distinction, with an example." },
58
+ t,
59
+ 1_500,
60
+ );
61
+ const text = [
62
+ "**Retry** starts the failed step again from its last durable checkpoint. **Resume** continues the whole run from where it paused, keeping everything already done.",
63
+ "",
64
+ "### When to use which",
65
+ "",
66
+ "- **Retry** when a single step failed for a transient reason (timeout, rate limit).",
67
+ "- **Resume** when the run was *waiting* — for an approval, an answer, or capacity.",
68
+ "",
69
+ "```bash",
70
+ "opengeni sessions retry 7f3c --step build",
71
+ "```",
72
+ "",
73
+ "Neither replays side effects that already succeeded.",
74
+ ].join("\n");
75
+ r.push("agent.message.delta", { text }, t, 2_500);
76
+ r.push("agent.message.completed", { text }, t, 300);
77
+ r.push("turn.completed", { output: text }, t, 200);
78
+ return { id: "answer", title: "Quick answer", events: r.events, running: false };
79
+ }
80
+
81
+ function working(): LabScenario {
82
+ const r = recorder("lab-working");
83
+ const t = "turn-working";
84
+ r.push(
85
+ "user.message",
86
+ {
87
+ text: "The checkout tests started failing after the currency change. Can you find out why and fix it?",
88
+ },
89
+ t,
90
+ );
91
+ r.push("turn.started", {}, t, 400);
92
+ r.push(
93
+ "agent.reasoning.delta",
94
+ { text: "Start by reproducing the failure, then narrow to the currency formatting path." },
95
+ t,
96
+ 1_800,
97
+ );
98
+ r.tool(
99
+ t,
100
+ "c1",
101
+ "exec_command",
102
+ { cmd: "bun test checkout" },
103
+ "3 failed, 41 passed\n✗ formats totals in NOK\n✗ rounds VAT per line\n✗ shows discount",
104
+ 6_000,
105
+ );
106
+ r.tool(
107
+ t,
108
+ "c2",
109
+ "search_files",
110
+ { query: "formatCurrency(" },
111
+ "src/money/format.ts:12\nsrc/checkout/summary.tsx:48\nsrc/checkout/line.tsx:31",
112
+ );
113
+ r.push(
114
+ "agent.message.delta",
115
+ {
116
+ text: "Found it: the currency change switched to minor units, but the summary still divides by 100.",
117
+ phase: "commentary",
118
+ },
119
+ t,
120
+ 1_200,
121
+ );
122
+ r.push(
123
+ "agent.message.completed",
124
+ {
125
+ text: "Found it: the currency change switched to minor units, but the summary still divides by 100.",
126
+ phase: "commentary",
127
+ },
128
+ t,
129
+ 200,
130
+ );
131
+ r.tool(t, "c3", "apply_patch", { path: "src/checkout/summary.tsx" }, "Updated 1 file (+3 −5)");
132
+ r.tool(t, "c4", "exec_command", { cmd: "bun test checkout" });
133
+ return { id: "working", title: "Working", events: r.events, running: true };
134
+ }
135
+
136
+ function needsYou(): LabScenario {
137
+ const r = recorder("lab-needs-you");
138
+ const t = "turn-needs-you";
139
+ r.push("user.message", { text: "Ship the fix to the preview environment and tell the team." }, t);
140
+ r.push("turn.started", {}, t, 400);
141
+ r.tool(t, "n1", "exec_command", { cmd: "bun run build" }, "Build completed in 41s", 8_000);
142
+ r.tool(t, "n2", "list_channels", { query: "release" }, "#releases, #release-ops");
143
+ r.push(
144
+ "agent.toolCall.created",
145
+ {
146
+ id: "n3",
147
+ name: "deploy_preview",
148
+ arguments: { environment: "preview", ref: "fix/checkout-minor-units" },
149
+ },
150
+ t,
151
+ 900,
152
+ );
153
+ r.push(
154
+ "session.requiresAction",
155
+ {
156
+ approvals: [
157
+ {
158
+ id: "n3",
159
+ name: "deploy_preview",
160
+ arguments: { environment: "preview", ref: "fix/checkout-minor-units" },
161
+ },
162
+ ],
163
+ },
164
+ t,
165
+ 300,
166
+ );
167
+ r.push(
168
+ "session.humanInput.requested",
169
+ {
170
+ request: {
171
+ id: "hi-1",
172
+ allowSkip: true,
173
+ questions: [
174
+ {
175
+ id: "channel",
176
+ kind: "single_select",
177
+ prompt: "Where should I announce the preview?",
178
+ label: "Announcement",
179
+ required: true,
180
+ options: [
181
+ { id: "releases", label: "#releases", description: "Team-wide release notes" },
182
+ { id: "release-ops", label: "#release-ops", description: "Operators only" },
183
+ ],
184
+ },
185
+ ],
186
+ },
187
+ },
188
+ t,
189
+ 300,
190
+ );
191
+ return { id: "needs-you", title: "Needs you", events: r.events, running: true };
192
+ }
193
+
194
+ function history(): LabScenario {
195
+ const r = recorder("lab-history");
196
+ const a = "turn-h1";
197
+ r.push("user.message", { text: "Summarise yesterday's support tickets." }, a);
198
+ r.push("turn.started", {}, a, 400);
199
+ r.tool(a, "h1", "search_tickets", { since: "yesterday" }, "38 tickets", 4_000);
200
+ r.tool(a, "h2", "read_ticket", { id: "T-1182" }, "Customer cannot export invoices to PDF");
201
+ r.tool(a, "h3", "read_ticket", { id: "T-1190" }, "Login loop on Android 15");
202
+ const summary =
203
+ "38 tickets yesterday. The two themes:\n\n1. **PDF export** fails for invoices with attachments (11 tickets).\n2. **Android login loop** after the last release (7 tickets).\n\nEverything else was routine.";
204
+ r.push("agent.message.delta", { text: summary }, a, 2_000);
205
+ r.push("agent.message.completed", { text: summary }, a, 200);
206
+ r.push("turn.completed", { output: summary }, a, 200);
207
+ const b = "turn-h2";
208
+ r.push(
209
+ "user.message",
210
+ { text: "Open an issue for the Android one and assign it to mobile." },
211
+ b,
212
+ 60_000,
213
+ );
214
+ r.push("turn.started", {}, b, 400);
215
+ r.tool(
216
+ b,
217
+ "h4",
218
+ "create_issue",
219
+ { title: "Android login loop after release", team: "mobile" },
220
+ "Error: tracker token expired",
221
+ 2_000,
222
+ );
223
+ r.push(
224
+ "turn.failed",
225
+ { error: "The issue tracker connection needs to be reauthorized." },
226
+ b,
227
+ 400,
228
+ );
229
+ const c = "turn-h3";
230
+ r.push("user.message", { text: "Reconnected. Try again." }, c, 30_000);
231
+ r.push("turn.started", {}, c, 400);
232
+ r.tool(
233
+ c,
234
+ "h5",
235
+ "create_issue",
236
+ { title: "Android login loop after release", team: "mobile" },
237
+ "Created MOB-412",
238
+ 2_000,
239
+ );
240
+ const done = "Created **MOB-412** and assigned it to the mobile team.";
241
+ r.push("agent.message.delta", { text: done }, c, 1_000);
242
+ r.push("agent.message.completed", { text: done }, c, 200);
243
+ r.push("turn.completed", { output: done }, c, 200);
244
+ return { id: "history", title: "History", events: r.events, running: false };
245
+ }
246
+
247
+ function failure(): LabScenario {
248
+ const r = recorder("lab-failure");
249
+ const t = "turn-failure";
250
+ r.push("user.message", { text: "Deploy the latest build to staging." }, t);
251
+ r.push("turn.started", {}, t, 400);
252
+ r.tool(t, "f1", "exec_command", { cmd: "bun run build" }, "Build completed in 38s", 7_000);
253
+ r.tool(
254
+ t,
255
+ "f2",
256
+ "exec_command",
257
+ { cmd: "deploy --env staging" },
258
+ "error: staging cluster unreachable (connection refused after 3 attempts)",
259
+ 4_000,
260
+ );
261
+ r.push(
262
+ "turn.failed",
263
+ { error: "The staging cluster refused the connection. Nothing was deployed." },
264
+ t,
265
+ 400,
266
+ );
267
+ return { id: "failure", title: "Failure", events: r.events, running: false };
268
+ }
269
+
270
+ function longContent(): LabScenario {
271
+ const r = recorder("lab-long");
272
+ const t = "turn-long";
273
+ const question = [
274
+ "Here is the incident timeline from the on-call channel. Please turn it into a short postmortem with a summary, impact, root cause, a table of the timeline and the follow-ups.",
275
+ "",
276
+ "09:02 alerts fire for elevated 5xx on the checkout API; 09:05 on-call acknowledges; 09:11 rollback of release 412 starts; 09:19 error rate back to baseline; 09:40 root cause identified as a missing index after the orders migration; 10:15 index added and release 412 redeployed.",
277
+ ].join("\n");
278
+ r.push("user.message", { text: question }, t);
279
+ r.push("turn.started", {}, t, 400);
280
+ r.push(
281
+ "agent.reasoning.delta",
282
+ { text: "Structure it as a standard postmortem with a timeline table." },
283
+ t,
284
+ 1_500,
285
+ );
286
+ const text = [
287
+ "## Postmortem: checkout 5xx after release 412",
288
+ "",
289
+ "**Summary.** For 17 minutes the checkout API returned elevated server errors after release 412. Rolling back restored service; the fix shipped the same morning.",
290
+ "",
291
+ "**Impact.** Roughly 6% of checkout attempts failed between 09:02 and 09:19. No orders were lost, but some customers had to retry.",
292
+ "",
293
+ "**Root cause.** The orders migration in release 412 dropped and recreated a table without its `customer_id` index, so a hot query fell back to a sequential scan and timed out under load.",
294
+ "",
295
+ "### Timeline",
296
+ "",
297
+ "| Time | Event |",
298
+ "| --- | --- |",
299
+ "| 09:02 | Alerts fire for elevated 5xx on checkout |",
300
+ "| 09:05 | On-call acknowledges |",
301
+ "| 09:11 | Rollback of release 412 starts |",
302
+ "| 09:19 | Error rate back to baseline |",
303
+ "| 09:40 | Missing index identified |",
304
+ "| 10:15 | Index added, release 412 redeployed |",
305
+ "",
306
+ "### Follow-ups",
307
+ "",
308
+ "1. Add a migration check that fails when an index disappears.",
309
+ "2. Alert on query latency, not only error rate.",
310
+ "3. Document the rollback runbook link in the alert itself.",
311
+ "",
312
+ "```sql",
313
+ "CREATE INDEX CONCURRENTLY orders_customer_id_idx ON orders (customer_id);",
314
+ "```",
315
+ "",
316
+ "> Rollback was the right first move: it bought time to find the cause without pressure.",
317
+ ].join("\n");
318
+ r.push("agent.message.delta", { text }, t, 3_000);
319
+ r.push("agent.message.completed", { text }, t, 300);
320
+ r.push("turn.completed", { output: text }, t, 200);
321
+ return { id: "long", title: "Long content", events: r.events, running: false };
322
+ }
323
+
324
+ export function labScenarios(): LabScenario[] {
325
+ return [answer(), working(), needsYou(), history(), failure(), longContent()];
326
+ }
@@ -1,10 +1,12 @@
1
1
  import { KnowledgeReceiptRow } from "./knowledge-receipt";
2
+ import { workerRowTitle } from "./platform-activity-presentation";
2
3
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
3
4
  import { GenieLoading } from "./genie-loading";
4
5
  import { StartupDispatchDetails } from "./startup-dispatch-details";
5
6
  import { useStartupDetails } from "./startup-preference";
6
7
  import { ArrowRightIcon, BotIcon, BrainCircuitIcon, MessageSquareTextIcon } from "lucide-react";
7
8
  import {
9
+ Component,
8
10
  createContext,
9
11
  lazy,
10
12
  Suspense,
@@ -264,7 +266,9 @@ function ActivityNoteRow({ item }: { item: AgentMessageItem }) {
264
266
  {renderText ? (
265
267
  renderText(item.text, item)
266
268
  ) : (
267
- <Markdown streaming={item.streaming}>{item.text}</Markdown>
269
+ <Markdown softLineBreaks streaming={item.streaming}>
270
+ {item.text}
271
+ </Markdown>
268
272
  )}
269
273
  </div>
270
274
  </div>
@@ -305,13 +309,15 @@ export function renderActivity(
305
309
  case "tool-call": {
306
310
  const Renderer = toolRegistry.resolve(item);
307
311
  return (
308
- <ToolCallTruncationProvider value={item.truncation ?? null}>
309
- <Renderer
310
- item={item}
311
- loadRetainedScreenshot={loadRetainedScreenshot}
312
- loadRetainedArtifact={loadRetainedArtifact}
313
- />
314
- </ToolCallTruncationProvider>
312
+ <ToolRowBoundary name={toolDisplayName(item.name)} resetKeys={[item.status, Renderer]}>
313
+ <ToolCallTruncationProvider value={item.truncation ?? null}>
314
+ <Renderer
315
+ item={item}
316
+ loadRetainedScreenshot={loadRetainedScreenshot}
317
+ loadRetainedArtifact={loadRetainedArtifact}
318
+ />
319
+ </ToolCallTruncationProvider>
320
+ </ToolRowBoundary>
315
321
  );
316
322
  }
317
323
  case "worker":
@@ -457,22 +463,7 @@ function WorkerRow({
457
463
  const running = item.status === "running";
458
464
  const failed = item.status === "failed";
459
465
  const cancelled = item.status === "cancelled";
460
- const title =
461
- item.action === "spawn"
462
- ? running
463
- ? "Spawning worker"
464
- : failed
465
- ? "Worker spawn failed"
466
- : cancelled
467
- ? "Worker interrupted"
468
- : "Worker spawned"
469
- : running
470
- ? "Messaging worker"
471
- : failed
472
- ? "Worker message failed"
473
- : cancelled
474
- ? "Worker interrupted"
475
- : "Worker messaged";
466
+ const title = workerRowTitle(item);
476
467
  if (compact) {
477
468
  return (
478
469
  <ActivityDisclosure
@@ -560,3 +551,42 @@ function WorkerRow({
560
551
  }
561
552
  return <div className="flex items-start gap-2 px-1.5 py-1.5">{inner}</div>;
562
553
  }
554
+
555
+ /**
556
+ * One tool row that fails to render shows a one-line note in its place, so the
557
+ * rest of the work group (and the live preview) keeps reading normally.
558
+ */
559
+ class ToolRowBoundary extends Component<
560
+ { name: string; resetKeys: readonly unknown[]; children: ReactNode },
561
+ { failed: boolean; resetKeys: readonly unknown[] }
562
+ > {
563
+ state = { failed: false, resetKeys: this.props.resetKeys };
564
+
565
+ static getDerivedStateFromError(): { failed: boolean } {
566
+ return { failed: true };
567
+ }
568
+
569
+ /** A new status or renderer for the row gets a fresh attempt. */
570
+ static getDerivedStateFromProps(
571
+ props: { resetKeys: readonly unknown[] },
572
+ state: { failed: boolean; resetKeys: readonly unknown[] },
573
+ ): { failed: boolean; resetKeys: readonly unknown[] } | null {
574
+ const same =
575
+ props.resetKeys.length === state.resetKeys.length &&
576
+ props.resetKeys.every((key, index) => Object.is(key, state.resetKeys[index]));
577
+ return same ? null : { failed: false, resetKeys: props.resetKeys };
578
+ }
579
+
580
+ render(): ReactNode {
581
+ if (!this.state.failed) return this.props.children;
582
+ return (
583
+ <div
584
+ data-testid="tool-row-render-error"
585
+ role="status"
586
+ className="px-1.5 py-1.5 text-og-menu text-og-fg-subtle"
587
+ >
588
+ {this.props.name} · couldn't be displayed
589
+ </div>
590
+ );
591
+ }
592
+ }
@@ -0,0 +1,19 @@
1
+ /** Decorative loading copy shared by every renderer of the preparing state. */
2
+ export const GENIE_PREPARING_PHRASES: readonly string[] = [
3
+ "Polishing the lamp…",
4
+ "Consulting the carpet…",
5
+ "Untangling wishes…",
6
+ "Summoning a little cleverness…",
7
+ "Checking the fine print on infinity…",
8
+ "Warming up the abracadabra…",
9
+ "Rearranging the stars…",
10
+ "Negotiating with the lamp…",
11
+ "Dusting off a thousand years…",
12
+ "Wishful thinking…",
13
+ "Decanting a little magic…",
14
+ "Finding the good stardust…",
15
+ "Fluffing the magic carpet…",
16
+ "Putting a wish into motion…",
17
+ "A little hocus. A little pocus…",
18
+ ];
19
+ export const GENIE_WAITING_PHRASES: readonly string[] = ["Waiting for a response…"];
@@ -1,5 +1,6 @@
1
1
  import { ThinkingOrb } from "thinking-orbs";
2
2
  import { useThemeType } from "../lib/use-theme-type";
3
+ import { GENIE_PREPARING_PHRASES, GENIE_WAITING_PHRASES } from "./genie-copy";
3
4
  import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
4
5
 
5
6
  export type GenieLoadingRenderProps = {
@@ -39,24 +40,15 @@ export type GenieLoadingOptions = {
39
40
  };
40
41
  export const GenieLoadingOptionsContext = createContext<GenieLoadingOptions | undefined>(undefined);
41
42
 
42
- const PHRASES = [
43
- "Polishing the lamp…",
44
- "Consulting the carpet…",
45
- "Untangling wishes…",
46
- "Summoning a little cleverness…",
47
- "Checking the fine print on infinity…",
48
- "Warming up the abracadabra…",
49
- "Rearranging the stars…",
50
- "Negotiating with the lamp…",
51
- "Dusting off a thousand years…",
52
- "Wishful thinking…",
53
- "Decanting a little magic…",
54
- "Finding the good stardust…",
55
- "Fluffing the magic carpet…",
56
- "Putting a wish into motion…",
57
- "A little hocus. A little pocus…",
58
- ];
59
- const WAITING_PHRASES = ["Waiting for a response…"];
43
+ /**
44
+ * Neutral defaults for conversations embedded in another product
45
+ * (`SessionConversation`, `OpenGeniChat`), which should not carry Opengeni's
46
+ * own playful copy. The Opengeni app keeps the defaults above.
47
+ */
48
+ export const EMBEDDED_GENIE_LOADING = {
49
+ phrases: ["Thinking…"],
50
+ messages: { showDetails: "Show details" },
51
+ } as const satisfies GenieLoadingOptions;
60
52
 
61
53
  /** Decorative copy never substitutes for a failure or claims measurable progress. */
62
54
  export function GenieLoading({
@@ -75,7 +67,11 @@ export function GenieLoading({
75
67
  }) {
76
68
  const options = useContext(GenieLoadingOptionsContext);
77
69
  const waiting = phase === "waiting";
78
- const phrases = options?.phrases?.length ? options.phrases : waiting ? WAITING_PHRASES : PHRASES;
70
+ const phrases = options?.phrases?.length
71
+ ? options.phrases
72
+ : waiting
73
+ ? GENIE_WAITING_PHRASES
74
+ : GENIE_PREPARING_PHRASES;
79
75
  const theme = useThemeType(undefined);
80
76
  const [phrase, setPhrase] = useState(0);
81
77
  const [showDetails, setShowDetails] = useState(false);
@@ -74,6 +74,7 @@ export type {
74
74
  export type {
75
75
  CreateToolRegistryOptions,
76
76
  RetainedArtifactLoader,
77
+ RetainedArtifactLoadOptions,
77
78
  RetainedScreenshotLoader,
78
79
  VideoArtifactPlaybackLoader,
79
80
  ToolRegistry,
@@ -0,0 +1,22 @@
1
+ import type { NoticeItem } from "./types";
2
+
3
+ /** An approval notice the person has since answered. */
4
+ export function noticeIsResolvedApproval(item: Pick<NoticeItem, "text" | "resolvedAt">): boolean {
5
+ return Boolean(item.resolvedAt && item.text.startsWith("Approval needed"));
6
+ }
7
+
8
+ /** A resolved approval notice reads as history. */
9
+ export function noticeDisplayText(item: Pick<NoticeItem, "text" | "resolvedAt">): string {
10
+ return noticeIsResolvedApproval(item) ? "You responded to this approval." : item.text;
11
+ }
12
+
13
+ /** Notice pill tone: failures stay red, an open wait keeps the waiting hue. */
14
+ export function noticeTone(
15
+ item: Pick<NoticeItem, "tone" | "resolvedAt">,
16
+ ): "failed" | "waiting" | "neutral" {
17
+ return item.tone === "failed"
18
+ ? "failed"
19
+ : item.tone === "waiting" && !item.resolvedAt
20
+ ? "waiting"
21
+ : "neutral";
22
+ }
@@ -0,0 +1,135 @@
1
+ import type { RetainedArtifactReference } from "@opengeni/sdk";
2
+ import { CheckIcon, TerminalIcon } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { copyPendingTextToClipboard } from "../lib/clipboard";
5
+ import type { RetainedArtifactLoader } from "./registry";
6
+
7
+ export { isPatchFilename } from "./presented-image";
8
+
9
+ /** POSIX single-quote one argument so the URL reaches curl unchanged. */
10
+ function shellQuote(value: string): string {
11
+ return `'${value.replaceAll("'", `'\\''`)}'`;
12
+ }
13
+
14
+ /** One command that downloads this patch and applies it in the current folder. */
15
+ export function patchApplyCommand(url: string): string {
16
+ return `curl -fsSL ${shellQuote(url)} | git apply`;
17
+ }
18
+
19
+ /** "5 minutes" from the signed URL's expiry; the default download URL lifetime otherwise. */
20
+ export function applyCommandLifetime(expiresAt: string | undefined, now = Date.now()): string {
21
+ const expires = expiresAt ? Date.parse(expiresAt) : Number.NaN;
22
+ const minutes = Number.isFinite(expires) ? Math.round((expires - now) / 60_000) : 5;
23
+ if (minutes <= 1) return "1 minute";
24
+ return `${minutes} minutes`;
25
+ }
26
+
27
+ type ApplyState =
28
+ | { kind: "idle" }
29
+ | { kind: "loading" }
30
+ | { kind: "copied"; lifetime: string }
31
+ | { kind: "manual"; command: string; lifetime: string }
32
+ | { kind: "unavailable" }
33
+ | { kind: "error" };
34
+
35
+ const BUTTON_CLASS =
36
+ "inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-surface-2 disabled:cursor-wait disabled:opacity-60";
37
+
38
+ /**
39
+ * "Copy command to apply these changes": mints the existing short-lived,
40
+ * authenticated download URL for this published patch (the same authority as
41
+ * Download) and copies `curl -fsSL '<url>' | git apply`. The URL is minted on
42
+ * click, so the copied command works for the URL's lifetime only.
43
+ */
44
+ export function PatchApplyCommand({
45
+ artifact,
46
+ filename,
47
+ load,
48
+ }: {
49
+ artifact: RetainedArtifactReference;
50
+ filename: string;
51
+ load: RetainedArtifactLoader | undefined;
52
+ }) {
53
+ const [state, setState] = useState<ApplyState>({ kind: "idle" });
54
+
55
+ const copy = async () => {
56
+ if (!load) {
57
+ setState({ kind: "unavailable" });
58
+ return;
59
+ }
60
+ setState({ kind: "loading" });
61
+ let lifetime = "5 minutes";
62
+ // Start the URL request inside the click so the clipboard write may wait for it.
63
+ const command = load(artifact, new AbortController().signal, { prefer: "url" }).then(
64
+ (source) => {
65
+ if (!source || source instanceof Uint8Array) throw new UnavailableError();
66
+ lifetime = applyCommandLifetime(source.expiresAt);
67
+ return patchApplyCommand(source.url);
68
+ },
69
+ );
70
+ const copied = copyPendingTextToClipboard(command);
71
+ try {
72
+ const text = await command;
73
+ if (await copied) setState({ kind: "copied", lifetime });
74
+ else setState({ kind: "manual", command: text, lifetime });
75
+ } catch (failure) {
76
+ setState({ kind: failure instanceof UnavailableError ? "unavailable" : "error" });
77
+ }
78
+ };
79
+
80
+ const label =
81
+ state.kind === "loading"
82
+ ? "Preparing…"
83
+ : state.kind === "copied"
84
+ ? "Copied"
85
+ : state.kind === "error"
86
+ ? "Try again"
87
+ : "Copy command to apply these changes";
88
+
89
+ return (
90
+ <div className="flex min-w-0 flex-col gap-1.5">
91
+ <div>
92
+ <button
93
+ type="button"
94
+ onClick={() => void copy()}
95
+ disabled={state.kind === "loading"}
96
+ className={BUTTON_CLASS}
97
+ >
98
+ {state.kind === "copied" ? (
99
+ <CheckIcon className="size-3.5" aria-hidden />
100
+ ) : (
101
+ <TerminalIcon className="size-3.5" aria-hidden />
102
+ )}
103
+ {label}
104
+ </button>
105
+ </div>
106
+ <p className="m-0 text-og-xs leading-relaxed text-og-fg-muted" role="status">
107
+ {state.kind === "copied"
108
+ ? `Copied. Paste it in a terminal in your project folder and press Enter. The link in it expires in ${state.lifetime}.`
109
+ : state.kind === "manual"
110
+ ? `Copy this command, then run it in your project folder. The link in it expires in ${state.lifetime}.`
111
+ : state.kind === "unavailable"
112
+ ? `Download ${filename}, then run git apply ${filename} in your project folder.`
113
+ : state.kind === "error"
114
+ ? "Couldn't prepare the command. Try again, or download the file and run git apply."
115
+ : "Runs git apply in your project folder. Works on macOS, Linux and Git Bash."}
116
+ </p>
117
+ {state.kind === "manual" ? (
118
+ <input
119
+ readOnly
120
+ value={state.command}
121
+ aria-label="Command to apply these changes"
122
+ onFocus={(event) => event.currentTarget.select()}
123
+ className="w-full min-w-0 rounded-og-sm border border-og-border bg-og-surface-2 px-2 py-1.5 font-mono text-og-xs text-og-fg"
124
+ />
125
+ ) : null}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ class UnavailableError extends Error {
131
+ constructor() {
132
+ super("A download URL is unavailable here");
133
+ this.name = "UnavailableError";
134
+ }
135
+ }