@opengeni/react 1.4.2 → 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 (240) 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-WGJ43YGI.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-MDIXFKA3.js → chunk-6FOTSVWU.js} +303 -109
  12. package/dist/chunk-6FOTSVWU.js.map +1 -0
  13. package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
  14. package/dist/chunk-7RRBX5EM.js.map +1 -0
  15. package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
  16. package/dist/chunk-7ZB7I3KP.js.map +1 -0
  17. package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
  18. package/dist/chunk-ATIRUHBZ.js.map +1 -0
  19. package/dist/chunk-BOFEQC6K.js +162 -0
  20. package/dist/chunk-BOFEQC6K.js.map +1 -0
  21. package/dist/{chunk-VCSERYXH.js → chunk-CESBPQP5.js} +309 -162
  22. package/dist/chunk-CESBPQP5.js.map +1 -0
  23. package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
  24. package/dist/chunk-GHJU7XG7.js.map +1 -0
  25. package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
  26. package/dist/chunk-HNVWMDNN.js.map +1 -0
  27. package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
  28. package/dist/chunk-I5EGRXXG.js.map +1 -0
  29. package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
  30. package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
  31. package/dist/chunk-IX3K4THH.js.map +1 -0
  32. package/dist/chunk-IZV5FTJC.js +55 -0
  33. package/dist/chunk-IZV5FTJC.js.map +1 -0
  34. package/dist/chunk-JDJJMBB3.js +1883 -0
  35. package/dist/chunk-JDJJMBB3.js.map +1 -0
  36. package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
  37. package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
  38. package/dist/chunk-LHF6BVM4.js +83 -0
  39. package/dist/chunk-LHF6BVM4.js.map +1 -0
  40. package/dist/chunk-N7S72EXK.js +25 -0
  41. package/dist/chunk-N7S72EXK.js.map +1 -0
  42. package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
  43. package/dist/chunk-PEHTX3V7.js.map +1 -0
  44. package/dist/chunk-R5R5RQQD.js +51 -0
  45. package/dist/chunk-R5R5RQQD.js.map +1 -0
  46. package/dist/chunk-RI42H5IU.js +326 -0
  47. package/dist/chunk-RI42H5IU.js.map +1 -0
  48. package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
  49. package/dist/chunk-SHLZMAJN.js.map +1 -0
  50. package/dist/{chunk-CREMJMVN.js → chunk-TZQSHCZN.js} +692 -1996
  51. package/dist/chunk-TZQSHCZN.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-46UIN7F3.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-3ZFWWJ6O.js} +6 -4
  76. package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.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-computer-session.d.ts +2 -0
  80. package/dist/hooks/use-goal.d.ts +2 -1
  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-SWC6QEVN.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/machines.js +2 -2
  90. package/dist/model-mark-paths.d.ts +34 -0
  91. package/dist/model-mark-paths.js +16 -0
  92. package/dist/model-picker-model.d.ts +111 -0
  93. package/dist/model-picker-model.js +25 -0
  94. package/dist/model-picker-model.js.map +1 -0
  95. package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
  96. package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
  97. package/dist/model-policy.d.ts +10 -0
  98. package/dist/model-policy.js +5 -1
  99. package/dist/organization-model.d.ts +32 -0
  100. package/dist/organization-model.js +65 -0
  101. package/dist/organization-model.js.map +1 -0
  102. package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
  103. package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
  104. package/dist/queue-presentation-model.d.ts +72 -0
  105. package/dist/queue-presentation-model.js +38 -0
  106. package/dist/queue-presentation-model.js.map +1 -0
  107. package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
  108. package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
  109. package/dist/realtime.js +5 -3
  110. package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
  111. package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
  112. package/dist/sandbox-label-model.d.ts +23 -0
  113. package/dist/sandbox-label-model.js +28 -0
  114. package/dist/sandbox-label-model.js.map +1 -0
  115. package/dist/session-attention-model.d.ts +8 -0
  116. package/dist/session-attention-model.js +46 -0
  117. package/dist/session-attention-model.js.map +1 -0
  118. package/dist/session-feedback-model.d.ts +20 -0
  119. package/dist/session-feedback-model.js +56 -0
  120. package/dist/session-feedback-model.js.map +1 -0
  121. package/dist/session-list-model.d.ts +96 -0
  122. package/dist/session-list-model.js +219 -0
  123. package/dist/session-list-model.js.map +1 -0
  124. package/dist/session-status-model.d.ts +19 -0
  125. package/dist/session-ui.js +26 -17
  126. package/dist/session-ui.js.map +1 -1
  127. package/dist/session.js +8 -6
  128. package/dist/testing/session-scenarios.d.ts +10 -0
  129. package/dist/testing/session-scenarios.js +303 -0
  130. package/dist/testing/session-scenarios.js.map +1 -0
  131. package/dist/timeline/genie-copy.d.ts +3 -0
  132. package/dist/timeline/genie-loading.d.ts +11 -0
  133. package/dist/timeline/index.d.ts +1 -1
  134. package/dist/timeline/notice-presentation.d.ts +7 -0
  135. package/dist/timeline/patch-apply-command.d.ts +18 -0
  136. package/dist/timeline/platform-activity-presentation.d.ts +7 -0
  137. package/dist/timeline/presented-image.d.ts +6 -1
  138. package/dist/timeline/question-nav-model.d.ts +23 -0
  139. package/dist/timeline/registry.d.ts +10 -1
  140. package/dist/timeline/retained-loaders.d.ts +1 -1
  141. package/dist/timeline/tool-presentation.d.ts +172 -0
  142. package/dist/timeline/turn-summary-model.d.ts +90 -0
  143. package/dist/timeline/turn-summary.d.ts +4 -74
  144. package/dist/timeline/work-presentation.d.ts +42 -0
  145. package/dist/timeline-model.d.ts +24 -0
  146. package/dist/timeline-model.js +152 -0
  147. package/dist/timeline-model.js.map +1 -0
  148. package/package.json +47 -3
  149. package/src/artifacts.ts +1 -0
  150. package/src/client.ts +9 -3
  151. package/src/components/browser-viewer.tsx +23 -2
  152. package/src/components/claude-mark.tsx +2 -1
  153. package/src/components/composer.tsx +4 -144
  154. package/src/components/computer-viewer.tsx +139 -30
  155. package/src/components/grok-mark.tsx +2 -1
  156. package/src/components/human-input-form.tsx +13 -124
  157. package/src/components/markdown.tsx +19 -1
  158. package/src/components/message-timeline.tsx +45 -99
  159. package/src/components/model-mark.tsx +2 -1
  160. package/src/components/model-policy-picker-menu.tsx +30 -85
  161. package/src/components/model-policy-picker.tsx +15 -148
  162. package/src/components/opper-mark.tsx +18 -0
  163. package/src/components/provider-recovery-notice.tsx +41 -0
  164. package/src/components/queue-surface-implementation.tsx +20 -349
  165. package/src/components/remark-soft-line-breaks.ts +166 -0
  166. package/src/components/sandbox-workspace.tsx +17 -1
  167. package/src/components/session-chrome.tsx +114 -99
  168. package/src/components/session-conversation.tsx +55 -6
  169. package/src/components/session-list.tsx +1 -1
  170. package/src/components/session-status.tsx +17 -16
  171. package/src/composer-messages.ts +150 -0
  172. package/src/connect-setup.tsx +45 -11
  173. package/src/connect.ts +1 -1
  174. package/src/hooks/shared-machines-list.ts +34 -1
  175. package/src/hooks/use-browser-session.ts +445 -168
  176. package/src/hooks/use-client-config-flags.ts +8 -0
  177. package/src/hooks/use-computer-session.ts +334 -98
  178. package/src/hooks/use-goal.ts +66 -10
  179. package/src/human-input-model.ts +147 -0
  180. package/src/index.ts +16 -0
  181. package/src/lib/clipboard.ts +29 -0
  182. package/src/lib/format.ts +7 -0
  183. package/src/lib/provider-recovery.ts +202 -0
  184. package/src/model-mark-paths.ts +53 -0
  185. package/src/model-picker-model.ts +330 -0
  186. package/src/model-policy.ts +43 -2
  187. package/src/organization-model.ts +118 -0
  188. package/src/queue-presentation-model.ts +464 -0
  189. package/src/sandbox-label-model.ts +41 -0
  190. package/src/session-attention-model.ts +59 -0
  191. package/src/session-feedback-model.ts +95 -0
  192. package/src/session-list-model.ts +327 -0
  193. package/src/session-status-model.ts +62 -0
  194. package/src/testing/session-scenarios.ts +326 -0
  195. package/src/timeline/activity-rail.tsx +5 -17
  196. package/src/timeline/genie-copy.ts +19 -0
  197. package/src/timeline/genie-loading.tsx +13 -18
  198. package/src/timeline/index.ts +1 -0
  199. package/src/timeline/notice-presentation.ts +22 -0
  200. package/src/timeline/patch-apply-command.tsx +135 -0
  201. package/src/timeline/platform-activity-presentation.ts +122 -0
  202. package/src/timeline/platform-activity-row.tsx +6 -96
  203. package/src/timeline/presented-image.ts +16 -2
  204. package/src/timeline/question-nav-model.ts +28 -0
  205. package/src/timeline/registry.ts +11 -1
  206. package/src/timeline/retained-loaders.ts +9 -4
  207. package/src/timeline/rolling-activity.tsx +7 -19
  208. package/src/timeline/tool-presentation.ts +1316 -0
  209. package/src/timeline/tool-renderers.tsx +226 -1025
  210. package/src/timeline/turn-summary-model.ts +338 -0
  211. package/src/timeline/turn-summary.tsx +21 -326
  212. package/src/timeline/work-presentation.ts +139 -0
  213. package/src/timeline-model.ts +108 -0
  214. package/styles/compiled.css +8 -0
  215. package/styles/connect.css +4 -0
  216. package/dist/chunk-CREMJMVN.js.map +0 -1
  217. package/dist/chunk-CVYCLFMH.js.map +0 -1
  218. package/dist/chunk-G6YRCIYJ.js.map +0 -1
  219. package/dist/chunk-HXYOSSQO.js +0 -117
  220. package/dist/chunk-HXYOSSQO.js.map +0 -1
  221. package/dist/chunk-IUHHPMAX.js.map +0 -1
  222. package/dist/chunk-IYQCWGU5.js.map +0 -1
  223. package/dist/chunk-JH7V627O.js.map +0 -1
  224. package/dist/chunk-L2J3PU7A.js.map +0 -1
  225. package/dist/chunk-LVS3MH5T.js.map +0 -1
  226. package/dist/chunk-MDIXFKA3.js.map +0 -1
  227. package/dist/chunk-PHBFJCEU.js.map +0 -1
  228. package/dist/chunk-VCSERYXH.js.map +0 -1
  229. package/dist/chunk-XS67YG5R.js.map +0 -1
  230. package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
  231. package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
  232. /package/dist/{browser-viewer-WGJ43YGI.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
  233. /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
  234. /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
  235. /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
  236. /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
  237. /package/dist/{computer-viewer-46UIN7F3.js.map → composer-messages.js.map} +0 -0
  238. /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
  239. /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
  240. /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The model picker's presentation logic, free of any renderer: which rows show,
3
+ * how they group and filter, and which thinking/latency controls apply. The web
4
+ * menu and native sheets both render from this.
5
+ */
6
+ import type { ClientModel, LatencyMode, ReasoningEffort } from "@opengeni/sdk";
7
+ import {
8
+ coerceReasoningEffortForModel,
9
+ effortOptionsForModel,
10
+ findPickerRow,
11
+ groupPickerRowsByBillingClass,
12
+ payerSummaryForModel,
13
+ projectClientModelRows,
14
+ runnableLatencyModesForModel,
15
+ scopedBillingClassLabel,
16
+ type PickerBillingClass,
17
+ type PickerModelRow,
18
+ } from "./model-policy";
19
+
20
+ type ClientPickerModelRow = PickerModelRow<ClientModel>;
21
+
22
+ export type ModelPolicyPickerMessages = {
23
+ label: string;
24
+ loading: string;
25
+ noModels: string;
26
+ connectTitle: string;
27
+ connectBody: string;
28
+ connectAction: string;
29
+ thinking: string;
30
+ fast: string;
31
+ fastRateHint: string;
32
+ codexOnly: string;
33
+ searchLabel?: string;
34
+ searchPlaceholder?: string;
35
+ currentModel?: string;
36
+ noMatches?: string;
37
+ unsupportedAttachments?: string;
38
+ thinkingEffort?: string;
39
+ selected?: string;
40
+ free?: string;
41
+
42
+ billingHints: Record<PickerBillingClass, string>;
43
+ };
44
+
45
+ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
46
+ label: "Model and effort",
47
+ loading: "Loading model catalog…",
48
+ noModels: "No models available.",
49
+ connectTitle: "Connect a model",
50
+ connectBody: "Use a subscription or a provider key you already have.",
51
+ connectAction: "Open Models",
52
+ thinking: "Thinking",
53
+ fast: "Fast",
54
+ fastRateHint: "2× rate",
55
+ codexOnly: "Codex-only session",
56
+ searchLabel: "Search models or providers",
57
+ searchPlaceholder: "Search models or providers…",
58
+ currentModel: "Current model",
59
+ noMatches: "No matching models. Try a model or provider name.",
60
+ unsupportedAttachments: "This model cannot view the attached images.",
61
+ thinkingEffort: "Thinking effort",
62
+ selected: "Selected",
63
+ free: "Free",
64
+
65
+ billingHints: {
66
+ opengeni_credits: "Provided by Opengeni",
67
+ external: "Provider terms and limits apply",
68
+ codex_subscription: "ChatGPT / Codex plan",
69
+ supergrok_subscription: "SuperGrok / xAI plan",
70
+ claude_subscription: "Claude plan",
71
+ byok: "Billed to the connected provider account",
72
+ organization_byok: "Billed to the connected provider account",
73
+ },
74
+ };
75
+
76
+ /** Host branding for one payment group; the icon type belongs to the renderer. */
77
+ export type ModelPickerGroupBranding<TIcon = unknown> = {
78
+ label?: string | undefined;
79
+ /** Omit to preserve the default; null hides the supporting text. */
80
+ description?: string | null | undefined;
81
+ /** Decorative, non-interactive content. Omit for the default; null hides it. */
82
+ icon?: TIcon | undefined;
83
+ };
84
+
85
+ export type ModelPickerGroupPresentation<TIcon = unknown> = Partial<
86
+ Record<PickerBillingClass, ModelPickerGroupBranding<TIcon>>
87
+ >;
88
+
89
+ /** The renderer-neutral inputs every picker surface shares. */
90
+ export type ModelPickerInput<TIcon = unknown> = {
91
+ groupPresentation?: ModelPickerGroupPresentation<TIcon> | undefined;
92
+ models?: ClientModel[] | undefined;
93
+ rows?: PickerModelRow[] | undefined;
94
+ hasImageAttachments?: boolean | undefined;
95
+ model: string;
96
+ effort: ReasoningEffort;
97
+ latencyMode: LatencyMode;
98
+ codexOnly?: boolean | undefined;
99
+ collapseScopes?: boolean | undefined;
100
+ allowLatencyMode?: boolean | undefined;
101
+ messages?: Partial<ModelPolicyPickerMessages> | undefined;
102
+ };
103
+
104
+ /** Models settings keeps naming who is billed for a key. */
105
+ export const SCOPED_BILLING_HINTS: Partial<Record<PickerBillingClass, string>> = {
106
+ byok: "Billed to the workspace provider account",
107
+ organization_byok: "Billed to the organization provider account",
108
+ };
109
+
110
+ export function isCodexPickerModel(model: ClientModel): boolean {
111
+ return model.id.startsWith("codex/") || model.source === "codex";
112
+ }
113
+
114
+ /** The payment group a selection the catalog no longer lists most likely belonged to. */
115
+ export function billingClassForMissingSelection(modelId: string): PickerBillingClass {
116
+ if (modelId.startsWith("workspace-claude-subscription/")) return "claude_subscription";
117
+ if (modelId.startsWith("workspace-anthropic/")) return "byok";
118
+ if (modelId.startsWith("organization-claude-subscription/")) return "claude_subscription";
119
+ if (modelId.startsWith("organization-anthropic/")) return "organization_byok";
120
+ if (modelId.startsWith("workspace-gateway/")) return "byok";
121
+ if (modelId.startsWith("workspace-openrouter/")) return "byok";
122
+ if (modelId.startsWith("workspace-opper/")) return "byok";
123
+ // A deployment OpenRouter or Opper ID does not encode its workspace-facing cost.
124
+ // Missing rows therefore use the credits-safe rail instead of falsely
125
+ // claiming that an unknown former selection was externally funded.
126
+ if (modelId.startsWith("openrouter/")) return "opengeni_credits";
127
+ if (modelId.startsWith("opper/")) return "opengeni_credits";
128
+ if (modelId.startsWith("codex/")) return "codex_subscription";
129
+ if (modelId.startsWith("supergrok/")) return "supergrok_subscription";
130
+ return "opengeni_credits";
131
+ }
132
+
133
+ function applyCodexOnly(
134
+ rows: ClientPickerModelRow[],
135
+ codexOnly: boolean,
136
+ unavailableReason: string,
137
+ ): ClientPickerModelRow[] {
138
+ if (!codexOnly) return rows;
139
+ return rows.map((row) =>
140
+ isCodexPickerModel(row.catalog)
141
+ ? row
142
+ : {
143
+ ...row,
144
+ selectable: false,
145
+ unavailableReason: row.unavailableReason ?? unavailableReason,
146
+ },
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Host branding for a payment group. While scopes are collapsed, API keys are
152
+ * one group, so either API-key presentation serves both.
153
+ */
154
+ export function groupPresentationFor<TIcon>(
155
+ props: {
156
+ groupPresentation?: ModelPickerGroupPresentation<TIcon> | undefined;
157
+ collapseScopes?: boolean | undefined;
158
+ },
159
+ billingClass: PickerBillingClass,
160
+ ): ModelPickerGroupBranding<TIcon> | undefined {
161
+ const own = props.groupPresentation?.[billingClass];
162
+ if (own !== undefined || props.collapseScopes === false) return own;
163
+ if (billingClass === "byok") return props.groupPresentation?.organization_byok;
164
+ if (billingClass === "organization_byok") return props.groupPresentation?.byok;
165
+ return undefined;
166
+ }
167
+
168
+ export function effectiveRows<TIcon>(props: ModelPickerInput<TIcon>): ClientPickerModelRow[] {
169
+ const rows = props.rows !== undefined ? props.rows : projectClientModelRows(props.models ?? []);
170
+ const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
171
+ return applyCodexOnly(rows, props.codexOnly === true, messages.codexOnly).map((row) => {
172
+ const scoped =
173
+ props.collapseScopes === false
174
+ ? {
175
+ ...row,
176
+ label: row.catalog.label,
177
+ billingClassLabel: scopedBillingClassLabel(row.billingClass),
178
+ }
179
+ : row;
180
+ if (props.collapseScopes === false) {
181
+ if (row.catalog.shortLabel) scoped.shortLabel = row.catalog.shortLabel;
182
+ else delete scoped.shortLabel;
183
+ }
184
+ const label = groupPresentationFor(props, row.billingClass)?.label;
185
+ return label === undefined ? scoped : { ...scoped, billingClassLabel: label };
186
+ });
187
+ }
188
+
189
+ export type ModelPickerMenuGroup = {
190
+ billingClass: PickerBillingClass;
191
+ label: string;
192
+ description: string | null;
193
+ rows: ClientPickerModelRow[];
194
+ };
195
+
196
+ export type ModelPickerThinking = {
197
+ efforts: ReasoningEffort[];
198
+ value: ReasoningEffort;
199
+ showThinking: boolean;
200
+ showFast: boolean;
201
+ /** The selected row cannot run, so its controls are read-only. */
202
+ disabled: boolean;
203
+ };
204
+
205
+ export type ModelPickerMenuView = {
206
+ messages: ModelPolicyPickerMessages;
207
+ rows: ClientPickerModelRow[];
208
+ selected: ClientPickerModelRow | null;
209
+ /** Any row can run; otherwise the surface offers to connect models instead. */
210
+ anySelectable: boolean;
211
+ /** Whether the query is non-empty (decides "no matches" versus "no models"). */
212
+ searching: boolean;
213
+ groups: ModelPickerMenuGroup[];
214
+ matchCount: number;
215
+ /** Thinking and Fast controls for the selected model, or null when it has none. */
216
+ thinking: ModelPickerThinking | null;
217
+ /** The selected model cannot view images the draft carries. */
218
+ unsupportedAttachments: boolean;
219
+ };
220
+
221
+ /** Everything a picker menu shows for this input and search query. */
222
+ export function modelPickerMenuView<TIcon>(
223
+ props: ModelPickerInput<TIcon>,
224
+ query: string,
225
+ ): ModelPickerMenuView {
226
+ const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
227
+ const rows = effectiveRows(props);
228
+ const selected = findPickerRow(rows, props.model);
229
+ const words = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
230
+ const filtered = rows.filter((row) => {
231
+ const description = groupPresentationFor(props, row.billingClass)?.description;
232
+ const text =
233
+ `${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${description === undefined ? payerSummaryForModel(row.catalog) : (description ?? "")}`.toLowerCase();
234
+ return words.every((word) => text.includes(word));
235
+ });
236
+ const matchingIds = new Set(filtered.map((row) => row.id));
237
+ const groups = groupPickerRowsByBillingClass(rows, {
238
+ codexOnly: props.codexOnly === true,
239
+ selectedId: props.model,
240
+ collapseScopes: props.collapseScopes !== false,
241
+ })
242
+ .map((group) => {
243
+ const presentation = groupPresentationFor(props, group.billingClass);
244
+ const override = presentation?.description;
245
+ const defaultHint =
246
+ props.collapseScopes === false &&
247
+ messages.billingHints[group.billingClass] ===
248
+ defaultModelPolicyPickerMessages.billingHints[group.billingClass]
249
+ ? (SCOPED_BILLING_HINTS[group.billingClass] ?? messages.billingHints[group.billingClass])
250
+ : messages.billingHints[group.billingClass];
251
+ return {
252
+ billingClass: group.billingClass,
253
+ label: presentation?.label ?? group.label,
254
+ rows: group.rows.filter((row) => matchingIds.has(row.id)),
255
+ description:
256
+ override === undefined
257
+ ? group.billingClass === "opengeni_credits"
258
+ ? null
259
+ : defaultHint
260
+ : override,
261
+ };
262
+ })
263
+ .filter((group) => group.rows.length > 0);
264
+ const anySelectable = rows.some((row) => row.selectable);
265
+ const unsupportedAttachments =
266
+ props.hasImageAttachments === true &&
267
+ selected?.catalog.capabilities?.inputModalities.includes("image") === false;
268
+ return {
269
+ messages,
270
+ rows,
271
+ selected,
272
+ anySelectable,
273
+ searching: words.length > 0,
274
+ groups,
275
+ matchCount: filtered.length,
276
+ thinking: selected ? modelPickerThinking(props, selected) : null,
277
+ unsupportedAttachments,
278
+ };
279
+ }
280
+
281
+ function modelPickerThinking(
282
+ props: Pick<ModelPickerInput, "effort" | "allowLatencyMode">,
283
+ selected: ClientPickerModelRow,
284
+ ): ModelPickerThinking | null {
285
+ const efforts = effortOptionsForModel(selected.catalog);
286
+ const showThinking =
287
+ efforts.length > 1 && selected.catalog.capabilities?.reasoning.runnable !== false;
288
+ const showFast =
289
+ props.allowLatencyMode !== false &&
290
+ runnableLatencyModesForModel(selected.catalog).includes("fast");
291
+ if (!showThinking && !showFast) return null;
292
+ return {
293
+ efforts,
294
+ value: coerceReasoningEffortForModel(selected.catalog, props.effort),
295
+ showThinking,
296
+ showFast,
297
+ disabled: !selected.selectable,
298
+ };
299
+ }
300
+
301
+ export type ModelPickerChoice = {
302
+ /** The new model, or null when the row is already selected. */
303
+ model: string | null;
304
+ /** Always reported, so hosts can commit model and effort together. */
305
+ effort: ReasoningEffort;
306
+ /** Fast falls back to standard when the new model cannot run it. */
307
+ latencyMode: LatencyMode | null;
308
+ };
309
+
310
+ /** What choosing a row changes, or null when the row cannot be chosen. */
311
+ export function modelPickerChoice(
312
+ props: Pick<ModelPickerInput, "model" | "effort" | "latencyMode">,
313
+ row: ClientPickerModelRow,
314
+ ): ModelPickerChoice | null {
315
+ if (!row.selectable) return null;
316
+ return {
317
+ model: row.id === props.model ? null : row.id,
318
+ effort: coerceReasoningEffortForModel(row.catalog, props.effort),
319
+ latencyMode:
320
+ props.latencyMode !== "standard" &&
321
+ !runnableLatencyModesForModel(row.catalog).includes(props.latencyMode)
322
+ ? "standard"
323
+ : null,
324
+ };
325
+ }
326
+
327
+ /** The web segmented control's short labels ("X-high" fits a phone row). */
328
+ export function compactEffortLabel(effort: ReasoningEffort, label: string): string {
329
+ return effort === "xhigh" ? "X-high" : label;
330
+ }
@@ -55,7 +55,11 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
55
55
  };
56
56
 
57
57
  export function billingClassForModel(model: ClientModel): PickerBillingClass {
58
- if (model.provider === "organization-gateway" || model.provider === "organization-openrouter") {
58
+ if (
59
+ model.provider === "organization-gateway" ||
60
+ model.provider === "organization-openrouter" ||
61
+ model.provider === "organization-opper"
62
+ ) {
59
63
  return "organization_byok";
60
64
  }
61
65
  if (
@@ -63,7 +67,11 @@ export function billingClassForModel(model: ClientModel): PickerBillingClass {
63
67
  model.provider?.startsWith("workspace-azure-openai-")
64
68
  )
65
69
  return "byok";
66
- if (model.provider === "workspace-gateway" || model.provider === "workspace-openrouter") {
70
+ if (
71
+ model.provider === "workspace-gateway" ||
72
+ model.provider === "workspace-openrouter" ||
73
+ model.provider === "workspace-opper"
74
+ ) {
67
75
  return "byok";
68
76
  }
69
77
  return modelPickerBillingClassFor(model);
@@ -116,6 +124,30 @@ export function coerceReasoningEffortForModel(
116
124
  return defaultEffortForModel(model);
117
125
  }
118
126
 
127
+ /** Whether the composer offers (and therefore labels) a reasoning-effort choice. */
128
+ export function modelOffersEffortChoice(model: ClientModel): boolean {
129
+ return (
130
+ effortOptionsForModel(model).length > 1 && model.capabilities?.reasoning.runnable !== false
131
+ );
132
+ }
133
+
134
+ /**
135
+ * The composer model pill at phone width: the catalog's compact name (falling
136
+ * back to the display name) and the effort label when effort is a choice.
137
+ */
138
+ export function compactModelPill(
139
+ models: readonly ClientModel[],
140
+ modelId: string,
141
+ effort: ReasoningEffort | null | undefined,
142
+ ): { name: string; effort: string | null } {
143
+ const row = findPickerRow(projectClientModelRows([...models]), modelId);
144
+ return {
145
+ name: row?.shortLabel ?? row?.label ?? modelDisplayName(modelId),
146
+ effort:
147
+ row && effort && modelOffersEffortChoice(row.catalog) ? labelReasoningEffort(effort) : null,
148
+ };
149
+ }
150
+
119
151
  export function runnableLatencyModesForModel(model: ClientModel): LatencyModeId[] {
120
152
  const modes = model.capabilities?.latencyModes ?? [];
121
153
  return modes.filter((mode) => mode.runnable).map((mode) => mode.id);
@@ -212,6 +244,9 @@ export function advancedSourceSummary(model: ClientModel): string | null {
212
244
  if (model.provider === "workspace-openrouter") {
213
245
  return "Workspace OpenRouter connection";
214
246
  }
247
+ if (model.provider === "workspace-opper") {
248
+ return "Workspace Opper connection";
249
+ }
215
250
  if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
216
251
  return "Workspace Vercel AI Gateway";
217
252
  }
@@ -233,6 +268,9 @@ function workspaceProviderPayerSummary(model: ClientModel): string {
233
268
  if (model.provider === "workspace-openrouter") {
234
269
  return "Billed to the workspace OpenRouter account";
235
270
  }
271
+ if (model.provider === "workspace-opper") {
272
+ return "Billed to the workspace Opper account";
273
+ }
236
274
  if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
237
275
  return "Billed to the workspace Vercel account";
238
276
  }
@@ -247,6 +285,9 @@ function organizationProviderPayerSummary(model: ClientModel): string {
247
285
  if (model.provider === "organization-openrouter") {
248
286
  return "Billed to the organization OpenRouter account";
249
287
  }
288
+ if (model.provider === "organization-opper") {
289
+ return "Billed to the organization Opper account";
290
+ }
250
291
  if (model.provider === "organization-gateway") {
251
292
  return "Billed to the organization Vercel account";
252
293
  }
@@ -0,0 +1,118 @@
1
+ // Pure organization and workspace selection rules shared by the web rail's
2
+ // switcher and native renderers. The wire model has no account *name*, so a
3
+ // display label is derived from the access grant — preferring the account
4
+ // name, falling back to a short, stable id fragment. No DOM, no React.
5
+ import type { AccessContext, AccountGrant, Workspace } from "@opengeni/sdk";
6
+
7
+ export type OrgOption = {
8
+ accountId: string;
9
+ label: string;
10
+ /** Whether the subject has the owner/admin authority required by organization settings. */
11
+ canManage: boolean;
12
+ };
13
+
14
+ /** Organizations the subject administers (owner or admin), sorted by id. */
15
+ export function organizationAdministrationAccountIds(accessContext: AccessContext): string[] {
16
+ return accessContext.accountGrants
17
+ .filter(
18
+ (grant) =>
19
+ grant.subjectId === accessContext.subjectId &&
20
+ (grant.role === "owner" || grant.role === "admin"),
21
+ )
22
+ .map((grant) => grant.accountId)
23
+ .sort();
24
+ }
25
+
26
+ /** A short, stable id fragment for an account that has no human name. */
27
+ export function shortAccountId(accountId: string): string {
28
+ return accountId.length > 8 ? accountId.slice(0, 8) : accountId;
29
+ }
30
+
31
+ /** The organization's real name, or null when the API sent none. */
32
+ export function orgName(accountId: string, grants: AccountGrant[]): string | null {
33
+ const grant = grants.find((candidate) => candidate.accountId === accountId);
34
+ const label =
35
+ grant?.metadata && typeof grant.metadata.accountName === "string"
36
+ ? grant.metadata.accountName
37
+ : undefined;
38
+ return label?.trim() || null;
39
+ }
40
+
41
+ /** Human label for an organization: the grant's subjectLabel, else a short id. */
42
+ export function orgLabel(accountId: string, grants: AccountGrant[]): string {
43
+ return orgName(accountId, grants) ?? `Org ${shortAccountId(accountId)}`;
44
+ }
45
+
46
+ /**
47
+ * The organizations the subject belongs to, in a stable order (default org
48
+ * first, then by name). Derived from account grants, unioned with the accounts
49
+ * that own the accessible workspaces so an org always shows even without an
50
+ * explicit grant.
51
+ */
52
+ export function organizationsForSubject(
53
+ context: AccessContext,
54
+ workspaces: Workspace[],
55
+ ): OrgOption[] {
56
+ const administeredIds = new Set(organizationAdministrationAccountIds(context));
57
+ const ids = new Set<string>();
58
+ for (const grant of context.accountGrants) {
59
+ ids.add(grant.accountId);
60
+ }
61
+ for (const workspace of workspaces) {
62
+ ids.add(workspace.accountId);
63
+ }
64
+ const options = [...ids].map((accountId) => ({
65
+ accountId,
66
+ label: orgLabel(accountId, context.accountGrants),
67
+ canManage: administeredIds.has(accountId),
68
+ }));
69
+ return options.sort((a, b) => {
70
+ if (a.accountId === context.defaultAccountId) {
71
+ return -1;
72
+ }
73
+ if (b.accountId === context.defaultAccountId) {
74
+ return 1;
75
+ }
76
+ return a.label.localeCompare(b.label) || a.accountId.localeCompare(b.accountId);
77
+ });
78
+ }
79
+
80
+ /** Workspaces that belong to a given organization, ordered by name. */
81
+ export function workspacesInOrg(workspaces: Workspace[], accountId: string): Workspace[] {
82
+ return workspaces
83
+ .filter((workspace) => workspace.accountId === accountId)
84
+ .sort((a, b) => a.name.localeCompare(b.name));
85
+ }
86
+
87
+ /** Organization settings remain anchored to an accessible workspace in that organization. */
88
+ export function organizationSettingsWorkspaceId(
89
+ workspaces: Workspace[],
90
+ accountId: string,
91
+ activeWorkspaceId: string,
92
+ ): string | null {
93
+ const candidates = workspacesInOrg(workspaces, accountId);
94
+ return (
95
+ candidates.find((workspace) => workspace.id === activeWorkspaceId)?.id ??
96
+ candidates[0]?.id ??
97
+ null
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Where switching to an organization lands: the workspace last used there
103
+ * while it is still open to the person, else its first shared workspace, else
104
+ * their Personal workspace there. Null when nothing in it is open to them.
105
+ */
106
+ export function organizationLandingWorkspaceId(
107
+ workspaces: Workspace[],
108
+ accountId: string,
109
+ rememberedWorkspaceId?: string | null,
110
+ ): string | null {
111
+ const candidates = workspacesInOrg(workspaces, accountId);
112
+ return (
113
+ candidates.find((workspace) => workspace.id === rememberedWorkspaceId)?.id ??
114
+ candidates.find((workspace) => workspace.kind !== "personal")?.id ??
115
+ candidates[0]?.id ??
116
+ null
117
+ );
118
+ }