@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
@@ -1,5 +1,4 @@
1
1
  import type {
2
- HumanInputAnswer,
3
2
  HumanInputQuestion,
4
3
  SkillRecord,
5
4
  SessionHumanInputRequest,
@@ -9,64 +8,24 @@ import { ChevronDownIcon, ChevronUpIcon, MessageCircleQuestionIcon } from "lucid
9
8
  import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
10
9
  import { cn } from "../lib/cn";
11
10
  import { useErrorMessage } from "../lib/error-message";
11
+ import {
12
+ answersFromDrafts,
13
+ defaultHumanInputFormMessages,
14
+ emptyDraft,
15
+ initialDrafts,
16
+ type HumanInputAnswerDraft,
17
+ type HumanInputFormMessages,
18
+ } from "../human-input-model";
12
19
 
13
- export type HumanInputAnswerDraft = {
14
- values: string[];
15
- other: string;
16
- otherSelected: boolean;
20
+ export {
21
+ answersFromDrafts,
22
+ defaultHumanInputFormMessages,
23
+ type HumanInputAnswerDraft,
24
+ type HumanInputFormMessages,
17
25
  };
18
26
 
19
27
  class SkillReviewVerificationError extends Error {}
20
28
 
21
- export type HumanInputFormMessages = {
22
- title: string;
23
- description: string;
24
- submit: string;
25
- skip: string;
26
- submitting: string;
27
- other: string;
28
- deadlineLabel: string;
29
- formatDeadline: (value: string) => string;
30
- required: string;
31
- otherRequired: string;
32
- minSelections: (count: number) => string;
33
- maxSelections: (count: number) => string;
34
- optional: string;
35
- /** Shown when the question list overflows the card and more content is below. */
36
- moreBelow: string;
37
- questionCount: (count: number) => string;
38
- collapse: string;
39
- expand: string;
40
- selectionHint: (min: number | null | undefined, max: number | null | undefined) => string | null;
41
- };
42
-
43
- export const defaultHumanInputFormMessages: HumanInputFormMessages = {
44
- title: "Input required",
45
- /** Multi-question chrome has no default subtitle; hosts may still override. */
46
- description: "",
47
- submit: "Send answers",
48
- skip: "Skip",
49
- submitting: "Submitting…",
50
- other: "Other",
51
- deadlineLabel: "Expires",
52
- formatDeadline,
53
- required: "This question is required.",
54
- otherRequired: "Enter a value for Other.",
55
- minSelections: (count) => `Choose at least ${count} option${count === 1 ? "" : "s"}.`,
56
- maxSelections: (count) => `Choose no more than ${count} option${count === 1 ? "" : "s"}.`,
57
- optional: "Optional",
58
- moreBelow: "More below",
59
- questionCount: (count) => `${count} questions`,
60
- collapse: "Collapse",
61
- expand: "Expand",
62
- selectionHint: (min, max) => {
63
- if (min != null && max != null) return `Choose ${min}–${max}.`;
64
- if (min != null) return `Choose at least ${min}.`;
65
- if (max != null) return `Choose up to ${max}.`;
66
- return null;
67
- },
68
- };
69
-
70
29
  export type HumanInputFormProps = {
71
30
  /** Scoped immutable revision reader. Saving a Skill fails closed without its full preview. */
72
31
  loadSkillReview?:
@@ -1006,73 +965,3 @@ function QuestionControls({
1006
965
  </>
1007
966
  );
1008
967
  }
1009
-
1010
- export function answersFromDrafts(
1011
- questions: HumanInputQuestion[],
1012
- drafts: Record<string, HumanInputAnswerDraft>,
1013
- messageOverrides: Partial<HumanInputFormMessages> = {},
1014
- ): { answers: HumanInputAnswer[]; errors: Record<string, string> } {
1015
- const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
1016
- const answers: HumanInputAnswer[] = [];
1017
- const errors: Record<string, string> = {};
1018
- for (const question of questions) {
1019
- const draft = drafts[question.id] ?? emptyDraft();
1020
- const values = question.kind === "text" ? draft.values.filter(Boolean) : draft.values;
1021
- const other = draft.otherSelected ? draft.other : "";
1022
- const hasOther = Boolean(other.trim());
1023
- const supplied = values.length + (hasOther ? 1 : 0);
1024
-
1025
- // Other-selected-but-empty must win over generic "required" — otherwise the
1026
- // user sees the wrong diagnosis next to a clearly selected control.
1027
- if (question.kind !== "text" && draft.otherSelected && !hasOther) {
1028
- errors[question.id] = messages.otherRequired;
1029
- continue;
1030
- }
1031
-
1032
- if (question.required && supplied === 0) {
1033
- errors[question.id] = messages.required;
1034
- continue;
1035
- }
1036
- if (question.kind !== "text") {
1037
- const min = question.validation?.minSelections;
1038
- const max = question.kind === "single_select" ? 1 : question.validation?.maxSelections;
1039
- if (min != null && supplied < min) {
1040
- errors[question.id] = messages.minSelections(min);
1041
- continue;
1042
- }
1043
- if (max != null && supplied > max) {
1044
- errors[question.id] = messages.maxSelections(max);
1045
- continue;
1046
- }
1047
- }
1048
- if (supplied > 0) {
1049
- answers.push({
1050
- questionId: question.id,
1051
- values,
1052
- ...(hasOther ? { other } : {}),
1053
- });
1054
- }
1055
- }
1056
- return { answers, errors };
1057
- }
1058
-
1059
- function initialDrafts(questions: HumanInputQuestion[]): Record<string, HumanInputAnswerDraft> {
1060
- return Object.fromEntries(questions.map((question) => [question.id, emptyDraft()]));
1061
- }
1062
-
1063
- function emptyDraft(): HumanInputAnswerDraft {
1064
- return { values: [], other: "", otherSelected: false };
1065
- }
1066
-
1067
- function formatDeadline(value: string): string {
1068
- const date = new Date(value);
1069
- if (Number.isNaN(date.getTime())) return value;
1070
- const ms = date.getTime() - Date.now();
1071
- if (ms <= 0) return "deadline passed";
1072
- const minutes = Math.round(ms / 60_000);
1073
- if (minutes < 1) return "in under a minute";
1074
- if (minutes < 60) return `in ${minutes}m`;
1075
- const hours = Math.round(minutes / 60);
1076
- if (hours < 48) return `in ${hours}h`;
1077
- return date.toLocaleString();
1078
- }
@@ -28,6 +28,7 @@ import { MOTION_INSPECT_SCALE } from "../lib/motion-inspect";
28
28
  import { CopyButton } from "./copy-button";
29
29
  import { PreviewLoading } from "./preview-loading";
30
30
  import type { observeMarkdownTableLayout } from "./markdown-table-layout";
31
+ import { remarkSoftLineBreaks } from "./remark-soft-line-breaks";
31
32
  import { softenStreamingMarkdown } from "./soften-streaming-markdown";
32
33
  import { createStreamReveal, rehypeStreamReveal, type StreamReveal } from "./stream-reveal";
33
34
  import { TooltipProvider } from "./tooltip";
@@ -81,6 +82,14 @@ export type MarkdownProps = {
81
82
  renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
82
83
  children: string;
83
84
  className?: string | undefined;
85
+ /**
86
+ * Render a single newline inside prose as a line break (`<br>`), the way chat
87
+ * apps show messages; CommonMark otherwise folds it into a space. Chat message
88
+ * bodies enable this. Leave it off for documents and other authored Markdown.
89
+ * Code (fenced and inline), lists, tables, and blank-line paragraphs are
90
+ * unaffected either way.
91
+ */
92
+ softLineBreaks?: boolean | undefined;
84
93
  /**
85
94
  * While true, newly arrived source fades in through tip ink (`.og-stream-ink`):
86
95
  * an age window over each append batch — fast streams keep a large soft band,
@@ -630,10 +639,19 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
630
639
  );
631
640
  }
632
641
 
642
+ type RemarkPlugins = NonNullable<Parameters<typeof ReactMarkdown>[0]["remarkPlugins"]>;
643
+ const REMARK_PLUGINS: RemarkPlugins = [remarkGfm];
644
+ // Structural plugin types (see remark-soft-line-breaks.ts) need one cast at the seam.
645
+ const SOFT_LINE_BREAK_REMARK_PLUGINS = [
646
+ remarkGfm,
647
+ remarkSoftLineBreaks,
648
+ ] as unknown as RemarkPlugins;
649
+
633
650
  function MarkdownImpl({
634
651
  children,
635
652
  artifactHref,
636
653
  className,
654
+ softLineBreaks = false,
637
655
  streaming = false,
638
656
  onSandboxFile,
639
657
  renderInteractiveBlock,
@@ -760,7 +778,7 @@ function MarkdownImpl({
760
778
  )}
761
779
  >
762
780
  <ReactMarkdown
763
- remarkPlugins={[remarkGfm]}
781
+ remarkPlugins={softLineBreaks ? SOFT_LINE_BREAK_REMARK_PLUGINS : REMARK_PLUGINS}
764
782
  rehypePlugins={rehypePlugins}
765
783
  components={markdownComponents}
766
784
  urlTransform={markdownUrlTransform}
@@ -1,4 +1,15 @@
1
+ import { QUESTION_NAV_MARGIN_PX, questionNavTarget } from "../timeline/question-nav-model";
1
2
  import { RollingActivity } from "../timeline/rolling-activity";
3
+ import {
4
+ clusterIsSettled,
5
+ compactedLandmarkCount,
6
+ durationBetween,
7
+ flattenActivityItems,
8
+ isPreparingWork,
9
+ readableWorkDefaultOpen,
10
+ readableWorkShowsPreview,
11
+ readableWorkStatus,
12
+ } from "../timeline/work-presentation";
2
13
  import { useErrorMessage } from "../lib/error-message";
3
14
  import { formatElapsed } from "../timeline/turn-summary";
4
15
  import { ActivityNoteTextContext } from "../timeline/activity-rail";
@@ -143,6 +154,11 @@ import {
143
154
  readableMachineInputSource,
144
155
  } from "./machine-input-display";
145
156
  import { SESSION_STATUS_META, StatusDot } from "./session-status";
157
+ import {
158
+ noticeDisplayText,
159
+ noticeIsResolvedApproval,
160
+ noticeTone,
161
+ } from "../timeline/notice-presentation";
146
162
  import { TimelineComputeLabelProvider } from "../timeline/compute-label";
147
163
  import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
148
164
  import {
@@ -338,8 +354,6 @@ const PIN_THRESHOLD_PX = 48;
338
354
  * line-sized streaming movement.
339
355
  */
340
356
  const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
341
- /** Breathing room above the question when following stops at an answer. */
342
- const QUESTION_NAV_MARGIN_PX = 12;
343
357
  /**
344
358
  * Floating timeline navigation: two identical small round arrow buttons at fixed
345
359
  * spots, centered on the conversation. Back to your message floats 12px below
@@ -505,25 +519,16 @@ function contentTopOf(node: HTMLElement, groupKey: string): number | null {
505
519
  */
506
520
  type QuestionNav = { key: string; belowHeader: boolean };
507
521
 
508
- /** How far a question's start must be above the viewport before navigation shows. */
509
- const QUESTION_NAV_HIDDEN_PX = 24;
510
-
511
522
  function readQuestionNav(node: HTMLElement): QuestionNav | null {
512
523
  const view = node.getBoundingClientRect();
513
- const prompts = [...node.querySelectorAll<HTMLElement>("[data-og-prompt]")];
514
- // The last prompt before the reading position owns the response/work in view.
515
- // A newer prompt farther down the conversation is not a navigation target.
516
- const middle = view.top + view.height / 2;
517
- const prompt = prompts
518
- .reverse()
519
- .find((candidate) => candidate.getBoundingClientRect().top <= middle);
520
- const key = prompt?.dataset.ogGroupKey;
521
- if (!prompt || !key) return null;
522
- const bounds = prompt.getBoundingClientRect();
523
- if (bounds.top >= view.top - QUESTION_NAV_HIDDEN_PX || bounds.bottom > view.top) {
524
- return null;
525
- }
526
- return { key, belowHeader: workHeaderPinned(node, view) };
524
+ const prompts = [...node.querySelectorAll<HTMLElement>("[data-og-prompt]")].flatMap((prompt) => {
525
+ const key = prompt.dataset.ogGroupKey;
526
+ if (!key) return [];
527
+ const bounds = prompt.getBoundingClientRect();
528
+ return [{ key, top: bounds.top, bottom: bounds.bottom }];
529
+ });
530
+ const key = questionNavTarget(prompts, { top: view.top, height: view.height });
531
+ return key ? { key, belowHeader: workHeaderPinned(node, view) } : null;
527
532
  }
528
533
 
529
534
  /** True while an expanded outer work header is stuck to the scrollport's top edge. */
@@ -633,6 +638,7 @@ export function MessageTimeline({
633
638
  const body = (
634
639
  <Markdown
635
640
  searchTarget={searchItem?.id === item.id ? searchTarget : null}
641
+ softLineBreaks
636
642
  streaming={item.kind === "agent-message" && item.streaming}
637
643
  renderInteractiveBlock={
638
644
  item.kind === "agent-message" ? renderInteractiveBlock : undefined
@@ -3401,18 +3407,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
3401
3407
  switch (group.kind) {
3402
3408
  case "activity":
3403
3409
  if (group.work) {
3404
- const phases = group.items.filter((item) => item.kind === "startup-phase");
3405
- const preparing =
3406
- !startupDetails &&
3407
- !startupDismissed &&
3408
- !group.work.endedAt &&
3409
- !group.work.waiting &&
3410
- phases.length > 0 &&
3411
- group.items.every(
3412
- (item) =>
3413
- item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()),
3414
- ) &&
3415
- !phases.some((item) => item.status === "failed" || item.status === "cancelled");
3410
+ const preparing = isPreparingWork(group, { startupDetails, startupDismissed });
3416
3411
  if (preparing) {
3417
3412
  // Preparation is primary, never hidden in an activity disclosure.
3418
3413
  // Keep work.startedAt unchanged for the handoff and settled duration.
@@ -3429,22 +3424,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
3429
3424
  />
3430
3425
  );
3431
3426
  }
3432
- const end = group.work.endedAt;
3433
- const status: TurnSummaryStatus = end
3434
- ? { kind: "worked", durationMs: durationBetween(group.work.startedAt, end) }
3435
- : group.work.waiting
3436
- ? { kind: "waiting", ...group.work.waiting }
3437
- : {
3438
- kind: "working",
3439
- since: group.work.startedAt,
3440
- preview: containsPresentedImage ? undefined : (
3427
+ const workStatus = readableWorkStatus({ ...group, work: group.work });
3428
+ const status: TurnSummaryStatus =
3429
+ workStatus.kind === "working"
3430
+ ? {
3431
+ ...workStatus,
3432
+ preview: readableWorkShowsPreview(group) ? (
3441
3433
  <RollingActivity
3442
3434
  items={group.items}
3443
3435
  toolRegistry={toolRegistry}
3444
3436
  showCount={false}
3445
3437
  />
3446
- ),
3447
- };
3438
+ ) : undefined,
3439
+ }
3440
+ : workStatus;
3448
3441
  return (
3449
3442
  // Marks this work row for the live-note fold (list keys can be carried
3450
3443
  // over from an earlier row, so they are not the work id).
@@ -3456,13 +3449,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
3456
3449
  outcome={group.outcome}
3457
3450
  failureText={group.failureText}
3458
3451
  foldKey={group.id}
3459
- defaultOpen={
3460
- containsPresentedImage ||
3461
- group.outcome === "failed" ||
3462
- phases.some((item) => item.status === "failed" || item.status === "cancelled")
3463
- ? true
3464
- : undefined
3465
- }
3452
+ defaultOpen={readableWorkDefaultOpen(group)}
3466
3453
  facets={turnSummary?.facets}
3467
3454
  contextCompactionCount={compactedLandmarkCount(group.work.details)}
3468
3455
  onOpenStateChange={
@@ -3783,15 +3770,6 @@ function renderFoldedGroups(
3783
3770
  ));
3784
3771
  }
3785
3772
 
3786
- function compactedLandmarkCount(groups: readonly TimelineGroup[]): number {
3787
- return groups.filter(
3788
- (group) =>
3789
- group.kind === "item" &&
3790
- group.item.kind === "context-compaction" &&
3791
- group.item.phase === "compacted",
3792
- ).length;
3793
- }
3794
-
3795
3773
  /**
3796
3774
  * Body under a turn/activity chip. Remount flashes are gated by the timeline
3797
3775
  * seen-activity-id map (not by killing entrance): FoldBody used to force
@@ -3892,18 +3870,6 @@ function isAgentProgress(next: TimelineGroup | undefined): boolean {
3892
3870
  /** No item still running or streaming — the only state safe to fold live.
3893
3871
  Position alone is a broken proxy: a pending queued message (or any trailing
3894
3872
  item) can sit after the ACTIVE cluster, which must never fold mid-work. */
3895
- function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>): boolean {
3896
- return group.items.every((item) => {
3897
- if (item.kind === "reasoning" || item.kind === "agent-message") {
3898
- return !item.streaming;
3899
- }
3900
- // Memory writes and fleet observations are discrete, already-settled events.
3901
- if (item.kind === "memory" || item.kind === "fleet-decision") {
3902
- return true;
3903
- }
3904
- return item.status !== "running";
3905
- });
3906
- }
3907
3873
 
3908
3874
  /** Settled activity clusters that could become nested chips under a turn. */
3909
3875
  function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number {
@@ -3916,18 +3882,6 @@ function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number
3916
3882
  return count;
3917
3883
  }
3918
3884
 
3919
- function flattenActivityItems(groups: readonly TimelineGroup[]): ActivityItem[] {
3920
- const items: ActivityItem[] = [];
3921
- for (const group of groups) {
3922
- if (group.kind === "activity") {
3923
- items.push(...flattenActivityItems(group.work?.details ?? []), ...group.items);
3924
- } else if (group.kind === "turn") {
3925
- items.push(...flattenActivityItems(group.groups));
3926
- }
3927
- }
3928
- return items;
3929
- }
3930
-
3931
3885
  /** Assistant prose inside a turn fold (mid-turn narration), joined for copy. */
3932
3886
  function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
3933
3887
  const parts: string[] = [];
@@ -4026,15 +3980,6 @@ function collectTurnCopyText(
4026
3980
  return parts.join("\n\n");
4027
3981
  }
4028
3982
 
4029
- function durationBetween(startedAt: string, endedAt: string): number | undefined {
4030
- const started = Date.parse(startedAt);
4031
- const ended = Date.parse(endedAt);
4032
- if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
4033
- return undefined;
4034
- }
4035
- return ended - started;
4036
- }
4037
-
4038
3983
  /* --- single rows ------------------------------------------------------------ */
4039
3984
 
4040
3985
  /**
@@ -4235,7 +4180,7 @@ function UserMessageRow({
4235
4180
  renderMessageText(item.text, item)
4236
4181
  ) : (
4237
4182
  <UserMessageBody messageId={item.id} text={item.text}>
4238
- <Markdown>{item.text}</Markdown>
4183
+ <Markdown softLineBreaks>{item.text}</Markdown>
4239
4184
  </UserMessageBody>
4240
4185
  )}
4241
4186
  </div>
@@ -4445,7 +4390,9 @@ function AgentMessageRow({
4445
4390
  const body = renderMessageText ? (
4446
4391
  renderMessageText(item.text, item)
4447
4392
  ) : (
4448
- <Markdown streaming={item.streaming}>{item.text}</Markdown>
4393
+ <Markdown softLineBreaks streaming={item.streaming}>
4394
+ {item.text}
4395
+ </Markdown>
4449
4396
  );
4450
4397
  // While streaming, copy is still useful (current text) but keep chrome calm —
4451
4398
  // stamp only after the message finishes (occurredAt tracks completion).
@@ -4934,11 +4881,12 @@ function NoticeRow({ item }: { item: NoticeItem }) {
4934
4881
  </details>
4935
4882
  );
4936
4883
  }
4937
- const resolvedApproval = Boolean(item.resolvedAt && item.text.startsWith("Approval needed"));
4884
+ const presentedTone = noticeTone(item);
4885
+ const resolvedApproval = noticeIsResolvedApproval(item);
4938
4886
  const tone =
4939
- item.tone === "failed"
4887
+ presentedTone === "failed"
4940
4888
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
4941
- : item.tone === "waiting" && !item.resolvedAt
4889
+ : presentedTone === "waiting"
4942
4890
  ? WAITING_PILL_CLASS
4943
4891
  : NEUTRAL_PILL;
4944
4892
  return (
@@ -4958,9 +4906,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
4958
4906
  />
4959
4907
  )}
4960
4908
  <div className="min-w-0 flex-1">
4961
- <span className="whitespace-pre-wrap break-words">
4962
- {resolvedApproval ? "You responded to this approval." : item.text}
4963
- </span>
4909
+ <span className="whitespace-pre-wrap break-words">{noticeDisplayText(item)}</span>
4964
4910
  {item.details ? (
4965
4911
  <details className="mt-2 text-og-control">
4966
4912
  <summary className="cursor-pointer font-medium">{item.details.label}</summary>
@@ -1,3 +1,4 @@
1
+ import { CHATGPT_MARK_PATH } from "../model-mark-paths";
1
2
  import {
2
3
  modelDisplayName,
3
4
  modelLogoUrl,
@@ -15,7 +16,7 @@ import { GrokMark } from "./grok-mark";
15
16
  export function ChatGptMark(props: SVGProps<SVGSVGElement>) {
16
17
  return (
17
18
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
18
- <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3653-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8414 3.3698-2.02 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.783-2.7622a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
19
+ <path d={CHATGPT_MARK_PATH} />
19
20
  </svg>
20
21
  );
21
22
  }
@@ -9,23 +9,17 @@ import {
9
9
  MENU_NOTE_CLASS,
10
10
  MENU_SURFACE_CLASS,
11
11
  } from "../lib/menu-styles";
12
+ import { labelReasoningEffort, type PickerModelRow } from "../model-policy";
12
13
  import {
13
- coerceReasoningEffortForModel,
14
- effortOptionsForModel,
15
- findPickerRow,
16
- groupPickerRowsByBillingClass,
17
- labelReasoningEffort,
18
- payerSummaryForModel,
19
- runnableLatencyModesForModel,
20
- type PickerModelRow,
21
- } from "../model-policy";
14
+ compactEffortLabel,
15
+ groupPresentationFor,
16
+ modelPickerChoice,
17
+ modelPickerMenuView,
18
+ } from "../model-picker-model";
22
19
  import {
23
20
  BillingClassMark,
24
21
  defaultModelPolicyPickerMessages,
25
- effectiveRows,
26
- groupPresentationFor,
27
22
  PickerNavRow,
28
- SCOPED_BILLING_HINTS,
29
23
  type ModelPolicyPickerProps,
30
24
  } from "./model-policy-picker";
31
25
  import { ModelMark } from "./model-mark";
@@ -33,57 +27,18 @@ type ClientPickerModelRow = PickerModelRow<ClientModel>;
33
27
 
34
28
  /** Model selection stays flat; reasoning never becomes a navigation destination. */
35
29
  export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
36
- const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
37
30
  const [query, setQuery] = useState("");
38
- const rows = effectiveRows(props);
39
- const selected = findPickerRow(rows, props.model);
40
- const words = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
41
- const filtered = rows.filter((row) => {
42
- const description = groupPresentationFor(props, row.billingClass)?.description;
43
- const text =
44
- `${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${description === undefined ? payerSummaryForModel(row.catalog) : (description ?? "")}`.toLowerCase();
45
- return words.every((word) => text.includes(word));
46
- });
47
- const matchingIds = new Set(filtered.map((row) => row.id));
48
- const groups = groupPickerRowsByBillingClass(rows, {
49
- codexOnly: props.codexOnly === true,
50
- selectedId: props.model,
51
- collapseScopes: props.collapseScopes !== false,
52
- })
53
- .map((group) => {
54
- const presentation = groupPresentationFor(props, group.billingClass);
55
- const override = presentation?.description;
56
- const defaultHint =
57
- props.collapseScopes === false &&
58
- messages.billingHints[group.billingClass] ===
59
- defaultModelPolicyPickerMessages.billingHints[group.billingClass]
60
- ? (SCOPED_BILLING_HINTS[group.billingClass] ?? messages.billingHints[group.billingClass])
61
- : messages.billingHints[group.billingClass];
62
- return {
63
- ...group,
64
- label: presentation?.label ?? group.label,
65
- rows: group.rows.filter((row) => matchingIds.has(row.id)),
66
- description:
67
- override === undefined
68
- ? group.billingClass === "opengeni_credits"
69
- ? null
70
- : defaultHint
71
- : override,
72
- };
73
- })
74
- .filter((group) => group.rows.length > 0);
31
+ const view = modelPickerMenuView(props, query);
32
+ const { messages, rows, groups } = view;
33
+ const selected = view.selected;
75
34
  const choose = (row: ClientPickerModelRow) => {
76
- if (!row.selectable || props.disabled) return;
77
- if (row.id !== props.model) props.onModelChange(row.id);
78
- const effort = coerceReasoningEffortForModel(row.catalog, props.effort);
35
+ if (props.disabled) return;
36
+ const choice = modelPickerChoice(props, row);
37
+ if (!choice) return;
38
+ if (choice.model !== null) props.onModelChange(choice.model);
79
39
  // Hosts may commit the combined model/effort draft through this callback.
80
- props.onEffortChange(effort);
81
- if (
82
- props.latencyMode !== "standard" &&
83
- !runnableLatencyModesForModel(row.catalog).includes(props.latencyMode)
84
- ) {
85
- props.onLatencyModeChange("standard");
86
- }
40
+ props.onEffortChange(choice.effort);
41
+ if (choice.latencyMode !== null) props.onLatencyModeChange(choice.latencyMode);
87
42
  };
88
43
  const modelRow = (row: ClientPickerModelRow) => (
89
44
  <PickerNavRow
@@ -214,9 +169,9 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
214
169
  {group.rows.map(modelRow)}
215
170
  </section>
216
171
  ))}
217
- {filtered.length === 0 ? (
172
+ {view.matchCount === 0 ? (
218
173
  <p className={MENU_NOTE_CLASS}>
219
- {words.length ? messages.noMatches : messages.noModels}
174
+ {view.searching ? messages.noMatches : messages.noModels}
220
175
  </p>
221
176
  ) : null}
222
177
  </>
@@ -226,17 +181,9 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
226
181
  <p className={MENU_NOTE_CLASS}>{messages.noModels}</p>
227
182
  )}
228
183
  </div>
229
- {rows.some((row) => row.selectable) &&
230
- selected &&
231
- ((props.hasImageAttachments &&
232
- selected.catalog.capabilities?.inputModalities.includes("image") === false) ||
233
- (effortOptionsForModel(selected.catalog).length > 1 &&
234
- selected.catalog.capabilities?.reasoning.runnable !== false) ||
235
- (props.allowLatencyMode !== false &&
236
- runnableLatencyModesForModel(selected.catalog).includes("fast"))) ? (
184
+ {view.anySelectable && selected && (view.unsupportedAttachments || view.thinking) ? (
237
185
  <div className="border-t border-og-border px-2.5 py-2.5">
238
- {props.hasImageAttachments &&
239
- selected.catalog.capabilities?.inputModalities.includes("image") === false ? (
186
+ {view.unsupportedAttachments ? (
240
187
  <p className="pb-1.5 text-og-control leading-relaxed text-og-fg-subtle">
241
188
  {messages.unsupportedAttachments}
242
189
  </p>
@@ -323,15 +270,13 @@ function ConnectModelsPanel(props: {
323
270
  }
324
271
 
325
272
  function ModelThinkingControls(props: ModelPolicyPickerProps) {
326
- const selected = findPickerRow(effectiveRows(props), props.model);
327
- if (!selected) return null;
328
- const efforts = effortOptionsForModel(selected.catalog);
329
- const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
330
- const supportsFast = runnableLatencyModesForModel(selected.catalog).includes("fast");
331
- const showThinking =
332
- efforts.length > 1 && selected.catalog.capabilities?.reasoning.runnable !== false;
333
- const showFast = supportsFast && props.allowLatencyMode !== false;
334
- if (!showThinking && !showFast) return null;
273
+ const view = modelPickerMenuView(props, "");
274
+ const thinking = view.thinking;
275
+ if (!view.selected || !thinking) return null;
276
+ const messages = view.messages;
277
+ const efforts = thinking.efforts;
278
+ const showThinking = thinking.showThinking;
279
+ const showFast = thinking.showFast;
335
280
  return (
336
281
  <div className="space-y-2 text-og-control" data-testid="model-picker-reasoning">
337
282
  <div className="flex min-h-7 items-center justify-between gap-3">
@@ -340,7 +285,7 @@ function ModelThinkingControls(props: ModelPolicyPickerProps) {
340
285
  <button
341
286
  type="button"
342
287
  data-testid="model-picker-fast"
343
- disabled={props.disabled || !selected.selectable}
288
+ disabled={props.disabled || thinking.disabled}
344
289
  aria-pressed={props.latencyMode === "fast"}
345
290
  title={messages.fast + " · " + messages.fastRateHint}
346
291
  onClick={() =>
@@ -357,8 +302,8 @@ function ModelThinkingControls(props: ModelPolicyPickerProps) {
357
302
  {showThinking ? (
358
303
  <RadioGroup.Root
359
304
  aria-label={messages.thinkingEffort}
360
- value={coerceReasoningEffortForModel(selected.catalog, props.effort)}
361
- disabled={props.disabled || !selected.selectable}
305
+ value={thinking.value}
306
+ disabled={props.disabled || thinking.disabled}
362
307
  onValueChange={(value) => props.onEffortChange(value as ReasoningEffort)}
363
308
  orientation="horizontal"
364
309
  className="flex flex-wrap gap-0.5 rounded-og-md bg-og-surface-2 p-0.5"
@@ -371,7 +316,7 @@ function ModelThinkingControls(props: ModelPolicyPickerProps) {
371
316
  title={labelReasoningEffort(effort)}
372
317
  className="min-h-8 min-w-10 flex-1 cursor-pointer whitespace-nowrap rounded-og-sm px-1.5 text-og-control text-og-fg-muted outline-hidden transition-colors hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 data-[state=checked]:bg-og-surface-1 data-[state=checked]:text-og-fg data-[state=checked]:shadow-sm disabled:cursor-not-allowed disabled:opacity-50"
373
318
  >
374
- {effort === "xhigh" ? "X-high" : labelReasoningEffort(effort)}
319
+ {compactEffortLabel(effort, labelReasoningEffort(effort))}
375
320
  </RadioGroup.Item>
376
321
  ))}
377
322
  </RadioGroup.Root>