@opengeni/react 1.4.4 → 1.4.5-canary.37458084490001

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 (234) 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-3ZC6QPOM.js +174 -0
  7. package/dist/chunk-3ZC6QPOM.js.map +1 -0
  8. package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
  9. package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
  10. package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
  11. package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
  12. package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
  13. package/dist/chunk-7RRBX5EM.js.map +1 -0
  14. package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
  15. package/dist/chunk-7ZB7I3KP.js.map +1 -0
  16. package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
  17. package/dist/chunk-ATIRUHBZ.js.map +1 -0
  18. package/dist/chunk-BOFEQC6K.js +162 -0
  19. package/dist/chunk-BOFEQC6K.js.map +1 -0
  20. package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
  21. package/dist/chunk-CESBPQP5.js.map +1 -0
  22. package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
  23. package/dist/chunk-GHJU7XG7.js.map +1 -0
  24. package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
  25. package/dist/chunk-HNVWMDNN.js.map +1 -0
  26. package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
  27. package/dist/chunk-I5EGRXXG.js.map +1 -0
  28. package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
  29. package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
  30. package/dist/chunk-IX3K4THH.js.map +1 -0
  31. package/dist/chunk-IZV5FTJC.js +55 -0
  32. package/dist/chunk-IZV5FTJC.js.map +1 -0
  33. package/dist/chunk-JDJJMBB3.js +1883 -0
  34. package/dist/chunk-JDJJMBB3.js.map +1 -0
  35. package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
  36. package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
  37. package/dist/chunk-LHF6BVM4.js +83 -0
  38. package/dist/chunk-LHF6BVM4.js.map +1 -0
  39. package/dist/chunk-N7S72EXK.js +25 -0
  40. package/dist/chunk-N7S72EXK.js.map +1 -0
  41. package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
  42. package/dist/chunk-PEHTX3V7.js.map +1 -0
  43. package/dist/chunk-R5R5RQQD.js +51 -0
  44. package/dist/chunk-R5R5RQQD.js.map +1 -0
  45. package/dist/chunk-RI42H5IU.js +326 -0
  46. package/dist/chunk-RI42H5IU.js.map +1 -0
  47. package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
  48. package/dist/chunk-SHLZMAJN.js.map +1 -0
  49. package/dist/{chunk-EAOSEIXB.js → chunk-TZQSHCZN.js} +692 -1996
  50. package/dist/chunk-TZQSHCZN.js.map +1 -0
  51. package/dist/chunk-ZIB4H5LN.js +101 -0
  52. package/dist/chunk-ZIB4H5LN.js.map +1 -0
  53. package/dist/client.d.ts +9 -3
  54. package/dist/clipboard.js +1 -1
  55. package/dist/components/browser-viewer.d.ts +6 -0
  56. package/dist/components/composer.d.ts +3 -69
  57. package/dist/components/human-input-form.d.ts +3 -32
  58. package/dist/components/markdown.d.ts +8 -0
  59. package/dist/components/model-policy-picker.d.ts +4 -37
  60. package/dist/components/opper-mark.d.ts +7 -0
  61. package/dist/components/provider-recovery-notice.d.ts +15 -0
  62. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  63. package/dist/components/sandbox-workspace.d.ts +13 -0
  64. package/dist/components/session-chrome.d.ts +5 -3
  65. package/dist/components/session-conversation.d.ts +15 -3
  66. package/dist/composer-messages.d.ts +74 -0
  67. package/dist/composer-messages.js +9 -0
  68. package/dist/composer.js +15 -10
  69. package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
  70. package/dist/connect-setup.d.ts +2 -0
  71. package/dist/connect.d.ts +1 -1
  72. package/dist/connect.js +21 -6
  73. package/dist/connect.js.map +1 -1
  74. package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
  75. package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
  76. package/dist/hooks/shared-machines-list.d.ts +4 -0
  77. package/dist/hooks/use-client-config-flags.d.ts +6 -0
  78. package/dist/hooks/use-goal.d.ts +2 -1
  79. package/dist/human-input-model.d.ts +40 -0
  80. package/dist/index.d.ts +4 -1
  81. package/dist/index.js +116 -73
  82. package/dist/index.js.map +1 -1
  83. package/dist/interaction.js +5 -5
  84. package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
  85. package/dist/lib/clipboard.d.ts +7 -0
  86. package/dist/lib/provider-recovery.d.ts +49 -0
  87. package/dist/machines.js +2 -2
  88. package/dist/model-mark-paths.d.ts +34 -0
  89. package/dist/model-mark-paths.js +16 -0
  90. package/dist/model-picker-model.d.ts +111 -0
  91. package/dist/model-picker-model.js +25 -0
  92. package/dist/model-picker-model.js.map +1 -0
  93. package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
  94. package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
  95. package/dist/model-policy.d.ts +10 -0
  96. package/dist/model-policy.js +5 -1
  97. package/dist/organization-model.d.ts +32 -0
  98. package/dist/organization-model.js +65 -0
  99. package/dist/organization-model.js.map +1 -0
  100. package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
  101. package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
  102. package/dist/queue-presentation-model.d.ts +72 -0
  103. package/dist/queue-presentation-model.js +38 -0
  104. package/dist/queue-presentation-model.js.map +1 -0
  105. package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
  106. package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
  107. package/dist/realtime.js +5 -3
  108. package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
  109. package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
  110. package/dist/sandbox-label-model.d.ts +23 -0
  111. package/dist/sandbox-label-model.js +28 -0
  112. package/dist/sandbox-label-model.js.map +1 -0
  113. package/dist/session-attention-model.d.ts +8 -0
  114. package/dist/session-attention-model.js +46 -0
  115. package/dist/session-attention-model.js.map +1 -0
  116. package/dist/session-feedback-model.d.ts +20 -0
  117. package/dist/session-feedback-model.js +56 -0
  118. package/dist/session-feedback-model.js.map +1 -0
  119. package/dist/session-list-model.d.ts +96 -0
  120. package/dist/session-list-model.js +219 -0
  121. package/dist/session-list-model.js.map +1 -0
  122. package/dist/session-status-model.d.ts +19 -0
  123. package/dist/session-ui.js +26 -17
  124. package/dist/session-ui.js.map +1 -1
  125. package/dist/session.js +8 -6
  126. package/dist/testing/session-scenarios.d.ts +10 -0
  127. package/dist/testing/session-scenarios.js +303 -0
  128. package/dist/testing/session-scenarios.js.map +1 -0
  129. package/dist/timeline/genie-copy.d.ts +3 -0
  130. package/dist/timeline/genie-loading.d.ts +11 -0
  131. package/dist/timeline/index.d.ts +1 -1
  132. package/dist/timeline/notice-presentation.d.ts +7 -0
  133. package/dist/timeline/patch-apply-command.d.ts +18 -0
  134. package/dist/timeline/platform-activity-presentation.d.ts +7 -0
  135. package/dist/timeline/presented-image.d.ts +6 -1
  136. package/dist/timeline/question-nav-model.d.ts +23 -0
  137. package/dist/timeline/registry.d.ts +10 -1
  138. package/dist/timeline/retained-loaders.d.ts +1 -1
  139. package/dist/timeline/tool-presentation.d.ts +172 -0
  140. package/dist/timeline/turn-summary-model.d.ts +90 -0
  141. package/dist/timeline/turn-summary.d.ts +4 -74
  142. package/dist/timeline/work-presentation.d.ts +42 -0
  143. package/dist/timeline-model.d.ts +24 -0
  144. package/dist/timeline-model.js +152 -0
  145. package/dist/timeline-model.js.map +1 -0
  146. package/package.json +47 -3
  147. package/src/artifacts.ts +1 -0
  148. package/src/client.ts +9 -3
  149. package/src/components/browser-viewer.tsx +23 -2
  150. package/src/components/claude-mark.tsx +2 -1
  151. package/src/components/composer.tsx +4 -144
  152. package/src/components/grok-mark.tsx +2 -1
  153. package/src/components/human-input-form.tsx +13 -124
  154. package/src/components/markdown.tsx +19 -1
  155. package/src/components/message-timeline.tsx +45 -99
  156. package/src/components/model-mark.tsx +2 -1
  157. package/src/components/model-policy-picker-menu.tsx +30 -85
  158. package/src/components/model-policy-picker.tsx +15 -148
  159. package/src/components/opper-mark.tsx +18 -0
  160. package/src/components/provider-recovery-notice.tsx +41 -0
  161. package/src/components/queue-surface-implementation.tsx +20 -349
  162. package/src/components/remark-soft-line-breaks.ts +166 -0
  163. package/src/components/sandbox-workspace.tsx +17 -1
  164. package/src/components/session-chrome.tsx +114 -99
  165. package/src/components/session-conversation.tsx +55 -6
  166. package/src/components/session-status.tsx +17 -16
  167. package/src/composer-messages.ts +150 -0
  168. package/src/connect-setup.tsx +45 -11
  169. package/src/connect.ts +1 -1
  170. package/src/hooks/shared-machines-list.ts +34 -1
  171. package/src/hooks/use-client-config-flags.ts +8 -0
  172. package/src/hooks/use-goal.ts +66 -10
  173. package/src/human-input-model.ts +147 -0
  174. package/src/index.ts +16 -0
  175. package/src/lib/clipboard.ts +29 -0
  176. package/src/lib/format.ts +7 -0
  177. package/src/lib/provider-recovery.ts +202 -0
  178. package/src/model-mark-paths.ts +53 -0
  179. package/src/model-picker-model.ts +330 -0
  180. package/src/model-policy.ts +43 -2
  181. package/src/organization-model.ts +118 -0
  182. package/src/queue-presentation-model.ts +464 -0
  183. package/src/sandbox-label-model.ts +41 -0
  184. package/src/session-attention-model.ts +59 -0
  185. package/src/session-feedback-model.ts +95 -0
  186. package/src/session-list-model.ts +327 -0
  187. package/src/session-status-model.ts +62 -0
  188. package/src/testing/session-scenarios.ts +326 -0
  189. package/src/timeline/activity-rail.tsx +5 -17
  190. package/src/timeline/genie-copy.ts +19 -0
  191. package/src/timeline/genie-loading.tsx +13 -18
  192. package/src/timeline/index.ts +1 -0
  193. package/src/timeline/notice-presentation.ts +22 -0
  194. package/src/timeline/patch-apply-command.tsx +135 -0
  195. package/src/timeline/platform-activity-presentation.ts +122 -0
  196. package/src/timeline/platform-activity-row.tsx +6 -96
  197. package/src/timeline/presented-image.ts +16 -2
  198. package/src/timeline/question-nav-model.ts +28 -0
  199. package/src/timeline/registry.ts +11 -1
  200. package/src/timeline/retained-loaders.ts +9 -4
  201. package/src/timeline/rolling-activity.tsx +7 -19
  202. package/src/timeline/tool-presentation.ts +1316 -0
  203. package/src/timeline/tool-renderers.tsx +226 -1025
  204. package/src/timeline/turn-summary-model.ts +338 -0
  205. package/src/timeline/turn-summary.tsx +21 -326
  206. package/src/timeline/work-presentation.ts +139 -0
  207. package/src/timeline-model.ts +108 -0
  208. package/styles/compiled.css +3 -0
  209. package/styles/connect.css +4 -0
  210. package/dist/chunk-2OQA3CPB.js.map +0 -1
  211. package/dist/chunk-CVYCLFMH.js.map +0 -1
  212. package/dist/chunk-EAOSEIXB.js.map +0 -1
  213. package/dist/chunk-G6YRCIYJ.js.map +0 -1
  214. package/dist/chunk-HXYOSSQO.js +0 -117
  215. package/dist/chunk-HXYOSSQO.js.map +0 -1
  216. package/dist/chunk-IUHHPMAX.js.map +0 -1
  217. package/dist/chunk-IYQCWGU5.js.map +0 -1
  218. package/dist/chunk-JH7V627O.js.map +0 -1
  219. package/dist/chunk-L2J3PU7A.js.map +0 -1
  220. package/dist/chunk-LVS3MH5T.js.map +0 -1
  221. package/dist/chunk-PHBFJCEU.js.map +0 -1
  222. package/dist/chunk-XS67YG5R.js.map +0 -1
  223. package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
  224. package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
  225. /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
  226. /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
  227. /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
  228. /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
  229. /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
  230. /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
  231. /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
  232. /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
  233. /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
  234. /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * User-facing names for where a session's commands run, shared by the web
3
+ * sandbox switcher and every timeline that labels command rows ("on Cloud
4
+ * sandbox · …"). No DOM, no React.
5
+ */
6
+ import type { SandboxBackend } from "@opengeni/sdk";
7
+ import type { MachineView } from "./types/machines";
8
+
9
+ export const CLOUD_SANDBOX_LABEL = "Cloud sandbox";
10
+ export const NO_SANDBOX_LABEL = "No sandbox";
11
+ export const LOCAL_SANDBOX_LABEL = "Local sandbox";
12
+
13
+ /**
14
+ * User-facing name for a session's own managed box. Hosting vendors (Modal,
15
+ * Daytona, ...) are deployment detail, not something a user chose, so hosted
16
+ * providers read as the neutral "Cloud sandbox" and the local-dev Docker box as
17
+ * "Local sandbox". The local backend runs directly on the host, so it keeps the
18
+ * honest "this computer" instead of implying isolation. Connected Machines keep
19
+ * their own names.
20
+ */
21
+ export function sessionSandboxLabel(backend: SandboxBackend | string): string {
22
+ if (backend === "none") return NO_SANDBOX_LABEL;
23
+ if (backend === "local") return "this computer";
24
+ if (backend === "docker") return LOCAL_SANDBOX_LABEL;
25
+ return CLOUD_SANDBOX_LABEL;
26
+ }
27
+
28
+ /** Display name for a fleet row: the session's own box never shows its vendor. */
29
+ export function machineDisplayName(
30
+ machine: Pick<MachineView, "isSessionGroup" | "kind" | "name">,
31
+ ): string {
32
+ return machine.isSessionGroup ? sessionSandboxLabel(machine.kind) : machine.name;
33
+ }
34
+
35
+ /** The compute label a session timeline shows: the active machine, else the cloud box. */
36
+ export function sessionComputeLabel(
37
+ machines: readonly Pick<MachineView, "active" | "isSessionGroup" | "kind" | "name">[],
38
+ ): string {
39
+ const active = machines.find((machine) => machine.active);
40
+ return active ? machineDisplayName(active) : CLOUD_SANDBOX_LABEL;
41
+ }
@@ -0,0 +1,59 @@
1
+ import type { SessionEvent } from "@opengeni/sdk";
2
+
3
+ // Keep this client scheduling predicate aligned with the meaningful-attention
4
+ // predicate in packages/db/src/session-meaningful-events.ts. The server remains
5
+ // authoritative; this only avoids personal-state writes for streaming activity.
6
+ export const SESSION_ATTENTION_EVENT_TYPES: ReadonlySet<string> = new Set([
7
+ "agent.message.completed",
8
+ "turn.completed",
9
+ "turn.failed",
10
+ "session.requiresAction",
11
+ "session.humanInput.requested",
12
+ "tool.auth_needed",
13
+ "credential.auth_needed",
14
+ "goal.completed",
15
+ "goal.paused",
16
+ "goal.progress",
17
+ "goal.rewrite.proposed",
18
+ "rig.setup.failed",
19
+ "sandbox.operation.failed",
20
+ "sandbox.box.lost",
21
+ "workspace.revision.degraded",
22
+ "machine.op.failed",
23
+ "machine.link.lost",
24
+ "session.event.envelope_omitted",
25
+ ]);
26
+
27
+ /**
28
+ * The latest delivered attention boundary, independent of raw token activity.
29
+ * A foreground reader acknowledges through this sequence, so streaming flushes
30
+ * never cause a personal-state write and later activity stays unread.
31
+ */
32
+ export function sessionAttentionReadThroughSequence(events: readonly SessionEvent[]): number {
33
+ for (let index = events.length - 1; index >= 0; index -= 1) {
34
+ const event = events[index]!;
35
+ if (
36
+ !SESSION_ATTENTION_EVENT_TYPES.has(event.type) ||
37
+ event.duplicateOfEventId ||
38
+ (event.turnAssociation != null && event.turnAssociation !== "current")
39
+ )
40
+ continue;
41
+ const payload =
42
+ event.payload && typeof event.payload === "object"
43
+ ? (event.payload as Record<string, unknown>)
44
+ : {};
45
+ if (
46
+ event.type === "agent.message.completed" &&
47
+ (payload.phase === "commentary" || payload.text == null || payload.text === "")
48
+ )
49
+ continue;
50
+ if (event.type === "turn.completed") {
51
+ if (Object.hasOwn(payload, "maintenance") || Object.hasOwn(payload, "segmentLimit")) continue;
52
+ const output = payload.output ?? payload.result;
53
+ if ((output == null || output === "") && (payload.reply == null || payload.reply === ""))
54
+ continue;
55
+ }
56
+ return event.sequence;
57
+ }
58
+ return 0;
59
+ }
@@ -0,0 +1,95 @@
1
+ // Reply and session feedback shared by web and native: the one feedback read
2
+ // per session (with recovery), saved turn ratings, the submission payload with
3
+ // a retained idempotency key, and the dialog copy. No DOM, no React.
4
+ import type {
5
+ CreateFeedbackRequest,
6
+ Feedback,
7
+ FeedbackSentiment,
8
+ OpenGeniClient,
9
+ } from "@opengeni/sdk";
10
+
11
+ export type FeedbackClient = Pick<OpenGeniClient, "createFeedback" | "listOwnFeedback">;
12
+
13
+ /** One shared read per session, with recovery after a transient network failure. */
14
+ export function loadSessionFeedback(
15
+ client: Pick<OpenGeniClient, "listOwnFeedback">,
16
+ workspaceId: string,
17
+ sessionId: string,
18
+ onLoaded: (result: Awaited<ReturnType<OpenGeniClient["listOwnFeedback"]>>) => void,
19
+ ): () => void {
20
+ let current = true;
21
+ let delay = 1_000;
22
+ let timer: ReturnType<typeof setTimeout> | undefined;
23
+ const load = () => {
24
+ void client.listOwnFeedback(workspaceId, { sessionId, includeTurns: true }).then(
25
+ (result) => {
26
+ if (current) onLoaded(result);
27
+ },
28
+ () => {
29
+ if (!current) return;
30
+ timer = setTimeout(load, delay);
31
+ delay = Math.min(delay * 2, 30_000);
32
+ },
33
+ );
34
+ };
35
+ load();
36
+ return () => {
37
+ current = false;
38
+ clearTimeout(timer);
39
+ };
40
+ }
41
+
42
+ /** The first saved rating per turn (the list is newest-first). */
43
+ export function turnRatingsFromFeedback(
44
+ feedback: readonly Feedback[],
45
+ ): Record<string, FeedbackSentiment> {
46
+ const ratings: Record<string, FeedbackSentiment> = {};
47
+ for (const entry of feedback) {
48
+ if (entry.turnId && entry.sentiment && !ratings[entry.turnId])
49
+ ratings[entry.turnId] = entry.sentiment;
50
+ }
51
+ return ratings;
52
+ }
53
+
54
+ export type FeedbackPayload = Omit<CreateFeedbackRequest, "idempotencyKey">;
55
+
56
+ /**
57
+ * The request to send, reusing the previous idempotency key when the user
58
+ * retries the identical submission (a lost response must not duplicate it).
59
+ */
60
+ export function feedbackSubmission(
61
+ payload: FeedbackPayload,
62
+ previous: CreateFeedbackRequest | null,
63
+ newKey: () => string,
64
+ ): CreateFeedbackRequest {
65
+ if (
66
+ previous &&
67
+ previous.sessionId === payload.sessionId &&
68
+ previous.turnId === payload.turnId &&
69
+ previous.sentiment === payload.sentiment &&
70
+ previous.comment === payload.comment
71
+ ) {
72
+ return previous;
73
+ }
74
+ return { ...payload, idempotencyKey: newKey() };
75
+ }
76
+
77
+ export function feedbackDialogCopy(target: {
78
+ sessionId?: string | undefined;
79
+ turnId?: string | undefined;
80
+ }) {
81
+ return {
82
+ title: target.turnId
83
+ ? "Rate this reply"
84
+ : target.sessionId
85
+ ? "Rate this session"
86
+ : "Send feedback",
87
+ description: target.sessionId
88
+ ? "Share what worked or what could be better."
89
+ : "Tell us what could make Opengeni better.",
90
+ };
91
+ }
92
+
93
+ export function feedbackSentimentLabel(sentiment: FeedbackSentiment): string {
94
+ return sentiment === "positive" ? "Thumbs up" : "Thumbs down";
95
+ }
@@ -0,0 +1,327 @@
1
+ // Pure session-list presentation shared by the web app (rail and recent
2
+ // sessions) and native renderers: relative-time labels, recency bucketing
3
+ // (Today / Yesterday / Previous 7 days / Older), the running-first ordering
4
+ // rule, status tones and the compact row metadata. No DOM, no React.
5
+ import {
6
+ deriveAutomaticSessionTitlePreview,
7
+ deriveSessionDisplayTitle,
8
+ sessionTitleIsPending,
9
+ type ClientModel,
10
+ type Session,
11
+ type SessionListEntry,
12
+ type SessionStatus,
13
+ } from "@opengeni/sdk";
14
+ import { modelDisplayName } from "@opengeni/sdk/model-display";
15
+ import { findPickerRow, type PickerModelRow } from "./model-policy";
16
+
17
+ /** A list row may be a compact page entry or a complete session response. */
18
+ export type SessionListRow = Session | SessionListEntry;
19
+
20
+ /**
21
+ * The title shown for a session: durable agent/user metadata once available,
22
+ * otherwise a bounded opening-prompt preview while automatic naming is still
23
+ * pending. Every list, header and row reads identically.
24
+ */
25
+ export function sessionDisplayTitle(session: SessionListRow): string {
26
+ return "displayTitle" in session ? session.displayTitle : deriveSessionDisplayTitle(session);
27
+ }
28
+
29
+ /** The maximum length a session title may be renamed to. */
30
+ export const SESSION_TITLE_MAX_LENGTH = 200;
31
+
32
+ /**
33
+ * The value a rename editor seeds from. A safe provisional prompt preview is
34
+ * editable because it is also what the user sees; the UUID-derived reference
35
+ * remains an empty draft rather than becoming an accidental rename.
36
+ */
37
+ export function sessionRenameSeed(session: SessionListRow): string {
38
+ if ("renameSeed" in session) return session.renameSeed;
39
+ if (!sessionTitleIsPending(session)) {
40
+ return session.title?.trim() || "";
41
+ }
42
+ return deriveAutomaticSessionTitlePreview(session.initialMessage) ?? "";
43
+ }
44
+
45
+ /**
46
+ * Resolve a submitted draft against the current display title. Returns the
47
+ * trimmed title to persist, or `null` when the edit is a no-op (empty,
48
+ * unchanged from what is already shown, or still equal to the value that
49
+ * seeded this edit) and should simply cancel.
50
+ */
51
+ export function resolveRenameSubmission(
52
+ draft: string,
53
+ display: string,
54
+ editSeed?: string,
55
+ ): string | null {
56
+ const next = draft.trim();
57
+ if (!next || next === display || (editSeed !== undefined && next === editSeed.trim())) {
58
+ return null;
59
+ }
60
+ return next;
61
+ }
62
+
63
+ export type SessionRecencyGroup = "today" | "yesterday" | "previous7" | "older";
64
+
65
+ export const SESSION_GROUP_LABELS: Record<SessionRecencyGroup, string> = {
66
+ today: "Today",
67
+ yesterday: "Yesterday",
68
+ previous7: "Previous 7 days",
69
+ older: "Older",
70
+ };
71
+
72
+ /** The render order of recency groups, top → bottom. */
73
+ export const SESSION_GROUP_ORDER: SessionRecencyGroup[] = [
74
+ "today",
75
+ "yesterday",
76
+ "previous7",
77
+ "older",
78
+ ];
79
+
80
+ /** Live states that earn the pinned-to-top, breathing-dot treatment. */
81
+ const RUNNING_STATUSES = new Set<SessionStatus>([
82
+ "running",
83
+ "queued",
84
+ "waiting_capacity",
85
+ "recovering",
86
+ "requires_action",
87
+ ]);
88
+
89
+ export function isRunningStatus(status: SessionStatus): boolean {
90
+ return RUNNING_STATUSES.has(status);
91
+ }
92
+
93
+ /** An idle session waiting on its own timer, while its control is active. */
94
+ export function sessionInputWait(
95
+ session: Pick<SessionListRow, "status" | "effectiveControl" | "inputWait">,
96
+ ) {
97
+ return session.status === "idle" && session.effectiveControl?.state === "active"
98
+ ? (session.inputWait ?? null)
99
+ : null;
100
+ }
101
+
102
+ export function hasActiveEffectiveControl(session: SessionListRow): boolean {
103
+ return (session.effectiveControl?.state ?? "active") === "active";
104
+ }
105
+
106
+ export function isEffectivelyRunning(session: SessionListRow): boolean {
107
+ // Background commands have their own chat indicator, not agent working status.
108
+ return (
109
+ hasActiveEffectiveControl(session) &&
110
+ (isRunningStatus(session.status) || Boolean(sessionInputWait(session)))
111
+ );
112
+ }
113
+
114
+ /** Most-recent activity timestamp for a session (updatedAt, then createdAt). */
115
+ export function sessionActivityTime(session: SessionListRow): number {
116
+ const updated = Date.parse(session.updatedAt);
117
+ if (!Number.isNaN(updated)) {
118
+ return updated;
119
+ }
120
+ const created = Date.parse(session.createdAt);
121
+ return Number.isNaN(created) ? 0 : created;
122
+ }
123
+
124
+ /** Deterministic newest-first ordering for every flat or forest session list. */
125
+ export function compareSessionActivity(left: SessionListRow, right: SessionListRow): number {
126
+ return sessionActivityTime(right) - sessionActivityTime(left) || right.id.localeCompare(left.id);
127
+ }
128
+
129
+ /** Deterministic personal-pin order: newest pin first, then descending id. */
130
+ export function compareSessionPins(left: SessionListRow, right: SessionListRow): number {
131
+ const leftPinnedAt = Date.parse(left.pinnedAt ?? "");
132
+ const rightPinnedAt = Date.parse(right.pinnedAt ?? "");
133
+ const leftTime = Number.isNaN(leftPinnedAt) ? 0 : leftPinnedAt;
134
+ const rightTime = Number.isNaN(rightPinnedAt) ? 0 : rightPinnedAt;
135
+ return rightTime - leftTime || right.id.localeCompare(left.id);
136
+ }
137
+
138
+ /** Split explicit personal pins from ordinary rows without changing the input. */
139
+ export function partitionPinnedSessions<T extends SessionListRow>(
140
+ sessions: T[],
141
+ ): {
142
+ pinned: T[];
143
+ ordinary: T[];
144
+ } {
145
+ const pinned: T[] = [];
146
+ const ordinary: T[] = [];
147
+ for (const session of sessions) {
148
+ (session.pinned ? pinned : ordinary).push(session);
149
+ }
150
+ return { pinned: pinned.sort(compareSessionPins), ordinary };
151
+ }
152
+
153
+ /**
154
+ * Which recency bucket a timestamp falls into, relative to `now`. "Today" and
155
+ * "Yesterday" are calendar-local; "Previous 7 days" is the rest of the trailing
156
+ * week; everything earlier is "Older".
157
+ */
158
+ export function recencyGroupFor(timestampMs: number, now: Date = new Date()): SessionRecencyGroup {
159
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
160
+ const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
161
+ const startOfWeekWindow = startOfToday - 7 * 24 * 60 * 60 * 1000;
162
+ if (timestampMs >= startOfToday) {
163
+ return "today";
164
+ }
165
+ if (timestampMs >= startOfYesterday) {
166
+ return "yesterday";
167
+ }
168
+ if (timestampMs >= startOfWeekWindow) {
169
+ return "previous7";
170
+ }
171
+ return "older";
172
+ }
173
+
174
+ export type SessionRecencyBucket<T extends SessionListRow = SessionListRow> = {
175
+ group: SessionRecencyGroup;
176
+ label: string;
177
+ sessions: T[];
178
+ };
179
+
180
+ export type GroupedSessions<T extends SessionListRow = SessionListRow> = {
181
+ /** Running sessions, pinned above every recency group, most-recent first. */
182
+ running: T[];
183
+ /** Non-running sessions bucketed by recency (empty buckets dropped). */
184
+ grouped: SessionRecencyBucket<T>[];
185
+ };
186
+
187
+ /**
188
+ * Order + bucket the sessions for the rail. Running sessions are lifted into a
189
+ * synthetic, always-first position regardless of recency (rendered with a
190
+ * "running" marker); the remainder are bucketed by recency, most-recent first
191
+ * within each bucket. Empty groups are dropped.
192
+ */
193
+ export function groupSessionsForRail<T extends SessionListRow>(
194
+ sessions: T[],
195
+ now: Date = new Date(),
196
+ ): GroupedSessions<T> {
197
+ const running = sessions.filter(isEffectivelyRunning).sort(compareSessionActivity);
198
+ const rest = sessions
199
+ .filter((session) => !isEffectivelyRunning(session))
200
+ .sort(compareSessionActivity);
201
+
202
+ const buckets = new Map<SessionRecencyGroup, T[]>();
203
+ for (const session of rest) {
204
+ const group = recencyGroupFor(sessionActivityTime(session), now);
205
+ const list = buckets.get(group) ?? [];
206
+ list.push(session);
207
+ buckets.set(group, list);
208
+ }
209
+
210
+ const grouped: SessionRecencyBucket<T>[] = [];
211
+ for (const group of SESSION_GROUP_ORDER) {
212
+ const list = buckets.get(group);
213
+ if (list && list.length > 0) {
214
+ grouped.push({
215
+ group,
216
+ label: SESSION_GROUP_LABELS[group],
217
+ sessions: list,
218
+ });
219
+ }
220
+ }
221
+ return { running, grouped };
222
+ }
223
+
224
+ export function relativeTimeLabel(value: string, now: Date = new Date()): string {
225
+ const timestamp = Date.parse(value);
226
+ if (Number.isNaN(timestamp)) {
227
+ return "";
228
+ }
229
+ const diffSeconds = Math.max(0, Math.floor((now.getTime() - timestamp) / 1000));
230
+ if (diffSeconds < 45) {
231
+ return "now";
232
+ }
233
+ const minutes = Math.floor(diffSeconds / 60);
234
+ if (minutes < 60) {
235
+ return `${minutes}m`;
236
+ }
237
+ const hours = Math.floor(minutes / 60);
238
+ if (hours < 24) {
239
+ return `${hours}h`;
240
+ }
241
+ const days = Math.floor(hours / 24);
242
+ if (days < 7) {
243
+ return `${days}d`;
244
+ }
245
+ return new Date(timestamp).toLocaleDateString(undefined, {
246
+ month: "short",
247
+ day: "numeric",
248
+ });
249
+ }
250
+
251
+ /** Codex subscription product ids are prefixed `codex/`. */
252
+ export function isCodexProductModel(modelId: string): boolean {
253
+ return modelId.startsWith("codex/");
254
+ }
255
+
256
+ export type SessionStatusTone = "queued" | "running" | "waiting" | "idle" | "failed" | "cancelled";
257
+
258
+ export const SESSION_STATUS_TONE: Record<SessionStatus, SessionStatusTone> = {
259
+ queued: "queued",
260
+ running: "running",
261
+ recovering: "running",
262
+ waiting_capacity: "waiting",
263
+ requires_action: "waiting",
264
+ idle: "idle",
265
+ failed: "failed",
266
+ cancelled: "cancelled",
267
+ };
268
+
269
+ /** The status dot of a recent-session row: a background command reads as running. */
270
+ export function recentSessionStatus(
271
+ session: Pick<SessionListRow, "status" | "backgroundCommandActivity">,
272
+ ): { tone: SessionStatusTone; pulse: boolean } {
273
+ const hasBackgroundCommand = session.backgroundCommandActivity !== undefined;
274
+ return {
275
+ tone: hasBackgroundCommand ? "running" : SESSION_STATUS_TONE[session.status],
276
+ pulse: hasBackgroundCommand || session.status === "running",
277
+ };
278
+ }
279
+
280
+ /** A short `owner/repo` label from the session's first repository resource. */
281
+ export function sessionRepoLabel(session: Pick<Session, "resources">): string | null {
282
+ const repo = session.resources.find((resource) => resource.kind === "repository");
283
+ if (!repo || repo.kind !== "repository") {
284
+ return null;
285
+ }
286
+ const parts = repo.uri
287
+ .replace(/\.git$/, "")
288
+ .split("/")
289
+ .filter(Boolean);
290
+ return parts.length >= 2 ? parts.slice(-2).join("/") : (parts.at(-1) ?? null);
291
+ }
292
+
293
+ export function recentSessionModelPresentation<TCatalog extends ClientModel>(
294
+ modelId: string,
295
+ catalogRows: readonly PickerModelRow<TCatalog>[],
296
+ ): {
297
+ label: string;
298
+ billingClass: PickerModelRow["billingClass"];
299
+ logoUrl?: string | undefined;
300
+ } {
301
+ const row = findPickerRow([...catalogRows], modelId);
302
+ return {
303
+ label: row?.label ?? modelDisplayName(modelId),
304
+ logoUrl: row?.catalog.logoUrl,
305
+ billingClass:
306
+ row?.billingClass ??
307
+ (isCodexProductModel(modelId) ? "codex_subscription" : "opengeni_credits"),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * The home screen's recent sessions: server pins first, then running, then
313
+ * recency buckets, capped. `sessions` keeps the all-visible-row contract, so
314
+ * its pins are removed before recombining the explicit pinned section.
315
+ */
316
+ export function recentSessionsForHome<T extends SessionListRow>(
317
+ sessions: T[],
318
+ pinned: T[],
319
+ limit = 6,
320
+ now: Date = new Date(),
321
+ ): T[] {
322
+ const ordinary = sessions.filter((session) => !session.pinned);
323
+ const { running, grouped } = groupSessionsForRail(ordinary, now);
324
+ return [...pinned, ...running, ...grouped.flatMap((bucket) => bucket.sessions)].slice(0, limit);
325
+ }
326
+
327
+ export { findPickerRow };
@@ -0,0 +1,62 @@
1
+ import type { SessionStatus } from "@opengeni/sdk";
2
+
3
+ /** Status token family (`--og-color-status-<tone>`) for a session status. */
4
+ export type SessionStatusTone = "queued" | "running" | "waiting" | "idle" | "failed" | "cancelled";
5
+
6
+ /** Renderer-neutral session status presentation: label, color family and liveness. */
7
+ export const SESSION_STATUS_PRESENTATION: Record<
8
+ SessionStatus,
9
+ { label: string; tone: SessionStatusTone; pulse: boolean }
10
+ > = {
11
+ queued: { label: "Queued", tone: "queued", pulse: false },
12
+ running: { label: "Running", tone: "running", pulse: true },
13
+ recovering: { label: "Recovering", tone: "running", pulse: true },
14
+ waiting_capacity: { label: "Waiting for capacity", tone: "waiting", pulse: true },
15
+ idle: { label: "Idle", tone: "idle", pulse: false },
16
+ requires_action: { label: "Waiting on you", tone: "waiting", pulse: true },
17
+ failed: { label: "Failed", tone: "failed", pulse: false },
18
+ cancelled: { label: "Cancelled", tone: "cancelled", pulse: false },
19
+ };
20
+
21
+ /**
22
+ * The status badge's color tokens (text, border and its alpha); the fill is the
23
+ * status color at 10%. Mirrors the web badge classes, for non-DOM renderers.
24
+ */
25
+ export const SESSION_STATUS_BADGE: Record<
26
+ SessionStatus,
27
+ { text: string; border: string; borderAlpha: number; fill: string }
28
+ > = {
29
+ queued: { text: "fg-muted", border: "border", borderAlpha: 1, fill: "status-queued" },
30
+ running: {
31
+ text: "status-running",
32
+ border: "status-running",
33
+ borderAlpha: 0.3,
34
+ fill: "status-running",
35
+ },
36
+ recovering: {
37
+ text: "status-running",
38
+ border: "status-running",
39
+ borderAlpha: 0.3,
40
+ fill: "status-running",
41
+ },
42
+ waiting_capacity: {
43
+ text: "status-waiting",
44
+ border: "status-waiting",
45
+ borderAlpha: 0.35,
46
+ fill: "status-waiting",
47
+ },
48
+ idle: { text: "status-idle", border: "status-idle", borderAlpha: 0.3, fill: "status-idle" },
49
+ requires_action: {
50
+ text: "status-waiting",
51
+ border: "status-waiting",
52
+ borderAlpha: 0.35,
53
+ fill: "status-waiting",
54
+ },
55
+ failed: {
56
+ text: "status-failed",
57
+ border: "status-failed",
58
+ borderAlpha: 0.35,
59
+ fill: "status-failed",
60
+ },
61
+ cancelled: { text: "fg-subtle", border: "border", borderAlpha: 1, fill: "status-cancelled" },
62
+ };